from PyQt4.QtCore import * from PyQt4.QtGui import * import math #import sys, traceback YesNo = NoYes = ("No", "Yes") groupBoxMargin = 7 import os.path def id_generator(id): while True: id += 1 yield id OrangeUserRole = id_generator(Qt.UserRole) enter_icon = None def getdeepattr(obj, attr, **argkw): if type(obj) == dict: return obj.get(attr) try: return reduce(lambda o, n: getattr(o, n), attr.split("."), obj) except: # I (JD) commented this out. This is ugly and dangerous. # If any widget wants this behavour, it should redefine its __getattr__ to return defaults. # if argkw.has_key("default"): # return argkw["default"] # else: raise AttributeError, "'%s' has no attribute '%s'" % (obj, attr) def getEnterIcon(): global enter_icon if not enter_icon: enter_icon = QIcon(os.path.dirname(__file__) + "/icons/Dlg_enter.png") return enter_icon # constructs a box (frame) if not none, and returns the right master widget def widgetBox(widget, box=None, orientation='vertical', addSpace=False, sizePolicy = None, margin = -1, spacing = -1, flat = 0, addToLayout = 1): if box: b = QGroupBox(widget) if type(box) in (str, unicode): # if you pass 1 for box, there will be a box, but no text b.setTitle(" "+box.strip()+" ") if margin == -1: margin = groupBoxMargin b.setFlat(flat) else: b = QWidget(widget) if margin == -1: margin = 0 if addToLayout and widget.layout() is not None: widget.layout().addWidget(b) if isinstance(orientation, QLayout): b.setLayout(orientation) elif orientation == 'horizontal' or not orientation: b.setLayout(QHBoxLayout()) ## b.setSizePolicy(sizePolicy or QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)) else: b.setLayout(QVBoxLayout()) ## b.setSizePolicy(sizePolicy or QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)) if sizePolicy: b.setSizePolicy(sizePolicy) if spacing == -1: spacing = 4 b.layout().setSpacing(spacing) if margin != -1: b.layout().setMargin(margin) if addSpace and isinstance(addSpace, bool): separator(widget) elif addSpace and isinstance(addSpace, int): separator(widget, addSpace, addSpace) elif addSpace: separator(widget) return b def indentedBox(widget, sep=20, orientation = True, addSpace=False): r = widgetBox(widget, orientation = "horizontal", spacing=0) separator(r, sep, 0) if addSpace and isinstance(addSpace, bool): separator(widget) elif addSpace and isinstance(addSpace, int): separator(widget, 0, addSpace) elif addSpace: separator(widget) return widgetBox(r, orientation = orientation) def widgetLabel(widget, label=None, labelWidth=None, addToLayout = 1): if label is not None: lbl = QLabel(label, widget) if labelWidth: lbl.setFixedSize(labelWidth, lbl.sizeHint().height()) if widget.layout() is not None and addToLayout: widget.layout().addWidget(lbl) else: lbl = None return lbl import re __re_frmt = re.compile(r"(^|[^%])%\((?P<value>[a-zA-Z]\w*)\)") def label(widget, master, label, labelWidth = None): lbl = QLabel("", widget) if widget.layout() is not None: widget.layout().addWidget(lbl) reprint = CallFrontLabel(lbl, label, master) for mo in __re_frmt.finditer(label): master.controlledAttributes[mo.group("value")] = reprint reprint() if labelWidth: lbl.setFixedSize(labelWidth, lbl.sizeHint().height()) return lbl class SpinBoxWFocusOut(QSpinBox): def __init__(self, min, max, step, bi): QSpinBox.__init__(self, bi) self.setRange(min, max) self.setSingleStep(step) self.inSetValue = False self.enterButton = None def onChange(self, value): if not self.inSetValue: self.placeHolder.hide() self.enterButton.show() def onEnter(self): if self.enterButton.isVisible(): self.enterButton.hide() self.placeHolder.show() if self.cback: self.cback(int(str(self.text()))) if self.cfunc: self.cfunc() # doesn't work: it's probably LineEdit's focusOut that we should (and can't) catch def focusOutEvent(self, *e): QSpinBox.focusOutEvent(self, *e) if self.enterButton and self.enterButton.isVisible(): self.onEnter() def setValue(self, value): self.inSetValue = True QSpinBox.setValue(self, value) self.inSetValue = False def checkWithSpin(widget, master, label, min, max, checked, value, posttext = None, step = 1, tooltip=None, checkCallback=None, spinCallback=None, getwidget=None, labelWidth=None, debuggingEnabled = 1, controlWidth=55, callbackOnReturn = False): return spin(widget, master, value, min, max, step, None, label, labelWidth, 0, tooltip, spinCallback, debuggingEnabled, controlWidth, callbackOnReturn, checked, checkCallback, posttext) def spin(widget, master, value, min, max, step=1, box=None, label=None, labelWidth=None, orientation=None, tooltip=None, callback=None, debuggingEnabled = 1, controlWidth = None, callbackOnReturn = False, checked = "", checkCallback = None, posttext = None, addToLayout=True, alignment = Qt.AlignLeft, keyboardTracking=True): if box or label and not checked: b = widgetBox(widget, box, orientation) hasHBox = orientation == 'horizontal' or not orientation else: b = widget hasHBox = False if not hasHBox and (checked or callback and callbackOnReturn or posttext): bi = widgetBox(b, "", 0) else: bi = b if checked: wb = checkBox(bi, master, checked, label, labelWidth = labelWidth, callback=checkCallback, debuggingEnabled = debuggingEnabled) elif label: b.label = widgetLabel(b, label, labelWidth) wa = bi.control = SpinBoxWFocusOut(min, max, step, bi) wa.setAlignment(alignment) wa.setKeyboardTracking(keyboardTracking) # If false it wont emit valueChanged signals while editing the text if addToLayout and bi.layout() is not None: bi.layout().addWidget(wa) # must be defined because of the setText below if controlWidth: wa.setFixedWidth(controlWidth) if tooltip: wa.setToolTip(tooltip) if value: wa.setValue(getdeepattr(master, value)) cfront, wa.cback, wa.cfunc = connectControl(wa, master, value, callback, not (callback and callbackOnReturn) and "valueChanged(int)", CallFrontSpin(wa)) if checked: wb.disables = [wa] wb.makeConsistent() if callback and callbackOnReturn: wa.enterButton, wa.placeHolder = enterButton(bi, wa.sizeHint().height()) QObject.connect(wa, SIGNAL("valueChanged(const QString &)"), wa.onChange) QObject.connect(wa, SIGNAL("editingFinished()"), wa.onEnter) QObject.connect(wa.enterButton, SIGNAL("clicked()"), wa.onEnter) if hasattr(wa, "upButton"): QObject.connect(wa.upButton(), SIGNAL("clicked()"), lambda c=wa.editor(): c.setFocus()) QObject.connect(wa.downButton(), SIGNAL("clicked()"), lambda c=wa.editor(): c.setFocus()) if posttext: widgetLabel(bi, posttext) if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("spin", wa, value, min, max, step, callback)] if checked: return wb, wa else: return b class DoubleSpinBoxWFocusOut(QDoubleSpinBox): def __init__(self, min, max, step, bi): QDoubleSpinBox.__init__(self, bi) self.setDecimals(math.ceil(-math.log10(step))) self.setRange(min, max) self.setSingleStep(step) self.inSetValue = False self.enterButton = None def onChange(self, value): if not self.inSetValue: self.placeHolder.hide() self.enterButton.show() def onEnter(self): if self.enterButton.isVisible(): self.enterButton.hide() self.placeHolder.show() if self.cback: self.cback(float(str(self.text()).replace(",", "."))) if self.cfunc: self.cfunc() # doesn't work: it's probably LineEdit's focusOut that we should (and can't) catch def focusOutEvent(self, *e): QDoubleSpinBox.focusOutEvent(self, *e) if self.enterButton and self.enterButton.isVisible(): self.onEnter() def setValue(self, value): self.inSetValue = True QDoubleSpinBox.setValue(self, value) self.inSetValue = False def doubleSpin(widget, master, value, min, max, step=1, box=None, label=None, labelWidth=None, orientation=None, tooltip=None, callback=None, debuggingEnabled = 1, controlWidth = None, callbackOnReturn = False, checked = "", checkCallback = None, posttext = None, addToLayout=True, alignment = Qt.AlignLeft, keyboardTracking=True, decimals=None): if box or label and not checked: b = widgetBox(widget, box, orientation) hasHBox = orientation == 'horizontal' or not orientation else: b = widget hasHBox = False if not hasHBox and (checked or callback and callbackOnReturn or posttext): bi = widgetBox(b, "", 0) else: bi = b if checked: wb = checkBox(bi, master, checked, label, labelWidth = labelWidth, callback=checkCallback, debuggingEnabled = debuggingEnabled) elif label: widgetLabel(b, label, labelWidth) wa = bi.control = DoubleSpinBoxWFocusOut(min, max, step, bi) if decimals is not None: wa.setDecimals(decimals) wa.setAlignment(alignment) wa.setKeyboardTracking(keyboardTracking) # If false it wont emit valueChanged signals while editing the text if addToLayout and bi.layout() is not None: bi.layout().addWidget(wa) # must be defined because of the setText below if controlWidth: wa.setFixedWidth(controlWidth) if tooltip: wa.setToolTip(tooltip) if value: wa.setValue(getdeepattr(master, value)) cfront, wa.cback, wa.cfunc = connectControl(wa, master, value, callback, not (callback and callbackOnReturn) and "valueChanged(double)", CallFrontDoubleSpin(wa)) if checked: wb.disables = [wa] wb.makeConsistent() if callback and callbackOnReturn: wa.enterButton, wa.placeHolder = enterButton(bi, wa.sizeHint().height()) QObject.connect(wa, SIGNAL("valueChanged(const QString &)"), wa.onChange) QObject.connect(wa, SIGNAL("editingFinished()"), wa.onEnter) QObject.connect(wa.enterButton, SIGNAL("clicked()"), wa.onEnter) if hasattr(wa, "upButton"): QObject.connect(wa.upButton(), SIGNAL("clicked()"), lambda c=wa.editor(): c.setFocus()) QObject.connect(wa.downButton(), SIGNAL("clicked()"), lambda c=wa.editor(): c.setFocus()) if posttext: widgetLabel(bi, posttext) ## if debuggingEnabled and hasattr(master, "_guiElements"): ## master._guiElements = getattr(master, "_guiElements", []) + [("spin", wa, value, min, max, step, callback)] if checked: return wb, wa else: if b==widget: wa.control = b.control # Backward compatibility return wa else: return b def checkBox(widget, master, value, label, box=None, tooltip=None, callback=None, getwidget=None, id=None, disabled=0, labelWidth=None, disables = [], addToLayout = 1, debuggingEnabled = 1): if box: b = widgetBox(widget, box, orientation=None) else: b = widget wa = QCheckBox(label, b) if addToLayout and b.layout() is not None: b.layout().addWidget(wa) if labelWidth: wa.setFixedSize(labelWidth, wa.sizeHint().height()) wa.setChecked(getdeepattr(master, value)) if disabled: wa.setDisabled(1) if tooltip: wa.setToolTip(tooltip) cfront, cback, cfunc = connectControl(wa, master, value, None, "toggled(bool)", CallFrontCheckBox(wa), cfunc = callback and FunctionCallback(master, callback, widget=wa, getwidget=getwidget, id=id)) wa.disables = disables or [] # need to create a new instance of list (in case someone would want to append...) wa.makeConsistent = Disabler(wa, master, value) QObject.connect(wa, SIGNAL("toggled(bool)"), wa.makeConsistent) wa.makeConsistent.__call__(value) if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("checkBox", wa, value, callback)] return wa def enterButton(parent, height, placeholder = True): button = QToolButton(parent) button.setFixedSize(height, height) button.setIcon(getEnterIcon()) if parent.layout() is not None: parent.layout().addWidget(button) if not placeholder: return button button.hide() holder = QWidget(parent) holder.setFixedSize(height, height) if parent.layout() is not None: parent.layout().addWidget(holder) return button, holder class LineEditWFocusOut(QLineEdit): def __init__(self, parent, master, callback, focusInCallback=None, placeholder=False): QLineEdit.__init__(self, parent) if parent.layout() is not None: parent.layout().addWidget(self) self.callback = callback self.focusInCallback = focusInCallback if placeholder: self.enterButton, self.placeHolder = enterButton(parent, self.sizeHint().height(), placeholder) else: self.enterButton = enterButton(parent, self.sizeHint().height(), placeholder) self.placeHolder = None QObject.connect(self.enterButton, SIGNAL("clicked()"), self.returnPressed) QObject.connect(self, SIGNAL("textChanged(const QString &)"), self.markChanged) QObject.connect(self, SIGNAL("returnPressed()"), self.returnPressed) def markChanged(self, *e): if self.placeHolder: self.placeHolder.hide() self.enterButton.show() def markUnchanged(self, *e): self.enterButton.hide() if self.placeHolder: self.placeHolder.show() def returnPressed(self): if self.enterButton.isVisible(): self.markUnchanged() if hasattr(self, "cback") and self.cback: self.cback(self.text()) if self.callback: self.callback() def setText(self, t): QLineEdit.setText(self, t) if self.enterButton: self.markUnchanged() def focusOutEvent(self, *e): QLineEdit.focusOutEvent(self, *e) self.returnPressed() def focusInEvent(self, *e): if self.focusInCallback: self.focusInCallback() return QLineEdit.focusInEvent(self, *e) def lineEdit(widget, master, value, label=None, labelWidth=None, orientation='vertical', box=None, tooltip=None, callback=None, valueType = unicode, validator=None, controlWidth = None, callbackOnType = False, focusInCallback = None, enterPlaceholder=False, **args): if box or label: b = widgetBox(widget, box, orientation) widgetLabel(b, label, labelWidth) hasHBox = orientation == 'horizontal' or not orientation else: b = widget hasHBox = False if args.has_key("baseClass"): wa = args["baseClass"](b) wa.enterButton = None if b and b.layout() is not None: b.layout().addWidget(wa) elif focusInCallback or callback and not callbackOnType: if not hasHBox: bi = widgetBox(b, "", 0) else: bi = b wa = LineEditWFocusOut(bi, master, callback, focusInCallback, enterPlaceholder) else: wa = QLineEdit(b) wa.enterButton = None if b and b.layout() is not None: b.layout().addWidget(wa) if value: wa.setText(unicode(getdeepattr(master, value))) if controlWidth: wa.setFixedWidth(controlWidth) if tooltip: wa.setToolTip(tooltip) if validator: wa.setValidator(validator) if value: wa.cback = connectControl(wa, master, value, callbackOnType and callback, "textChanged(const QString &)", CallFrontLineEdit(wa), fvcb = value and valueType)[1] wa.box = b return wa def button(widget, master, label, callback = None, disabled=0, tooltip=None, debuggingEnabled = 1, width = None, height = None, toggleButton = False, value = "", addToLayout = 1, default=False, autoDefault=False): btn = QPushButton(label, widget) if addToLayout and widget.layout() is not None: widget.layout().addWidget(btn) if width: btn.setFixedWidth(width) if height: btn.setFixedHeight(height) btn.setDisabled(disabled) if tooltip: btn.setToolTip(tooltip) if toggleButton or value: btn.setCheckable(True) btn.setDefault(default) btn.setAutoDefault(autoDefault) if value: btn.setChecked(getdeepattr(master, value)) cfront, cback, cfunc = connectControl(btn, master, value, None, "toggled(bool)", CallFrontButton(btn), cfunc = callback and FunctionCallback(master, callback, widget=btn)) elif callback: QObject.connect(btn, SIGNAL("clicked()"), callback) if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("button", btn, callback)] return btn def toolButton(widget, master, label="", callback = None, width = None, height = None, tooltip = None, addToLayout = 1, debuggingEnabled = 1): if not isinstance(label, basestring) and hasattr(label, "__call__"): import warnings warnings.warn("Third positional argument to 'OWGUI.toolButton' must be a string.", DeprecationWarning) label, callback = "", label btn = QToolButton(widget) if addToLayout and widget.layout() is not None: widget.layout().addWidget(btn) if label: btn.setText(label) if width != None: btn.setFixedWidth(width) if height!= None: btn.setFixedHeight(height) if tooltip != None: btn.setToolTip(tooltip) if callback: QObject.connect(btn, SIGNAL("clicked()"), callback) if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("button", btn, callback)] return btn def separator(widget, width=4, height=4): # if isinstance(widget.layout(), QVBoxLayout): # return widget.layout().addSpacing(height) # elif isinstance(widget.layout(), QHBoxLayout): # return widget.layout().addSpacing(width) # return None sep = QWidget(widget) # sep.setStyleSheet("background: #000000;") if widget.layout() is not None: widget.layout().addWidget(sep) sep.setFixedSize(width, height) return sep def rubber(widget): widget.layout().addStretch(100) def createAttributePixmap(char, color = Qt.black): pixmap = QPixmap(13,13) painter = QPainter() painter.begin(pixmap) painter.setPen( color ); painter.setBrush( color ); painter.drawRect( 0, 0, 13, 13 ); painter.setPen( QColor(Qt.white)) painter.drawText(3, 11, char) painter.end() return QIcon(pixmap) attributeIconDict = None def getAttributeIcons(): import orange global attributeIconDict if not attributeIconDict: attributeIconDict = {orange.VarTypes.Continuous: createAttributePixmap("C", QColor(202,0,32)), orange.VarTypes.Discrete: createAttributePixmap("D", QColor(26,150,65)), orange.VarTypes.String: createAttributePixmap("S", Qt.black), -1: createAttributePixmap("?", QColor(128, 128, 128))} return attributeIconDict def 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): bg = box and widgetBox(widget, box, orientation = "horizontal") or widget lb = OrangeListBox(master, value, enableDragDrop, dragDropCallback, dataValidityCallback, sizeHint, bg) lb.box = bg lb.setSelectionMode(selectionMode) if bg.layout() is not None: bg.layout().addWidget(lb) if value != None: clist = getdeepattr(master, value) if type(clist) >= ControlledList: clist = ControlledList(clist, lb) master.__setattr__(value, clist) lb.ogValue = value lb.ogLabels = labels lb.ogMaster = master if tooltip: lb.setToolTip(tooltip) connectControl(lb, master, value, callback, "itemSelectionChanged()", CallFrontListBox(lb), CallBackListBox(lb, master)) if hasattr(master, "controlledAttributes") and labels != None: master.controlledAttributes[labels] = CallFrontListBoxLabels(lb) if labels != None: setattr(master, labels, getdeepattr(master, labels)) if value != None: setattr(master, value, getdeepattr(master, value)) if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("listBox", lb, value, callback)] return lb # btnLabels is a list of either char strings or pixmaps def radioButtonsInBox(widget, master, value, btnLabels, box=None, tooltips=None, callback=None, debuggingEnabled = 1, addSpace = False, orientation = 'vertical', label = None): if box: bg = widgetBox(widget, box, orientation) else: bg = widget bg.group = QButtonGroup(bg) if addSpace: separator(widget) if not label is None: widgetLabel(bg, label) bg.buttons = [] bg.ogValue = value for i in range(len(btnLabels)): appendRadioButton(bg, master, value, btnLabels[i], tooltips and tooltips[i], callback = callback) connectControl(bg.group, master, value, callback, "buttonClicked (int)", CallFrontRadioButtons(bg), CallBackRadioButton(bg, master)) if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("radioButtonsInBox", bg, value, callback)] return bg def appendRadioButton(bg, master, value, label, tooltip = None, insertInto = None, callback = None, addToLayout=True): dest = insertInto or bg if not hasattr(bg, "buttons"): bg.buttons = [] i = len(bg.buttons) if type(label) in (str, unicode): w = QRadioButton(label) else: w = QRadioButton(unicode(i)) w.setIcon(QIcon(label)) #w.ogValue = value if addToLayout and dest.layout() is not None: dest.layout().addWidget(w) if not hasattr(bg, "group"): bg.group = QButtonGroup(bg) bg.group.addButton(w) w.setChecked(getdeepattr(master, value) == i) bg.buttons.append(w) # if callback == None and hasattr(bg, "callback"): # callback = bg.callback # if callback != None: # connectControl(w, master, value, callback, "clicked()", CallFrontRadioButtons(bg), CallBackRadioButton(w, master, bg)) if tooltip: w.setToolTip(tooltip) return w #def radioButton(widget, master, value, label, box = None, tooltip = None, callback = None, debuggingEnabled = 1): # if box: # bg = widgetBox(widget, box, orientation="horizontal") # else: # bg = widget # # if type(label) in (str, unicode): # w = QRadioButton(label, bg) # else: # w = QRadioButton("X") # w.setPixmap(label) # if bg.layout(): bg.layout().addWidget(w) # # w.setChecked(getdeepattr(master, value)) # if tooltip: # w.setToolTip(tooltip) # # connectControl(w, master, value, callback, "stateChanged(int)", CallFrontCheckBox(w)) # if debuggingEnabled and hasattr(master, "_guiElements"): # master._guiElements = getattr(master, "_guiElements", []) + [("radioButton", w, value, callback)] # return w def 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): sliderBox = widgetBox(widget, box, orientation = "horizontal") if label: lbl = widgetLabel(sliderBox, label) if vertical: sliderOrient = Qt.Vertical else: sliderOrient = Qt.Horizontal if intOnly: slider = QSlider(sliderOrient, sliderBox) slider.setRange(minValue, maxValue) if step != 0: slider.setSingleStep(step) slider.setPageStep(step) slider.setTickInterval(step) signal_signature = "valueChanged(int)" else: slider = FloatSlider(sliderOrient, minValue, maxValue, step) signal_signature = "valueChangedFloat(double)" slider.setValue(getdeepattr(master, value)) if tooltip: slider.setToolTip(tooltip) if width != None: slider.setFixedWidth(width) if sliderBox.layout() is not None: sliderBox.layout().addWidget(slider) if ticks: slider.setTickPosition(QSlider.TicksBelow) slider.setTickInterval(ticks) if createLabel: label = QLabel(sliderBox) if sliderBox.layout() is not None: sliderBox.layout().addWidget(label) label.setText(labelFormat % minValue) width1 = label.sizeHint().width() label.setText(labelFormat % maxValue) width2 = label.sizeHint().width() label.setFixedSize(max(width1, width2), label.sizeHint().height()) txt = labelFormat % (getdeepattr(master, value)/divideFactor) label.setText(txt) label.setLbl = lambda x, l=label, f=labelFormat: l.setText(f % (x/divideFactor)) QObject.connect(slider, SIGNAL(signal_signature), label.setLbl) connectControl(slider, master, value, callback, signal_signature, CallFrontHSlider(slider)) # For compatibility with qwtSlider slider.box = sliderBox if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("hSlider", slider, value, minValue, maxValue, step, callback)] return slider def 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): if not logarithmic: if type(precision) == str: format = precision elif precision == 0: format = " %d" else: format = " %s.%df" % ("%", precision) return hSlider(widget, master, value, box, minValue, maxValue, step, callback, label=label, labelFormat=format, width=maxWidth, tooltip=tooltip, debuggingEnabled=debuggingEnabled, intOnly=0) import PyQt4.Qwt5 as qwt init = getdeepattr(master, value) if label: hb = widgetBox(widget, box, orientation) lbl = widgetLabel(hb, label) if labelWidth: lbl.setFixedSize(labelWidth, lbl.sizeHint().height()) if orientation and orientation!="horizontal": separator(hb, height=2) hb = widgetBox(hb, 0) else: hb = widgetBox(widget, box, 0) if ticks: slider = qwt.QwtSlider(hb, Qt.Horizontal, qwt.QwtSlider.Bottom, qwt.QwtSlider.BgSlot) else: slider = qwt.QwtSlider(hb, Qt.Horizontal, qwt.QwtSlider.NoScale, qwt.QwtSlider.BgSlot) hb.layout().addWidget(slider) slider.setScale(minValue, maxValue, logarithmic) # the third parameter for logaritmic scale slider.setScaleMaxMinor(10) slider.setThumbWidth(20) slider.setThumbLength(12) if maxWidth: slider.setMaximumSize(maxWidth,40) if logarithmic: slider.setRange(math.log10(minValue), math.log10(maxValue), step) slider.setValue(math.log10(init)) else: slider.setRange(minValue, maxValue, step) slider.setValue(init) if tooltip: hb.setToolTip(tooltip) ## format = "%s%d.%df" % ("%", precision+3, precision) # format = " %s.%df" % ("%", precision) if type(precision) == str: format = precision else: format = " %s.%df" % ("%", precision) if showValueLabel: lbl = widgetLabel(hb, format % minValue) width1 = lbl.sizeHint().width() lbl.setText(format % maxValue) width2 = lbl.sizeHint().width() lbl.setFixedSize(max(width1, width2), lbl.sizeHint().height()) lbl.setText(format % init) if logarithmic: cfront = CallFrontLogSlider(slider) cback = ValueCallback(master, value, f=lambda x: 10**x) if showValueLabel: QObject.connect(slider, SIGNAL("valueChanged(double)"), SetLabelCallback(master, lbl, format=format, f=lambda x: 10**x)) else: cfront = CallFrontHSlider(slider) cback = ValueCallback(master, value) if showValueLabel: QObject.connect(slider, SIGNAL("valueChanged(double)"), SetLabelCallback(master, lbl, format=format)) connectControl(slider, master, value, callback, "valueChanged(double)", cfront, cback) slider.box = hb if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("qwtHSlider", slider, value, minValue, maxValue, step, callback)] return slider # list box where we can use drag and drop class OrangeListBox(QListWidget): def __init__(self, widget, value = None, enableDragDrop = 0, dragDropCallback = None, dataValidityCallback = None, sizeHint = None, *args): self.widget = widget self.value = value QListWidget.__init__(self, *args) self.enableDragDrop = enableDragDrop self.dragDopCallback = dragDropCallback self.dataValidityCallback = dataValidityCallback if not sizeHint: self.defaultSizeHint = QSize(150,100) else: self.defaultSizeHint = sizeHint if enableDragDrop: self.setDragEnabled(1) self.setAcceptDrops(1) self.setDropIndicatorShown(1) #self.setDragDropMode(QAbstractItemView.DragDrop) self.dragStartPosition = 0 def setAttributes(self, data, attributes): if isinstance(shownAttributes[0], tuple): setattr(self.widget, self.ogLabels, attributes) else: domain = data.domain setattr(self.widget, self.ogLabels, [(domain[a].name, domain[a].varType) for a in attributes]) def sizeHint(self): return self.defaultSizeHint def startDrag(self, supportedActions): if not self.enableDragDrop: return drag = QDrag(self) mime = QMimeData() if not self.ogValue: selectedItems = [i for i in range(self.count()) if self.item(i).isSelected()] else: selectedItems = getdeepattr(self.widget, self.ogValue, default = []) mime.setText(str(selectedItems)) mime.source = self drag.setMimeData(mime) drag.start(Qt.MoveAction) def dragEnterEvent(self, ev): if not self.enableDragDrop: return if self.dataValidityCallback: return self.dataValidityCallback(ev) if ev.mimeData().hasText(): ev.accept() else: ev.ignore() def dragMoveEvent(self, ev): if not self.enableDragDrop: return if self.dataValidityCallback: return self.dataValidityCallback(ev) if ev.mimeData().hasText(): ev.setDropAction(Qt.MoveAction) ev.accept() else: ev.ignore() def dropEvent(self, ev): if not self.enableDragDrop: return if ev.mimeData().hasText(): item = self.itemAt(ev.pos()) if item: index = self.indexFromItem(item).row() else: index = self.count() source = ev.mimeData().source selectedItemIndices = eval(str(ev.mimeData().text())) if self.ogLabels != None and self.ogValue != None: allSourceItems = getdeepattr(source.widget, source.ogLabels, default = []) selectedItems = [allSourceItems[i] for i in selectedItemIndices] allDestItems = getdeepattr(self.widget, self.ogLabels, default = []) if source != self: 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 setattr(self.widget, self.ogLabels, allDestItems[:index] + selectedItems + allDestItems[index:]) setattr(source.widget, source.ogValue, []) # clear selection in the source widget else: items = [item for item in allSourceItems if item not in selectedItems] if index < len(allDestItems): 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 index -= 1 destItem = allDestItems[index] index = items.index(destItem) else: index = max(0, index - len(selectedItems)) setattr(self.widget, self.ogLabels, items[:index] + selectedItems + items[index:]) setattr(self.widget, self.ogValue, range(index, index+len(selectedItems))) else: # if we don't have variables ogValue and ogLabel if source != self: self.insertItems(source.selectedItems()) for index in selectedItemIndices[::-1]: source.takeItem(index) else: if index < self.count(): 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 index -= 1 items = [source.item(i) for i in selectedItemIndices] for ind in selectedItemIndices[::-1]: source.takeItem(ind) if ind <= index: index-= 1 for item in items[::-1]: self.insertItem(index, item) self.clearSelection() for i in range(index, index+len(items)): self.item(i).setSelected(1) if self.dragDopCallback: # call the callback self.dragDopCallback() ev.setDropAction(Qt.MoveAction) ev.accept() else: ev.ignore() def updateGeometries(self): """ A workaround for a bug in Qt (see: http://bugreports.qt.nokia.com/browse/QTBUG-14412) """ if getattr(self, "_updatingGeometriesNow", False): # import sys # print >> sys.stderr, "Suppressing recursive update geometries" return self._updatingGeometriesNow = True try: return QListWidget.updateGeometries(self) finally: self._updatingGeometriesNow = False class SmallWidgetButton(QPushButton): def __init__(self, widget, text = "", pixmap = None, box = None, orientation='vertical', tooltip = None, autoHideWidget = None): #self.parent = parent if pixmap != None: import os iconDir = os.path.join(os.path.dirname(__file__), "icons") if isinstance(pixmap, basestring): if os.path.exists(pixmap): name = pixmap elif os.path.exists(os.path.join(iconDir, pixmap)): name = os.path.join(iconDir, pixmap) elif type(pixmap) == QPixmap or type(pixmap) == QIcon: name = pixmap else: name = os.path.join(iconDir, "arrow_down.png") QPushButton.__init__(self, QIcon(name), text, widget) else: QPushButton.__init__(self, text, widget) if widget.layout() is not None: widget.layout().addWidget(self) if tooltip != None: self.setToolTip(tooltip) # create autohide widget and set a layout if autoHideWidget != None: self.autohideWidget = autoHideWidget(None, Qt.Popup) else: self.autohideWidget = AutoHideWidget(None, Qt.Popup) self.widget = self.autohideWidget if isinstance(orientation, QLayout): self.widget.setLayout(orientation) elif orientation == 'horizontal' or not orientation: self.widget.setLayout(QHBoxLayout()) else: self.widget.setLayout(QVBoxLayout()) #self.widget.layout().setMargin(groupBoxMargin) if box: self.widget = widgetBox(self.widget, box, orientation) #self.setStyleSheet("QPushButton:hover { background-color: #F4F2F0; }") self.autohideWidget.hide() def mousePressEvent(self, ev): QWidget.mousePressEvent(self, ev) if self.autohideWidget.isVisible(): self.autohideWidget.hide() else: #self.widget.move(self.parent.mapToGlobal(QPoint(0, 0)).x(), self.mapToGlobal(QPoint(0, self.height())).y()) self.autohideWidget.move(self.mapToGlobal(QPoint(0, self.height()))) self.autohideWidget.show() class SmallWidgetLabel(QLabel): def __init__(self, widget, text = "", pixmap = None, box = None, orientation='vertical', tooltip = None): QLabel.__init__(self, widget) if text != "": self.setText("<font color=\"#C10004\">" + text + "</font>") elif pixmap != None: import os iconDir = os.path.join(os.path.dirname(__file__), "icons") if isinstance(pixmap, basestring): if os.path.exists(pixmap): name = pixmap elif os.path.exists(os.path.join(iconDir, pixmap)): name = os.path.join(iconDir, pixmap) elif type(pixmap) == QPixmap or type(pixmap) == QIcon: name = pixmap else: name = os.path.join(iconDir, "arrow_down.png") self.setPixmap(QPixmap(name)) if widget.layout() is not None: widget.layout().addWidget(self) if tooltip != None: self.setToolTip(tooltip) self.autohideWidget = self.widget = AutoHideWidget(None, Qt.Popup) if isinstance(orientation, QLayout): self.widget.setLayout(orientation) elif orientation == 'horizontal' or not orientation: self.widget.setLayout(QHBoxLayout()) else: self.widget.setLayout(QVBoxLayout()) if box: self.widget = widgetBox(self.widget, box, orientation) self.autohideWidget.hide() def mousePressEvent(self, ev): QLabel.mousePressEvent(self, ev) if self.autohideWidget.isVisible(): self.autohideWidget.hide() else: #self.widget.move(self.parent.mapToGlobal(QPoint(0, 0)).x(), self.mapToGlobal(QPoint(0, self.height())).y()) self.autohideWidget.move(self.mapToGlobal(QPoint(0, self.height()))) self.autohideWidget.show() class AutoHideWidget(QWidget): # def __init__(self, parent = None): # QWidget.__init__(self, parent, Qt.Popup) def leaveEvent(self, ev): self.hide() class SearchLineEdit(QLineEdit): def __init__(self, t, searcher): QLineEdit.__init__(self, t) self.searcher = searcher def keyPressEvent(self, e): k = e.key() if k == Qt.Key_Down: curItem = self.searcher.lb.currentItem() if curItem+1 < self.searcher.lb.count(): self.searcher.lb.setCurrentItem(curItem+1) elif k == Qt.Key_Up: curItem = self.searcher.lb.currentItem() if curItem: self.searcher.lb.setCurrentItem(curItem-1) elif k == Qt.Key_Escape: self.searcher.window.hide() else: return QLineEdit.keyPressEvent(self, e) class Searcher: def __init__(self, control, master): self.control = control self.master = master def __call__(self): self.window = t = QFrame(self.master, "", QStyle.WStyle_Dialog + QStyle.WStyle_Tool + QStyle.WStyle_Customize + QStyle.WStyle_NormalBorder) la = QVBoxLayout(t).setAutoAdd(1) gs = self.master.mapToGlobal(QPoint(0, 0)) gl = self.control.mapToGlobal(QPoint(0, 0)) t.move(gl.x()-gs.x(), gl.y()-gs.y()) self.allItems = [str(self.control.text(i)) for i in range(self.control.count())] le = SearchLineEdit(t, self) self.lb = QListBox(t) for i in self.allItems: self.lb.insertItem(i) t.setFixedSize(self.control.width(), 200) t.show() le.setFocus() QObject.connect(le, SIGNAL("textChanged(const QString &)"), self.textChanged) QObject.connect(le, SIGNAL("returnPressed()"), self.returnPressed) QObject.connect(self.lb, SIGNAL("clicked(QListBoxItem *)"), self.mouseClicked) def textChanged(self, s): s = str(s) self.lb.clear() for i in self.allItems: if s.lower() in i.lower(): self.lb.insertItem(i) def returnPressed(self): if self.lb.count(): self.conclude(self.lb.text(max(0, self.lb.currentItem()))) else: self.window.hide() def mouseClicked(self, item): self.conclude(item.text()) def conclude(self, valueQStr): value = str(valueQStr) index = self.allItems.index(value) self.control.setCurrentItem(index) if self.control.cback: if self.control.sendSelectedValue: self.control.cback(value) else: self.control.cback(index) if self.control.cfunc: self.control.cfunc() self.window.hide() def 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): hb = widgetBox(widget, box, orientation) widgetLabel(hb, label, labelWidth) if tooltip: hb.setToolTip(tooltip) combo = QComboBox(hb) combo.setEditable(editable) combo.box = hb if addSpace: if isinstance(addSpace, bool): separator(widget) elif isinstance(addSpace, int): separator(widget, height=addSpace) else: separator(widget) if indent: hb = widgetBox(hb, orientation = "horizontal") hb.layout().addSpacing(indent) if hb.layout() is not None and addToLayout: hb.layout().addWidget(combo) if items: combo.addItems([unicode(i) for i in items]) if len(items)>0 and value != None: if sendSelectedValue and getdeepattr(master, value) in items: combo.setCurrentIndex(items.index(getdeepattr(master, value))) elif not sendSelectedValue and getdeepattr(master, value) < combo.count(): combo.setCurrentIndex(getdeepattr(master, value)) elif combo.count() > 0: combo.setCurrentIndex(0) else: combo.setDisabled(True) if value != None: if sendSelectedValue: control2attributeDict = dict(control2attributeDict) if emptyString: control2attributeDict[emptyString] = "" connectControl(combo, master, value, callback, "activated( const QString & )", CallFrontComboBox(combo, valueType, control2attributeDict), ValueCallbackCombo(master, value, valueType, control2attributeDict)) else: connectControl(combo, master, value, callback, "activated(int)", CallFrontComboBox(combo, None, control2attributeDict)) if debuggingEnabled and hasattr(master, "_guiElements"): master._guiElements = getattr(master, "_guiElements", []) + [("comboBox", combo, value, sendSelectedValue, valueType, callback)] return combo def comboBoxWithCaption(widget, master, value, label, box=None, items=None, tooltip=None, callback = None, sendSelectedValue=0, valueType = int, labelWidth = None, debuggingEnabled = 1): hbox = widgetBox(widget, box = box, orientation="horizontal") lab = widgetLabel(hbox, label + " ", labelWidth) combo = comboBox(hbox, master, value, items = items, tooltip = tooltip, callback = callback, sendSelectedValue = sendSelectedValue, valueType = valueType, debuggingEnabled = debuggingEnabled) return combo # 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 class collapsableWidgetBox(QGroupBox): def __init__(self, widget, box = "", master = None, value = "", orientation = "vertical", callback = None): QGroupBox.__init__(self, widget) self.setFlat(1) if orientation == 'vertical': self.setLayout(QVBoxLayout()) else: self.setLayout(QHBoxLayout()) if widget.layout() is not None: widget.layout().addWidget(self) if type(box) in (str, unicode): # if you pass 1 for box, there will be a box, but no text self.setTitle(" " + box.strip() + " ") self.setCheckable(1) self.master = master self.value = value self.callback = callback QObject.connect(self, SIGNAL("clicked()"), self.toggled) def toggled(self, val = 0): if self.value: self.master.__setattr__(self.value, self.isChecked()) self.updateControls() # self.setFlat(1) if self.callback != None: self.callback() def updateControls(self): val = self.master.getdeepattr(self.value) width = self.width() self.setChecked(val) self.setFlat(not val) if not val: self.setMinimumSize(QSize(width, 0)) else: self.setMinimumSize(QSize(0, 0)) for c in self.children(): if isinstance(c, QLayout): continue if val: c.show() else: c.hide() # creates an icon that allows you to show/hide the widgets in the widgets list class widgetHider(QWidget): def __init__(self, widget, master, value, size = (19,19), widgets = [], tooltip = None): QWidget.__init__(self, widget) if widget.layout() is not None: widget.layout().addWidget(self) self.value = value self.master = master if tooltip: self.setToolTip(tooltip) import os iconDir = os.path.join(os.path.dirname(__file__), "icons") icon1 = os.path.join(iconDir, "arrow_down.png") icon2 = os.path.join(iconDir, "arrow_up.png") self.pixmaps = [] self.pixmaps = [QPixmap(icon1), QPixmap(icon2)] self.setFixedSize(self.pixmaps[0].size()) self.disables = widgets or [] # need to create a new instance of list (in case someone would want to append...) self.makeConsistent = Disabler(self, master, value, type = HIDER) if widgets != []: self.setWidgets(widgets) def mousePressEvent(self, ev): self.master.__setattr__(self.value, not getdeepattr(self.master, self.value)) self.makeConsistent.__call__() def setWidgets(self, widgets): self.disables = widgets or [] self.makeConsistent.__call__() def paintEvent(self, ev): QWidget.paintEvent(self, ev) if self.pixmaps != []: pix = self.pixmaps[getdeepattr(self.master, self.value)] painter = QPainter(self) painter.drawPixmap(0, 0, pix) ############################################################################## # callback handlers def setStopper(master, sendButton, stopCheckbox, changedFlag, callback): stopCheckbox.disables.append((-1, sendButton)) sendButton.setDisabled(stopCheckbox.isChecked()) QObject.connect(stopCheckbox, SIGNAL("toggled(bool)"), lambda x, master=master, changedFlag=changedFlag, callback=callback: x and getdeepattr(master, changedFlag, default=True) and callback()) class ControlledList(list): def __init__(self, content, listBox = None): list.__init__(self, content) self.listBox = listBox def __reduce__(self): # cannot pickle self.listBox, but can't discard it (ControlledList may live on) import copy_reg return copy_reg._reconstructor, (list, list, ()), None, self.__iter__() def item2name(self, item): item = self.listBox.labels[item] if type(item) == tuple: return item[1] else: return item def __setitem__(self, index, item): self.listBox.item(list.__getitem__(self, index)).setSelected(0) item.setSelected(1) list.__setitem__(self, index, item) def __delitem__(self, index): self.listBox.item(__getitem__(self, index)).setSelected(0) list.__delitem__(self, index) def __setslice__(self, start, end, slice): for i in list.__getslice__(self, start, end): self.listBox.item(i).setSelected(0) for i in slice: self.listBox.item(i).setSelected(1) list.__setslice__(self, start, end, slice) def __delslice__(self, start, end): if not start and end==len(self): for i in range(self.listBox.count()): self.listBox.item(i).setSelected(0) else: for i in list.__getslice__(self, start, end): self.listBox.item(i).setSelected(0) list.__delslice__(self, start, end) def append(self, item): list.append(self, item) item.setSelected(1) def extend(self, slice): list.extend(self, slice) for i in slice: self.listBox.item(i).setSelected(1) def insert(self, index, item): item.setSelected(1) list.insert(self, index, item) def pop(self, index=-1): self.listBox.item(list.__getitem__(self, index)).setSelected(0) list.pop(self, index) def remove(self, item): item.setSelected(0) list.remove(self, item) def connectControlSignal(control, signal, f): if type(signal) == tuple: control, signal = signal QObject.connect(control, SIGNAL(signal), f) def connectControl(control, master, value, f, signal, cfront, cback = None, cfunc = None, fvcb = None): cback = cback or value and ValueCallback(master, value, fvcb) if cback: if signal: connectControlSignal(control, signal, cback) cback.opposite = cfront if value and cfront and hasattr(master, "controlledAttributes"): master.controlledAttributes[value] = cfront cfunc = cfunc or f and FunctionCallback(master, f) if cfunc: if signal: connectControlSignal(control, signal, cfunc) cfront.opposite = cback, cfunc else: cfront.opposite = (cback,) return cfront, cback, cfunc class ControlledCallback: def __init__(self, widget, attribute, f = None): self.widget = widget self.attribute = attribute self.f = f self.disabled = 0 if type(widget) == dict: return # we can't assign attributes to dict if not hasattr(widget, "callbackDeposit"): widget.callbackDeposit = [] widget.callbackDeposit.append(self) def acyclic_setattr(self, value): if self.disabled: return if isinstance(value, QString): value = unicode(value) if self.f: if self.f in [int, float] and (not value or type(value) in [str, unicode] and value in "+-"): value = self.f(0) else: value = self.f(value) opposite = getattr(self, "opposite", None) if opposite: try: opposite.disabled += 1 if type(self.widget) == dict: self.widget[self.attribute] = value else: setattr(self.widget, self.attribute, value) finally: opposite.disabled -= 1 else: if type(self.widget) == dict: self.widget[self.attribute] = value else: setattr(self.widget, self.attribute, value) class ValueCallback(ControlledCallback): def __call__(self, value): if value is not None: try: self.acyclic_setattr(value) except: print "OWGUI.ValueCallback: %s" % value import traceback, sys traceback.print_exception(*sys.exc_info()) class ValueCallbackCombo(ValueCallback): def __init__(self, widget, attribute, f = None, control2attributeDict = {}): ValueCallback.__init__(self, widget, attribute, f) self.control2attributeDict = control2attributeDict def __call__(self, value): value = unicode(value) return ValueCallback.__call__(self, self.control2attributeDict.get(value, value)) class ValueCallbackLineEdit(ControlledCallback): def __init__(self, control, widget, attribute, f = None): ControlledCallback.__init__(self, widget, attribute, f) self.control = control def __call__(self, value): if value is not None: try: pos = self.control.cursorPosition() self.acyclic_setattr(value) self.control.setCursorPosition(pos) except: print "invalid value ", value, type(value) class SetLabelCallback: def __init__(self, widget, label, format = "%5.2f", f = None): self.widget = widget self.label = label self.format = format self.f = f if hasattr(widget, "callbackDeposit"): widget.callbackDeposit.append(self) self.disabled = 0 def __call__(self, value): if not self.disabled and value is not None: if self.f: value = self.f(value) self.label.setText(self.format % value) class FunctionCallback: def __init__(self, master, f, widget=None, id=None, getwidget=None): self.master = master self.widget = widget self.f = f self.id = id self.getwidget = getwidget if hasattr(master, "callbackDeposit"): master.callbackDeposit.append(self) self.disabled = 0 def __call__(self, *value): if not self.disabled and value!=None: kwds = {} if self.id <> None: kwds['id'] = self.id if self.getwidget: kwds['widget'] = self.widget if isinstance(self.f, list): for f in self.f: f(**kwds) else: self.f(**kwds) class CallBackListBox: def __init__(self, control, widget): self.control = control self.widget = widget self.disabled = 0 def __call__(self, *args): # triggered by selectionChange() if not self.disabled and self.control.ogValue != None: clist = getdeepattr(self.widget, self.control.ogValue) list.__delslice__(clist, 0, len(clist)) control = self.control for i in range(control.count()): if control.item(i).isSelected(): list.append(clist, i) self.widget.__setattr__(self.control.ogValue, clist) class CallBackRadioButton: def __init__(self, control, widget): self.control = control self.widget = widget self.disabled = False def __call__(self, *args): # triggered by toggled() if not self.disabled and self.control.ogValue != None: arr = [butt.isChecked() for butt in self.control.buttons] self.widget.__setattr__(self.control.ogValue, arr.index(1)) ############################################################################## # call fronts (through this a change of the attribute value changes the related control) class ControlledCallFront: def __init__(self, control): self.control = control self.disabled = 0 def __call__(self, *args): if not self.disabled: opposite = getattr(self, "opposite", None) if opposite: try: for op in opposite: op.disabled += 1 self.action(*args) finally: for op in opposite: op.disabled -= 1 else: self.action(*args) class CallFrontSpin(ControlledCallFront): def action(self, value): if value is not None: self.control.setValue(value) class CallFrontDoubleSpin(ControlledCallFront): def action(self, value): if value is not None: self.control.setValue(value) class CallFrontCheckBox(ControlledCallFront): def action(self, value): if value != None: values = [Qt.Unchecked, Qt.Checked, Qt.PartiallyChecked] self.control.setCheckState(values[value]) class CallFrontButton(ControlledCallFront): def action(self, value): if value != None: self.control.setChecked(bool(value)) class CallFrontComboBox(ControlledCallFront): def __init__(self, control, valType = None, control2attributeDict = {}): ControlledCallFront.__init__(self, control) self.valType = valType self.attribute2controlDict = dict([(y, x) for x, y in control2attributeDict.items()]) def action(self, value): if value is not None: value = self.attribute2controlDict.get(value, value) if self.valType: for i in range(self.control.count()): if self.valType(str(self.control.itemText(i))) == value: self.control.setCurrentIndex(i) return values = "" for i in range(self.control.count()): values += str(self.control.itemText(i)) + (i < self.control.count()-1 and ", " or ".") print "unable to set %s to value '%s'. Possible values are %s" % (self.control, value, values) #import traceback #traceback.print_stack() else: if value < self.control.count(): self.control.setCurrentIndex(value) class CallFrontHSlider(ControlledCallFront): def action(self, value): if value is not None: self.control.setValue(value) class CallFrontLogSlider(ControlledCallFront): def action(self, value): if value is not None: if value < 1e-30: print "unable to set ", self.control, "to value ", value, " (value too small)" else: self.control.setValue(math.log10(value)) class CallFrontLineEdit(ControlledCallFront): def action(self, value): self.control.setText(unicode(value)) class CallFrontRadioButtons(ControlledCallFront): def action(self, value): if value < 0 or value >= len(self.control.buttons): value = 0 self.control.buttons[value].setChecked(1) class CallFrontListBox(ControlledCallFront): def action(self, value): if value is not None: if not isinstance(value, ControlledList): setattr(self.control.ogMaster, self.control.ogValue, ControlledList(value, self.control)) for i in range(self.control.count()): shouldBe = i in value if shouldBe != self.control.item(i).isSelected(): self.control.item(i).setSelected(shouldBe) class CallFrontListBoxLabels(ControlledCallFront): def action(self, value): icons = getAttributeIcons() self.control.clear() if value: for i in value: if type(i) == tuple: if isinstance(i[1], int): self.control.addItem(QListWidgetItem(icons.get(i[1], icons[-1]), i[0])) else: self.control.addItem( QListWidgetItem(i[0],i[1]) ) else: self.control.addItem(i) class CallFrontLabel: def __init__(self, control, label, master): self.control = control self.label = label self.master = master def __call__(self, *args): self.control.setText(self.label % self.master.__dict__) ############################################################################## ## Disabler is a call-back class for check box that can disable/enable other ## widgets according to state (checked/unchecked, enabled/disable) of the ## given check box ## ## Tricky: if self.propagateState is True (default), then if check box is ## disabled, the related widgets will be disabled (even if the checkbox is ## checked). If self.propagateState is False, the related widgets will be ## disabled/enabled if check box is checked/clear, disregarding whether the ## check box itself is enabled or not. (If you don't understand, see the code :-) DISABLER = 1 HIDER = 2 class Disabler: def __init__(self, widget, master, valueName, propagateState = 1, type = DISABLER): self.widget = widget self.master = master self.valueName = valueName self.propagateState = propagateState self.type = type def __call__(self, *value): currState = self.widget.isEnabled() if currState or not self.propagateState: if len(value): disabled = not value[0] else: disabled = not getdeepattr(self.master, self.valueName) else: disabled = 1 for w in self.widget.disables: if type(w) == tuple: if isinstance(w[0], int): i = 1 if w[0] == -1: disabled = not disabled else: i = 0 if self.type == DISABLER: w[i].setDisabled(disabled) elif self.type == HIDER: if disabled: w[i].hide() else: w[i].show() if hasattr(w[i], "makeConsistent"): w[i].makeConsistent() else: if self.type == DISABLER: w.setDisabled(disabled) elif self.type == HIDER: if disabled: w.hide() else: w.show() ############################################################################## # some table related widgets class tableItem(QTableWidgetItem): def __init__(self, table, x, y, text, editType = None, backColor=None, icon=None, type = QTableWidgetItem.Type): QTableWidgetItem.__init__(self, type) if icon: self.setIcon(QIcon(icon)) if editType != None: self.setFlags(editType) else: self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable) if backColor != None: self.setBackground(QBrush(backColor)) 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) table.setItem(x, y, self) import orange TableValueRole = OrangeUserRole.next() # Role to retrieve orange.Value TableClassValueRole = OrangeUserRole.next() # Role to retrieve the class value for the row's example TableDistribution = OrangeUserRole.next() # Role to retrieve the distribution of the column's attribute TableVariable = OrangeUserRole.next() # Role to retrieve the column's variable BarRatioRole = OrangeUserRole.next() # Ratio for drawing distribution bars BarBrushRole = OrangeUserRole.next() # Brush for distribution bar SortOrderRole = OrangeUserRole.next() # Used for sorting class TableBarItem(QItemDelegate): BarRole = OrangeUserRole.next() ColorRole = OrangeUserRole.next() def __init__(self, parent, table=None, color=QColor(255, 170, 127), color_schema=None): """ :param parent: OWWidget instance :type parent: :class:`PyQt4.QtCore.QObject` :param table: Table (unused, here for backwards compatibility). :type table: :class:`Orange.data.Table` :param color: Color of the distribution bar. :type color: :class:`PyQt4.QtCore.QColor` :param color_schema: If not None it must be an instance of :class:`OWColorPalette.ColorPaletteGenerator` (note: this parameter, if set, overrides the ``color``). :type color_schema: :class:`OWColorPalette.ColorPaletteGenerator` """ QItemDelegate.__init__(self, parent) self.color = color self.color_schema = color_schema self.table = table def paint(self, painter, option, index): painter.save() self.drawBackground(painter, option, index) ratio, ok = index.data(TableBarItem.BarRole).toDouble() if not ok or ratio > 1.0 or ratio < 0.0 or ratio != ratio: # not a float, out of 0..1 range or a NaN. ratio = None color = self.color if self.color_schema is not None: class_ = index.data(TableClassValueRole).toPyObject() if isinstance(class_, orange.Value) and \ isinstance(class_.variable, orange.EnumVariable) and \ not class_.isSpecial(): color = self.color_schema[int(class_)] if ratio is not None: painter.save() painter.setPen(QPen(QBrush(color), 5, Qt.SolidLine, Qt.RoundCap)) rect = option.rect.adjusted(3, 0, -3, -5) x, y = rect.x(), rect.y() + rect.height() painter.drawLine(x, y, x + rect.width() * ratio, y) painter.restore() # raise the lower edge 3 pixels up text_rect = option.rect.adjusted(0, 0, 0, -3) else: text_rect = option.rect text = index.data(Qt.DisplayRole).toString() self.drawDisplay(painter, option, text_rect, text) painter.restore() class BarItemDelegate(QStyledItemDelegate): def __init__(self, parent, brush=QBrush(QColor(255, 170, 127)), scale=(0.0, 1.0)): QStyledItemDelegate.__init__(self, parent) self.brush = brush self.scale = scale def paint(self, painter, option, index): qApp.style().drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter) qApp.style().drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter) rect = option.rect val, ok = index.data(Qt.DisplayRole).toDouble() if ok: min, max = self.scale val = (val - min) / (max - min) painter.save() if option.state & QStyle.State_Selected: painter.setOpacity(0.75) painter.setBrush(self.brush) painter.drawRect(rect.adjusted(1, 1, - rect.width() * (1.0 - val) -2, -2)) painter.restore() class IndicatorItemDelegate(QStyledItemDelegate): IndicatorRole = OrangeUserRole.next() def __init__(self, parent, role=IndicatorRole, indicatorSize=2): QStyledItemDelegate.__init__(self, parent) self.role = role self.indicatorSize = indicatorSize def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) rect = option.rect indicator, valid = index.data(self.role).toString(), True indicator = False if indicator == "false" else indicator if valid and indicator: painter.save() painter.setRenderHints(QPainter.Antialiasing) painter.setBrush(QBrush(Qt.black)) 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)) painter.restore() class LinkStyledItemDelegate(QStyledItemDelegate): LinkRole = OrangeUserRole.next() def __init__(self, parent): QStyledItemDelegate.__init__(self, parent) self.mousePressState = QModelIndex(), QPoint() self.connect(parent, SIGNAL("entered(QModelIndex)"), self.onEntered) def sizeHint(self, option, index): size = QStyledItemDelegate.sizeHint(self, option, index) return QSize(size.width(), max(size.height(), 20)) def linkRect(self, option, index): style = self.parent().style() text = self.displayText(index.data(Qt.DisplayRole), QLocale.system()) self.initStyleOption(option, index) textRect = style.subElementRect(QStyle.SE_ItemViewItemText, option) if not textRect.isValid(): textRect = option.rect margin = style.pixelMetric(QStyle.PM_FocusFrameHMargin, option) + 1 textRect = textRect.adjusted(margin, 0, -margin, 0) font = index.data(Qt.FontRole) if font.isValid(): font = QFont(font) else: font = option.font metrics = QFontMetrics(font) elideText = metrics.elidedText(text, option.textElideMode, textRect.width()) try: str(elideText) ## on Windows with PyQt 4.4 sometimes this fails except Exception, ex: elideText = text return metrics.boundingRect(textRect, option.displayAlignment, elideText) def editorEvent(self, event, model, option, index): if event.type()==QEvent.MouseButtonPress and self.linkRect(option, index).contains(event.pos()): self.mousePressState = QPersistentModelIndex(index), QPoint(event.pos()) elif event.type()== QEvent.MouseButtonRelease: link = index.data(LinkRole) pressedIndex, pressPos = self.mousePressState if pressedIndex == index and (pressPos - event.pos()).manhattanLength() < 5 and link.isValid(): import webbrowser webbrowser.open(link.toString()) self.mousePressState = QModelIndex(), event.pos() elif event.type()==QEvent.MouseMove: link = index.data(LinkRole) if link.isValid() and self.linkRect(option, index).contains(event.pos()): self.parent().viewport().setCursor(Qt.PointingHandCursor) else: self.parent().viewport().setCursor(Qt.ArrowCursor) return QStyledItemDelegate.editorEvent(self, event, model, option, index) def onEntered(self, index): link = index.data(LinkRole) if not link.isValid(): self.parent().viewport().setCursor(Qt.ArrowCursor) def paint(self, painter, option, index): if index.data(LinkRole).isValid(): style = qApp.style() style.drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter) style.drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter) text = self.displayText(index.data(Qt.DisplayRole), QLocale.system()) textRect = style.subElementRect(QStyle.SE_ItemViewItemText, option) if not textRect.isValid(): textRect = option.rect margin = style.pixelMetric(QStyle.PM_FocusFrameHMargin, option) + 1 textRect = textRect.adjusted(margin, 0, -margin, 0) elideText = QFontMetrics(option.font).elidedText(text, option.textElideMode, textRect.width()) painter.save() font = index.data(Qt.FontRole) if font.isValid(): painter.setFont(QFont(font)) else: painter.setFont(option.font) painter.setPen(QPen(Qt.blue)) painter.drawText(textRect, option.displayAlignment, elideText) painter.restore() else: QStyledItemDelegate.paint(self, painter, option, index) LinkRole = LinkStyledItemDelegate.LinkRole def _toPyObject(variant): val = variant.toPyObject() if isinstance(val, type(NotImplemented)): # PyQt 4.4 converts python int, floats ... to C types and # cannot convert them back again and returns an exception instance. qtype = variant.type() if qtype == QVariant.Double: val, ok = variant.toDouble() elif qtype == QVariant.Int: val, ok = variant.toInt() elif qtype == QVariant.LongLong: val, ok = variant.toLongLong() elif qtype == QVariant.String: val = variant.toString() return val class ColoredBarItemDelegate(QStyledItemDelegate): """ Item delegate that can also draws a distribution bar """ def __init__(self, parent=None, decimals=3, color=Qt.red): QStyledItemDelegate.__init__(self, parent) self.decimals = decimals self.float_fmt = "%%.%if" % decimals self.color = QColor(color) def displayText(self, value, locale): obj = _toPyObject(value) if isinstance(obj, float): return self.float_fmt % obj elif isinstance(obj, basestring): return obj elif obj is None: return "NA" else: return obj.__str__() def sizeHint(self, option, index): font = self.get_font(option, index) metrics = QFontMetrics(font) height = metrics.lineSpacing() + 8 # 4 pixel margin width = metrics.width(self.displayText(index.data(Qt.DisplayRole), QLocale())) + 8 return QSize(width, height) def paint(self, painter, option, index): self.initStyleOption(option, index) text = self.displayText(index.data(Qt.DisplayRole), QLocale()) ratio, have_ratio = self.get_bar_ratio(option, index) rect = option.rect if have_ratio: # The text is raised 3 pixels above the bar. text_rect = rect.adjusted(4, 1, -4, -4) # TODO: Style dependent margins? else: text_rect = rect.adjusted(4, 4, -4, -4) painter.save() font = self.get_font(option, index) painter.setFont(font) qApp.style().drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter) qApp.style().drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter) # TODO: Check ForegroundRole. if option.state & QStyle.State_Selected: color = option.palette.highlightedText().color() else: color = option.palette.text().color() painter.setPen(QPen(color)) align = self.get_text_align(option, index) metrics = QFontMetrics(font) elide_text = metrics.elidedText(text, option.textElideMode, text_rect.width()) painter.drawText(text_rect, align, elide_text) painter.setRenderHint(QPainter.Antialiasing, True) if have_ratio: brush = self.get_bar_brush(option, index) painter.setBrush(brush) painter.setPen(QPen(brush, 1)) bar_rect = QRect(text_rect) bar_rect.setTop(bar_rect.bottom() - 1) bar_rect.setBottom(bar_rect.bottom() + 1) w = text_rect.width() bar_rect.setWidth(max(0, min(w * ratio, w))) painter.drawRoundedRect(bar_rect, 2, 2) painter.restore() def get_font(self, option, index): font = index.data(Qt.FontRole) if font.isValid(): font = font.toPyObject() else: font = option.font return font def get_text_align(self, option, index): align = index.data(Qt.TextAlignmentRole) if align.isValid(): align = align.toInt() else: align = Qt.AlignLeft | Qt.AlignVCenter return align def get_bar_ratio(self, option, index): bar_ratio = index.data(BarRatioRole) ratio, have_ratio = bar_ratio.toDouble() return ratio, have_ratio def get_bar_brush(self, option, index): bar_brush = index.data(BarBrushRole) if bar_brush.isValid(): bar_brush = bar_brush.toPyObject() if not isinstance(bar_brush, (QColor, QBrush)): bar_brush = None else: bar_brush = None if bar_brush is None: bar_brush = self.color return QBrush(bar_brush) ############################################################################## # progress bar management class ProgressBar: def __init__(self, widget, iterations): self.iter = iterations self.widget = widget self.count = 0 self.widget.progressBarInit() def advance(self, count=1): self.count += count self.widget.progressBarSet(int(self.count*100/self.iter)) def finish(self): self.widget.progressBarFinished() from Orange.utils import progress_bar_milestones as progressBarMilestones ############################################################################## def tabWidget(widget): w = QTabWidget(widget) if widget.layout() is not None: widget.layout().addWidget(w) return w def createTabPage(tabWidget, name, widgetToAdd = None, canScroll = False): if widgetToAdd == None: widgetToAdd = widgetBox(tabWidget, addToLayout = 0, margin = 4) if canScroll: scrollArea = QScrollArea() tabWidget.addTab(scrollArea, name) scrollArea.setWidget(widgetToAdd) scrollArea.setWidgetResizable(1) scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) else: tabWidget.addTab(widgetToAdd, name) return widgetToAdd def table(widget, rows = 0, columns = 0, selectionMode = -1, addToLayout = 1): w = QTableWidget(rows, columns, widget) if widget and addToLayout and widget.layout() is not None: widget.layout().addWidget(w) if selectionMode != -1: w.setSelectionMode(selectionMode) w.setHorizontalScrollMode(QTableWidget.ScrollPerPixel) w.horizontalHeader().setMovable(True) return w class VisibleHeaderSectionContextEventFilter(QObject): def __init__(self, parent, itemView=None): QObject.__init__(self, parent) self.itemView = itemView def eventFilter(self, view, event): if type(event) == QContextMenuEvent: model = view.model() headers = [(view.isSectionHidden(i), model.headerData(i, view.orientation(), Qt.DisplayRole)) for i in range(view.count())] menu = QMenu("Visible headers", view) for i, (checked, name) in enumerate(headers): action = QAction(name.toString(), menu) action.setCheckable(True) action.setChecked(not checked) menu.addAction(action) def toogleHidden(bool, section=i): view.setSectionHidden(section, not bool) if bool: if self.itemView: self.itemView.resizeColumnToContents(section) else: view.resizeSection(section, max(view.sectionSizeHint(section), 10)) self.connect(action, SIGNAL("toggled(bool)"), toogleHidden) # self.connect(action, SIGNAL("toggled(bool)"), lambda bool, section=i: view.setSectionHidden(section, not bool)) menu.exec_(event.globalPos()) return True return False def checkButtonOffsetHint(button, style=None): option = QStyleOptionButton() option.initFrom(button) if style is None: style = button.style() if isinstance(button, QCheckBox): pm_spacing = QStyle.PM_CheckBoxLabelSpacing pm_indicator_width = QStyle.PM_IndicatorWidth else: pm_spacing = QStyle.PM_RadioButtonLabelSpacing pm_indicator_width = QStyle.PM_ExclusiveIndicatorWidth space = style.pixelMetric(pm_spacing, option, button) width = style.pixelMetric(pm_indicator_width, option, button) style_correction = {"macintosh (aqua)": -2, "macintosh(aqua)": -2, "plastique": 1, "cde": 1, "motif": 1} #TODO: add other styles (Maybe load corrections from .cfg file?) return space + width + style_correction.get(str(qApp.style().objectName()).lower(), 0) def toolButtonSizeHint(button=None, style=None): if button is None and style is None: style = qApp.style() elif style is None: style = button.style() button_size = style.pixelMetric(QStyle.PM_SmallIconSize) + \ style.pixelMetric(QStyle.PM_ButtonMargin) return button_size class FloatSlider(QSlider): def __init__(self, orientation, min_value, max_value, step, parent=None): QSlider.__init__(self, orientation, parent) self.setScale(min_value, max_value, step) QObject.connect(self, SIGNAL("valueChanged(int)"), self.sendValue) def update(self): self.setSingleStep(1) if self.min_value != self.max_value: self.setEnabled(True) self.setMinimum(int(self.min_value/self.step)) self.setMaximum(int(self.max_value/self.step)) else: self.setEnabled(False) def sendValue(self, slider_value): value = min(max(slider_value * self.step, self.min_value), self.max_value) self.emit(SIGNAL("valueChangedFloat(double)"), value) def setValue(self, value): QSlider.setValue(self, int(value/self.step)) def setScale(self, minValue, maxValue, step=0): if minValue >= maxValue: ## It would be more logical to disable the slider in this case (self.setEnabled(False)) ## However, we do nothing to keep consistency with Qwt return if step <= 0 or step > (maxValue-minValue): if type(maxValue) == int and type(minValue) == int: step = 1 else: step = float(minValue-maxValue)/100.0 self.min_value = float(minValue) self.max_value = float(maxValue) self.step = step self.update() def setRange(self, minValue, maxValue, step=1.0): # For compatibility with qwtSlider self.setScale(minValue, maxValue, step)