/Orange/widgets/gui.py
Python | 1343 lines | 1313 code | 17 blank | 13 comment | 15 complexity | 98003d3fc03dc6ee09a3852768c9a0e5 MD5 | raw file
- import math
- import os
- import re
- import itertools
- from types import LambdaType
- import pkg_resources
- import numpy
- from PyQt4 import QtGui, QtCore, QtWebKit
- from PyQt4.QtCore import Qt, pyqtSignal as Signal
- from PyQt4.QtGui import QCursor, QApplication
- import Orange.data
- from Orange.widgets.utils import getdeepattr
- from Orange.data import \
- ContinuousVariable, StringVariable, TimeVariable, DiscreteVariable, Variable
- from Orange.widgets.utils import vartype
- from Orange.widgets.utils.constants import CONTROLLED_ATTRIBUTES, ATTRIBUTE_CONTROLLERS
- from Orange.util import namegen
- YesNo = NoYes = ("No", "Yes")
- _enter_icon = None
- __re_label = re.compile(r"(^|[^%])%\((?P<value>[a-zA-Z]\w*)\)")
- OrangeUserRole = itertools.count(Qt.UserRole)
- LAMBDA_NAME = namegen('_lambda_')
- def resource_filename(path):
- """
- Return a resource filename (package data) for path.
- """
- return pkg_resources.resource_filename(__name__, path)
- class TableWidget(QtGui.QTableWidget):
- """ An easy to use, row-oriented table widget """
- ROW_DATA_ROLE = QtCore.Qt.UserRole + 1
- ITEM_DATA_ROLE = ROW_DATA_ROLE + 1
- class TableWidgetNumericItem(QtGui.QTableWidgetItem):
- """TableWidgetItem that sorts numbers correctly!"""
- def __lt__(self, other):
- return (self.data(TableWidget.ITEM_DATA_ROLE) <
- other.data(TableWidget.ITEM_DATA_ROLE))
- def selectionChanged(self, selected:[QtGui.QItemSelectionRange], deselected:[QtGui.QItemSelectionRange]):
- """Override or monkey-patch this method to catch selection changes"""
- super().selectionChanged(selected, deselected)
- def __setattr__(self, attr, value):
- """
- The following selectionChanged magic ensures selectionChanged
- slot, when monkey-patched, always calls the super's selectionChanged
- first (--> avoids Qt quirks), and the user needs not care about that.
- """
- if attr == 'selectionChanged':
- func = value
- @QtCore.pyqtSlot(QtGui.QItemSelection, QtGui.QItemSelection)
- def _f(selected, deselected):
- super(self.__class__, self).selectionChanged(selected, deselected)
- func(selected, deselected)
- value = _f
- self.__dict__[attr] = value
- def _update_headers(func):
- """Decorator to update certain table features after method calls"""
- def _f(self, *args, **kwargs):
- func(self, *args, **kwargs)
- if self.col_labels is not None:
- self.setHorizontalHeaderLabels(self.col_labels)
- if self.row_labels is not None:
- self.setVerticalHeaderLabels(self.row_labels)
- if self.stretch_last_section:
- self.horizontalHeader().setStretchLastSection(True)
- return _f
- @_update_headers
- def __init__(self,
- parent=None,
- col_labels=None,
- row_labels=None,
- stretch_last_section=True,
- multi_selection=False,
- select_rows=False):
- """
- Parameters
- ----------
- parent: QObject
- Parent QObject. If parent has layout(), this widget is added to it.
- col_labels: list of str
- Labels or [] (sequential numbers) or None (no horizontal header)
- row_label: list_of_str
- Labels or [] (sequential numbers) or None (no vertical header)
- stretch_last_section: bool
- multi_selection: bool
- Single selection if False
- select_rows: bool
- If True, select whole rows instead of individual cells.
- """
- super().__init__(parent)
- self._column_filter = {}
- self.col_labels = col_labels
- self.row_labels = row_labels
- self.stretch_last_section = stretch_last_section
- try: parent.layout().addWidget(self)
- except (AttributeError, TypeError): pass
- if col_labels is None:
- self.horizontalHeader().setVisible(False)
- if row_labels is None:
- self.verticalHeader().setVisible(False)
- if multi_selection:
- self.setSelectionMode(self.MultiSelection)
- if select_rows:
- self.setSelectionBehavior(self.SelectRows)
- self.setHorizontalScrollMode(self.ScrollPerPixel)
- self.setVerticalScrollMode(self.ScrollPerPixel)
- self.setEditTriggers(self.NoEditTriggers)
- self.setAlternatingRowColors(True)
- self.setShowGrid(False)
- self.setSortingEnabled(True)
- @_update_headers
- def addRow(self, items:tuple, data=None):
- """
- Appends iterable of `items` as the next row, optionally setting row
- data to `data`. Each item of `items` can be a string or tuple
- (item_name, item_data) if individual, cell-data is required.
- """
- row_data = data
- row = self.rowCount()
- self.insertRow(row)
- col_count = max(len(items), self.columnCount())
- if col_count != self.columnCount():
- self.setColumnCount(col_count)
- for col, item_data in enumerate(items):
- if isinstance(item_data, str):
- name = item_data
- elif hasattr(item_data, '__iter__') and len(item_data) == 2:
- name, item_data = item_data
- elif isinstance(item_data, float):
- name = '{:.4f}'.format(item_data)
- else:
- name = str(item_data)
- if isinstance(item_data, (float, int, numpy.number)):
- item = self.TableWidgetNumericItem(name)
- else:
- item = QtGui.QTableWidgetItem(name)
- item.setData(self.ITEM_DATA_ROLE, item_data)
- if col in self._column_filter:
- item = self._column_filter[col](item) or item
- self.setItem(row, col, item)
- self.resizeColumnsToContents()
- self.resizeRowsToContents()
- if row_data is not None:
- self.setRowData(row, row_data)
- def rowData(self, row:int):
- return self.item(row, 0).data(self.ROW_DATA_ROLE)
- def setRowData(self, row:int, data):
- self.item(row, 0).setData(self.ROW_DATA_ROLE, data)
- def setColumnFilter(self, item_filter_func, columns:int or list):
- """
- Pass item(s) at column(s) through `item_filter_func` before
- insertion. Useful for setting specific columns to bold or similar.
- """
- try: iter(columns)
- except TypeError: columns = [columns]
- for i in columns:
- self._column_filter[i] = item_filter_func
- def clear(self):
- super().clear()
- self.setRowCount(0)
- self.setColumnCount(0)
- def selectFirstRow(self):
- if self.rowCount() > 0:
- self.selectRow(0)
- def selectRowsWhere(self, col, value, n_hits=-1,
- flags=QtCore.Qt.MatchExactly, _select=True):
- """
- Select (also return) at most `n_hits` rows where column `col`
- has value (``data()``) `value`.
- """
- model = self.model()
- matches = model.match(model.index(0, col),
- self.ITEM_DATA_ROLE,
- value,
- n_hits,
- flags)
- model = self.selectionModel()
- selection_flag = model.Select if _select else model.Deselect
- for index in matches:
- if _select ^ model.isSelected(index):
- model.select(index, selection_flag | model.Rows)
- return matches
- def deselectRowsWhere(self, col, value, n_hits=-1,
- flags=QtCore.Qt.MatchExactly):
- """
- Deselect (also return) at most `n_hits` rows where column `col`
- has value (``data()``) `value`.
- """
- return self.selectRowsWhere(col, value, n_hits, flags, False)
- class WebviewWidget(QtWebKit.QWebView):
- """WebKit window in a window"""
- def __init__(self, parent=None, bridge=None, html=None, debug=None):
- """
- Parameters
- ----------
- parent: QObject
- Parent QObject. If parent has layout(), this widget is added to it.
- bridge: QObject
- The "bridge" object exposed as ``window.pybridge`` in JavaScript.
- Any bridge methods desired to be accessible from JS need to be
- decorated ``@QtCore.pyqtSlot(<*args>, result=<type>)``.
- html: str
- HTML content to set in the webview.
- debug: bool
- If True, enable context menu and webkit inspector.
- """
- super().__init__(parent)
- self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
- QtGui.QSizePolicy.Expanding))
- self._bridge = bridge
- try: parent.layout().addWidget(self)
- except (AttributeError, TypeError): pass
- settings = self.settings()
- settings.setAttribute(settings.LocalContentCanAccessFileUrls, True)
- if debug is None:
- import logging
- debug = logging.getLogger().level <= logging.DEBUG
- if debug:
- settings.setAttribute(settings.DeveloperExtrasEnabled, True)
- else:
- self.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
- if html:
- self.setHtml(html)
- def setContent(self, data, mimetype, url=''):
- super().setContent(data, mimetype, QtCore.QUrl(url))
- if self._bridge:
- self.page().mainFrame().addToJavaScriptWindowObject('pybridge', self._bridge)
- def setHtml(self, html, url=''):
- self.setContent(html.encode('utf-8'), 'text/html', url)
- def sizeHint(self):
- return QtCore.QSize(600, 500)
- def evalJS(self, javascript):
- self.page().mainFrame().evaluateJavaScript(javascript)
- class ControlledAttributesDict(dict):
- def __init__(self, master):
- super().__init__()
- self.master = master
- def __setitem__(self, key, value):
- if key not in self:
- dict.__setitem__(self, key, [value])
- else:
- dict.__getitem__(self, key).append(value)
- set_controllers(self.master, key, self.master, "")
- callbacks = lambda obj: getattr(obj, CONTROLLED_ATTRIBUTES, {})
- subcontrollers = lambda obj: getattr(obj, ATTRIBUTE_CONTROLLERS, {})
- def notify_changed(obj, name, value):
- if name in callbacks(obj):
- for callback in callbacks(obj)[name]:
- callback(value)
- return
- for controller, prefix in list(subcontrollers(obj)):
- if getdeepattr(controller, prefix, None) != obj:
- del subcontrollers(obj)[(controller, prefix)]
- continue
- full_name = prefix + "." + name
- if full_name in callbacks(controller):
- for callback in callbacks(controller)[full_name]:
- callback(value)
- continue
- prefix = full_name + "."
- prefix_length = len(prefix)
- for controlled in callbacks(controller):
- if controlled[:prefix_length] == prefix:
- set_controllers(value, controlled[prefix_length:], controller, full_name)
- def set_controllers(obj, controlled_name, controller, prefix):
- while obj:
- if prefix:
- if hasattr(obj, ATTRIBUTE_CONTROLLERS):
- getattr(obj, ATTRIBUTE_CONTROLLERS)[(controller, prefix)] = True
- else:
- setattr(obj, ATTRIBUTE_CONTROLLERS, {(controller, prefix): True})
- parts = controlled_name.split(".", 1)
- if len(parts) < 2:
- break
- new_prefix, controlled_name = parts
- obj = getattr(obj, new_prefix, None)
- if prefix:
- prefix += '.'
- prefix += new_prefix
- class OWComponent:
- def __init__(self, widget):
- setattr(self, CONTROLLED_ATTRIBUTES, ControlledAttributesDict(self))
- if widget.settingsHandler:
- widget.settingsHandler.initialize(self)
- def __setattr__(self, key, value):
- super().__setattr__(key, value)
- notify_changed(self, key, value)
- def miscellanea(control, box, parent,
- addToLayout=True, stretch=0, sizePolicy=None, addSpace=False,
- disabled=False, tooltip=None):
- """
- Helper function that sets various properties of the widget using a common
- set of arguments.
- The function
- - sets the `control`'s attribute `box`, if `box` is given and `control.box`
- is not yet set,
- - attaches a tool tip to the `control` if specified,
- - disables the `control`, if `disabled` is set to `True`,
- - adds the `box` to the `parent`'s layout unless `addToLayout` is set to
- `False`; the stretch factor can be specified,
- - adds the control into the box's layout if the box is given (regardless
- of `addToLayout`!)
- - sets the size policy for the box or the control, if the policy is given,
- - adds space in the `parent`'s layout after the `box` if `addSpace` is set
- and `addToLayout` is not `False`.
- If `box` is the same as `parent` it is set to `None`; this is convenient
- because of the way complex controls are inserted.
- :param control: the control, e.g. a `QCheckBox`
- :type control: PyQt4.QtGui.QWidget
- :param box: the box into which the widget was inserted
- :type box: PyQt4.QtGui.QWidget or None
- :param parent: the parent into whose layout the box or the control will be
- inserted
- :type parent: PyQt4.QtGui.QWidget
- :param addSpace: the amount of space to add after the widget
- :type addSpace: bool or int
- :param disabled: If set to `True`, the widget is initially disabled
- :type disabled: bool
- :param addToLayout: If set to `False` the widget is not added to the layout
- :type addToLayout: bool
- :param stretch: the stretch factor for this widget, used when adding to
- the layout (default: 0)
- :type stretch: int
- :param tooltip: tooltip that is attached to the widget
- :type tooltip: str or None
- :param sizePolicy: the size policy for the box or the control
- :type sizePolicy: PyQt4.QtQui.QSizePolicy
- """
- if disabled:
- # if disabled==False, do nothing; it can be already disabled
- control.setDisabled(disabled)
- if tooltip is not None:
- control.setToolTip(tooltip)
- if box is parent:
- box = None
- elif box and box is not control and not hasattr(control, "box"):
- control.box = box
- if box and box.layout() is not None and \
- isinstance(control, QtGui.QWidget) and \
- box.layout().indexOf(control) == -1:
- box.layout().addWidget(control)
- if sizePolicy is not None:
- (box or control).setSizePolicy(sizePolicy)
- if addToLayout and parent and parent.layout() is not None:
- parent.layout().addWidget(box or control, stretch)
- _addSpace(parent, addSpace)
- def setLayout(widget, orientation):
- """
- Set the layout of the widget according to orientation. Argument
- `orientation` can be an instance of :obj:`~PyQt4.QtGui.QLayout`, in which
- case is it used as it is. If `orientation` is `'vertical'` or `True`,
- the layout is set to :obj:`~PyQt4.QtGui.QVBoxLayout`. If it is
- `'horizontal'` or `False`, it is set to :obj:`~PyQt4.QtGui.QVBoxLayout`.
- :param widget: the widget for which the layout is being set
- :type widget: PyQt4.QtGui.QWidget
- :param orientation: orientation for the layout
- :type orientation: str or bool or PyQt4.QtGui.QLayout
- """
- if isinstance(orientation, QtGui.QLayout):
- widget.setLayout(orientation)
- elif orientation == 'horizontal' or not orientation:
- widget.setLayout(QtGui.QHBoxLayout())
- else:
- widget.setLayout(QtGui.QVBoxLayout())
- def _enterButton(parent, control, placeholder=True):
- """
- Utility function that returns a button with a symbol for "Enter" and
- optionally a placeholder to show when the enter button is hidden. Both
- are inserted into the parent's layout, if it has one. If placeholder is
- constructed it is shown and the button is hidden.
- The height of the button is the same as the height of the widget passed
- as argument `control`.
- :param parent: parent widget into which the button is inserted
- :type parent: PyQt4.QtGui.QWidget
- :param control: a widget for determining the height of the button
- :type control: PyQt4.QtGui.QWidget
- :param placeholder: a flag telling whether to construct a placeholder
- (default: True)
- :type placeholder: bool
- :return: a tuple with a button and a place holder (or `None`)
- :rtype: PyQt4.QtGui.QToolButton or tuple
- """
- global _enter_icon
- if not _enter_icon:
- _enter_icon = QtGui.QIcon(
- os.path.dirname(__file__) + "/icons/Dlg_enter.png")
- button = QtGui.QPushButton(parent)
- button.setAutoDefault(True)
- button.setDefault(True)
- height = control.sizeHint().height()
- button.setFixedSize(height, height)
- button.setIcon(_enter_icon)
- if parent.layout() is not None:
- parent.layout().addWidget(button)
- if placeholder:
- button.hide()
- holder = QtGui.QWidget(parent)
- holder.setFixedSize(height, height)
- if parent.layout() is not None:
- parent.layout().addWidget(holder)
- else:
- holder = None
- return button, holder
- def _addSpace(widget, space):
- """
- A helper function that adds space into the widget, if requested.
- The function is called by functions that have the `addSpace` argument.
- :param widget: Widget into which to insert the space
- :type widget: PyQt4.QtGui.QWidget
- :param space: Amount of space to insert. If False, the function does
- nothing. If the argument is an `int`, the specified space is inserted.
- Otherwise, the default space is inserted by calling a :obj:`separator`.
- :type space: bool or int
- """
- if space:
- if type(space) == int: # distinguish between int and bool!
- separator(widget, space, space)
- else:
- separator(widget)
- def separator(widget, width=4, height=4):
- """
- Add a separator of the given size into the widget.
- :param widget: the widget into whose layout the separator is added
- :type widget: PyQt4.QtGui.QWidget
- :param width: width of the separator
- :type width: int
- :param height: height of the separator
- :type height: int
- :return: separator
- :rtype: PyQt4.QtGui.QWidget
- """
- sep = QtGui.QWidget(widget)
- if widget.layout() is not None:
- widget.layout().addWidget(sep)
- sep.setFixedSize(width, height)
- return sep
- def rubber(widget):
- """
- Insert a stretch 100 into the widget's layout
- """
- widget.layout().addStretch(100)
- def widgetBox(widget, box=None, orientation='vertical', margin=None, spacing=4,
- **misc):
- """
- Construct a box with vertical or horizontal layout, and optionally,
- a border with an optional label.
- If the widget has a frame, the space after the widget is added unless
- explicitly disabled.
- :param widget: the widget into which the box is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param box: tells whether the widget has a border, and its label
- :type box: int or str or None
- :param orientation: orientation for the layout. If the argument is an
- instance of :obj:`~PyQt4.QtGui.QLayout`, it is used as a layout. If
- "horizontal" or false-ish, the layout is horizontal
- (:obj:`~PyQt4.QtGui.QHBoxLayout`), otherwise vertical
- (:obj:`~PyQt4.QtGui.QHBoxLayout`).
- :type orientation: str, int or :obj:`PyQt4.QtGui.QLayout`
- :param sizePolicy: The size policy for the widget (default: None)
- :type sizePolicy: :obj:`~PyQt4.QtGui.QSizePolicy`
- :param margin: The margin for the layout. Default is 7 if the widget has
- a border, and 0 if not.
- :type margin: int
- :param spacing: Spacing within the layout (default: 4)
- :type spacing: int
- :return: Constructed box
- :rtype: PyQt4.QtGui.QGroupBox or PyQt4.QtGui.QWidget
- """
- if box:
- b = QtGui.QGroupBox(widget)
- if isinstance(box, str):
- b.setTitle(" " + box.strip() + " ")
- if margin is None:
- margin = 7
- else:
- b = QtGui.QWidget(widget)
- b.setContentsMargins(0, 0, 0, 0)
- if margin is None:
- margin = 0
- setLayout(b, orientation)
- b.layout().setSpacing(spacing)
- b.layout().setMargin(margin)
- misc.setdefault('addSpace', bool(box))
- miscellanea(b, None, widget, **misc)
- return b
- def hBox(*args, **kwargs):
- return widgetBox(orientation="horizontal", *args, **kwargs)
- def vBox(*args, **kwargs):
- return widgetBox(orientation="vertical", *args, **kwargs)
- def indentedBox(widget, sep=20, orientation="vertical", **misc):
- """
- Creates an indented box. The function can also be used "on the fly"::
- gui.checkBox(gui.indentedBox(box), self, "spam", "Enable spam")
- To align the control with a check box, use :obj:`checkButtonOffsetHint`::
- gui.hSlider(gui.indentedBox(self.interBox), self, "intervals")
- :param widget: the widget into which the box is inserted
- :type widget: PyQt4.QtGui.QWidget
- :param sep: Indent size (default: 20)
- :type sep: int
- :param orientation: layout of the inserted box; see :obj:`widgetBox` for
- details
- :type orientation: str, int or PyQt4.QtGui.QLayout
- :return: Constructed box
- :rtype: PyQt4.QtGui.QGroupBox or PyQt4.QtGui.QWidget
- """
- outer = widgetBox(widget, orientation=False, spacing=0)
- separator(outer, sep, 0)
- indented = widgetBox(outer, orientation=orientation)
- miscellanea(indented, outer, widget, **misc)
- return indented
- def widgetLabel(widget, label="", labelWidth=None, **misc):
- """
- Construct a simple, constant label.
- :param widget: the widget into which the box is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param label: The text of the label (default: None)
- :type label: str
- :param labelWidth: The width of the label (default: None)
- :type labelWidth: int
- :return: Constructed label
- :rtype: PyQt4.QtGui.QLabel
- """
- lbl = QtGui.QLabel(label, widget)
- if labelWidth:
- lbl.setFixedSize(labelWidth, lbl.sizeHint().height())
- miscellanea(lbl, None, widget, **misc)
- return lbl
- def label(widget, master, label, labelWidth=None, box=None,
- orientation="vertical", **misc):
- """
- Construct a label that contains references to the master widget's
- attributes; when their values change, the label is updated.
- Argument :obj:`label` is a format string following Python's syntax
- (see the corresponding Python documentation): the label's content is
- rendered as `label % master.__dict__`. For instance, if the
- :obj:`label` is given as "There are %(mm)i monkeys", the value of
- `master.mm` (which must be an integer) will be inserted in place of
- `%(mm)i`.
- :param widget: the widget into which the box is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param master: master widget
- :type master: OWWidget or OWComponent
- :param label: The text of the label, including attribute names
- :type label: str
- :param labelWidth: The width of the label (default: None)
- :type labelWidth: int
- :return: label
- :rtype: PyQt4.QtGui.QLabel
- """
- if box:
- b = widgetBox(widget, box, orientation=None, addToLayout=False)
- else:
- b = widget
- lbl = QtGui.QLabel("", b)
- reprint = CallFrontLabel(lbl, label, master)
- for mo in __re_label.finditer(label):
- getattr(master, CONTROLLED_ATTRIBUTES)[mo.group("value")] = reprint
- reprint()
- if labelWidth:
- lbl.setFixedSize(labelWidth, lbl.sizeHint().height())
- miscellanea(lbl, b, widget, **misc)
- return lbl
- class SpinBoxWFocusOut(QtGui.QSpinBox):
- """
- A class derived from QtGui.QSpinBox, which postpones the synchronization
- of the control's value with the master's attribute until the user presses
- Enter or clicks an icon that appears beside the spin box when the value
- is changed.
- The class overloads :obj:`onChange` event handler to show the commit button,
- and :obj:`onEnter` to commit the change when enter is pressed.
- .. attribute:: enterButton
- A widget (usually an icon) that is shown when the value is changed.
- .. attribute:: placeHolder
- A placeholder which is shown when the button is hidden
- .. attribute:: inSetValue
- A flag that is set when the value is being changed through
- :obj:`setValue` to prevent the programmatic changes from showing the
- commit button.
- """
- def __init__(self, minv, maxv, step, parent=None):
- """
- Construct the object and set the range (`minv`, `maxv`) and the step.
- :param minv: Minimal value
- :type minv: int
- :param maxv: Maximal value
- :type maxv: int
- :param step: Step
- :type step: int
- :param parent: Parent widget
- :type parent: PyQt4.QtGui.QWidget
- """
- super().__init__(parent)
- self.setRange(minv, maxv)
- self.setSingleStep(step)
- self.inSetValue = False
- self.enterButton = None
- self.placeHolder = None
- def onChange(self, _):
- """
- Hides the place holder and shows the commit button unless
- :obj:`inSetValue` is set.
- """
- if not self.inSetValue:
- self.placeHolder.hide()
- self.enterButton.show()
- def onEnter(self):
- """
- If the commit button is visible, the overload event handler commits
- the change by calling the appropriate callbacks. It also hides the
- commit button and shows the placeHolder.
- """
- 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
- # (but can't) catch
- def focusOutEvent(self, *e):
- """
- This handler was intended to catch the focus out event and reintepret
- it as if enter was pressed. It does not work, though.
- """
- super().focusOutEvent(*e)
- if self.enterButton and self.enterButton.isVisible():
- self.onEnter()
- def setValue(self, value):
- """
- Set the :obj:`inSetValue` flag and call the inherited method.
- """
- self.inSetValue = True
- super().setValue(value)
- self.inSetValue = False
- class DoubleSpinBoxWFocusOut(QtGui.QDoubleSpinBox):
- """
- Same as :obj:`SpinBoxWFocusOut`, except that it is derived from
- :obj:`~PyQt4.QtGui.QDoubleSpinBox`"""
- def __init__(self, minv, maxv, step, parent):
- super().__init__(parent)
- self.setDecimals(math.ceil(-math.log10(step)))
- self.setRange(minv, maxv)
- self.setSingleStep(step)
- self.inSetValue = False
- self.enterButton = None
- self.placeHolder = None
- def onChange(self, _):
- 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):
- super().focusOutEvent(*e)
- if self.enterButton and self.enterButton.isVisible():
- self.onEnter()
- def setValue(self, value):
- self.inSetValue = True
- super().setValue(value)
- self.inSetValue = False
- def spin(widget, master, value, minv, maxv, step=1, box=None, label=None,
- labelWidth=None, orientation=None, callback=None,
- controlWidth=None, callbackOnReturn=False, checked=None,
- checkCallback=None, posttext=None, disabled=False,
- alignment=Qt.AlignLeft, keyboardTracking=True,
- decimals=None, spinType=int, **misc):
- """
- A spinbox with lots of bells and whistles, such as a checkbox and various
- callbacks. It constructs a control of type :obj:`SpinBoxWFocusOut` or
- :obj:`DoubleSpinBoxWFocusOut`.
- :param widget: the widget into which the box is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param master: master widget
- :type master: OWWidget or OWComponent
- :param value: the master's attribute with which the value is synchronized
- :type value: str
- :param minv: minimal value
- :type minv: int
- :param maxv: maximal value
- :type maxv: int
- :param step: step (default: 1)
- :type step: int
- :param box: tells whether the widget has a border, and its label
- :type box: int or str or None
- :param label: label that is put in above or to the left of the spin box
- :type label: str
- :param labelWidth: optional label width (default: None)
- :type labelWidth: int
- :param orientation: tells whether to put the label above (`"vertical"` or
- `True`) or to the left (`"horizontal"` or `False`)
- :type orientation: int or bool or str
- :param callback: a function that is called when the value is entered; if
- :obj:`callbackOnReturn` is `True`, the function is called when the
- user commits the value by pressing Enter or clicking the icon
- :type callback: function
- :param controlWidth: the width of the spin box
- :type controlWidth: int
- :param callbackOnReturn: if `True`, the spin box has an associated icon
- that must be clicked to confirm the value (default: False)
- :type callbackOnReturn: bool
- :param checked: if not None, a check box is put in front of the spin box;
- when unchecked, the spin box is disabled. Argument `checked` gives the
- name of the master's attribute given whose value is synchronized with
- the check box's state (default: None).
- :type checked: str
- :param checkCallback: a callback function that is called when the check
- box's state is changed
- :type checkCallback: function
- :param posttext: a text that is put to the right of the spin box
- :type posttext: str
- :param alignment: alignment of the spin box (e.g. `QtCore.Qt.AlignLeft`)
- :type alignment: PyQt4.QtCore.Qt.Alignment
- :param keyboardTracking: If `True`, the valueChanged signal is emitted
- when the user is typing (default: True)
- :type keyboardTracking: bool
- :param spinType: determines whether to use QSpinBox (int) or
- QDoubleSpinBox (float)
- :type spinType: type
- :param decimals: number of decimals (if `spinType` is `float`)
- :type decimals: int
- :return: Tuple `(spin box, check box) if `checked` is `True`, otherwise
- the spin box
- :rtype: tuple or gui.SpinBoxWFocusOut
- """
- # b is the outermost box or the widget if there are no boxes;
- # b is the widget that is inserted into the layout
- # bi is the box that contains the control or the checkbox and the control;
- # bi can be the widget itself, if there are no boxes
- # cbox is the checkbox (or None)
- # sbox is the spinbox itself
- if box or label and not checked:
- b = widgetBox(widget, box, orientation, addToLayout=False)
- 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, orientation=0, addToLayout=False)
- else:
- bi = b
- cbox = None
- if checked is not None:
- cbox = checkBox(bi, master, checked, label, labelWidth=labelWidth,
- callback=checkCallback)
- elif label:
- b.label = widgetLabel(b, label, labelWidth)
- if posttext:
- widgetLabel(bi, posttext)
- isDouble = spinType == float
- sbox = bi.control = \
- (SpinBoxWFocusOut, DoubleSpinBoxWFocusOut)[isDouble](minv, maxv,
- step, bi)
- if bi is not widget:
- bi.setDisabled(disabled)
- else:
- sbox.setDisabled(disabled)
- if decimals is not None:
- sbox.setDecimals(decimals)
- sbox.setAlignment(alignment)
- sbox.setKeyboardTracking(keyboardTracking)
- if controlWidth:
- sbox.setFixedWidth(controlWidth)
- if value:
- sbox.setValue(getdeepattr(master, value))
- cfront, sbox.cback, sbox.cfunc = connectControl(
- master, value, callback,
- not (callback and callbackOnReturn) and
- sbox.valueChanged[(int, float)[isDouble]],
- (CallFrontSpin, CallFrontDoubleSpin)[isDouble](sbox))
- if checked:
- cbox.disables = [sbox]
- cbox.makeConsistent()
- if callback and callbackOnReturn:
- sbox.enterButton, sbox.placeHolder = _enterButton(bi, sbox)
- sbox.valueChanged[str].connect(sbox.onChange)
- sbox.editingFinished.connect(sbox.onEnter)
- sbox.enterButton.clicked.connect(sbox.onEnter)
- if hasattr(sbox, "upButton"):
- sbox.upButton().clicked.connect(
- lambda c=sbox.editor(): c.setFocus())
- sbox.downButton().clicked.connect(
- lambda c=sbox.editor(): c.setFocus())
- miscellanea(sbox, b if b is not widget else bi, widget, **misc)
- if checked:
- if isDouble and b == widget:
- # TODO Backward compatilibity; try to find and eliminate
- sbox.control = b.control
- return sbox
- return cbox, sbox
- else:
- return sbox
- # noinspection PyTypeChecker
- def doubleSpin(widget, master, value, minv, maxv, step=1, box=None, label=None,
- labelWidth=None, orientation=None, callback=None,
- controlWidth=None, callbackOnReturn=False, checked=None,
- checkCallback=None, posttext=None,
- alignment=Qt.AlignLeft, keyboardTracking=True,
- decimals=None, **misc):
- """
- Backward compatilibity function: calls :obj:`spin` with `spinType=float`.
- """
- return spin(widget, master, value, minv, maxv, step, box=box, label=label,
- labelWidth=labelWidth, orientation=orientation,
- callback=callback, controlWidth=controlWidth,
- callbackOnReturn=callbackOnReturn, checked=checked,
- checkCallback=checkCallback, posttext=posttext,
- alignment=alignment, keyboardTracking=keyboardTracking,
- decimals=decimals, spinType=float, **misc)
- def checkBox(widget, master, value, label, box=None,
- callback=None, getwidget=False, id_=None, labelWidth=None,
- disables=None, **misc):
- """
- A simple checkbox.
- :param widget: the widget into which the box is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param master: master widget
- :type master: OWWidget or OWComponent
- :param value: the master's attribute with which the value is synchronized
- :type value: str
- :param label: label
- :type label: str
- :param box: tells whether the widget has a border, and its label
- :type box: int or str or None
- :param callback: a function that is called when the check box state is
- changed
- :type callback: function
- :param getwidget: If set `True`, the callback function will get a keyword
- argument `widget` referencing the check box
- :type getwidget: bool
- :param id_: If present, the callback function will get a keyword argument
- `id` with this value
- :type id_: any
- :param labelWidth: the width of the label
- :type labelWidth: int
- :param disables: a list of widgets that are disabled if the check box is
- unchecked
- :type disables: list or PyQt4.QtGui.QWidget or None
- :return: constructed check box; if is is placed within a box, the box is
- return in the attribute `box`
- :rtype: PyQt4.QtGui.QCheckBox
- """
- if box:
- b = widgetBox(widget, box, orientation=None, addToLayout=False)
- else:
- b = widget
- cbox = QtGui.QCheckBox(label, b)
- if labelWidth:
- cbox.setFixedSize(labelWidth, cbox.sizeHint().height())
- cbox.setChecked(getdeepattr(master, value))
- connectControl(master, value, None, cbox.toggled[bool],
- CallFrontCheckBox(cbox),
- cfunc=callback and FunctionCallback(
- master, callback, widget=cbox, getwidget=getwidget,
- id=id_))
- if isinstance(disables, QtGui.QWidget):
- disables = [disables]
- cbox.disables = disables or []
- cbox.makeConsistent = Disabler(cbox, master, value)
- cbox.toggled[bool].connect(cbox.makeConsistent)
- cbox.makeConsistent(value)
- miscellanea(cbox, b, widget, **misc)
- return cbox
- class LineEditWFocusOut(QtGui.QLineEdit):
- """
- A class derived from QtGui.QLineEdit, which postpones the synchronization
- of the control's value with the master's attribute until the user leaves
- the line edit, presses Enter or clicks an icon that appears beside the
- line edit when the value is changed.
- The class also allows specifying a callback function for focus-in event.
- .. attribute:: enterButton
- A widget (usually an icon) that is shown when the value is changed.
- .. attribute:: placeHolder
- A placeholder which is shown when the button is hidden
- .. attribute:: inSetValue
- A flag that is set when the value is being changed through
- :obj:`setValue` to prevent the programmatic changes from showing the
- commit button.
- .. attribute:: callback
- Callback that is called when the change is confirmed
- .. attribute:: focusInCallback
- Callback that is called on the focus-in event
- """
- def __init__(self, parent, callback, focusInCallback=None,
- placeholder=False):
- super().__init__(parent)
- if parent.layout() is not None:
- parent.layout().addWidget(self)
- self.callback = callback
- self.focusInCallback = focusInCallback
- self.enterButton, self.placeHolder = \
- _enterButton(parent, self, placeholder)
- self.enterButton.clicked.connect(self.returnPressedHandler)
- self.textChanged[str].connect(self.markChanged)
- self.returnPressed.connect(self.returnPressedHandler)
- def markChanged(self, *_):
- if self.placeHolder:
- self.placeHolder.hide()
- self.enterButton.show()
- def markUnchanged(self, *_):
- self.enterButton.hide()
- if self.placeHolder:
- self.placeHolder.show()
- def returnPressedHandler(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):
- super().setText(t)
- if self.enterButton:
- self.markUnchanged()
- def focusOutEvent(self, *e):
- super().focusOutEvent(*e)
- self.returnPressedHandler()
- def focusInEvent(self, *e):
- if self.focusInCallback:
- self.focusInCallback()
- return super().focusInEvent(*e)
- def lineEdit(widget, master, value, label=None, labelWidth=None,
- orientation='vertical', box=None, callback=None,
- valueType=str, validator=None, controlWidth=None,
- callbackOnType=False, focusInCallback=None,
- enterPlaceholder=False, **misc):
- """
- Insert a line edit.
- :param widget: the widget into which the box is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param master: master widget
- :type master: OWWidget or OWComponent
- :param value: the master's attribute with which the value is synchronized
- :type value: str
- :param label: label
- :type label: str
- :param labelWidth: the width of the label
- :type labelWidth: int
- :param orientation: tells whether to put the label above (`"vertical"` or
- `True`) or to the left (`"horizontal"` or `False`)
- :type orientation: int or bool or str
- :param box: tells whether the widget has a border, and its label
- :type box: int or str or None
- :param callback: a function that is called when the check box state is
- changed
- :type callback: function
- :param valueType: the type into which the entered string is converted
- when synchronizing to `value`
- :type valueType: type
- :param validator: the validator for the input
- :type validator: PyQt4.QtGui.QValidator
- :param controlWidth: the width of the line edit
- :type controlWidth: int
- :param callbackOnType: if set to `True`, the callback is called at each
- key press (default: `False`)
- :type callbackOnType: bool
- :param focusInCallback: a function that is called when the line edit
- receives focus
- :type focusInCallback: function
- :param enterPlaceholder: if set to `True`, space of appropriate width is
- left empty to the right for the icon that shows that the value is
- changed but has not been committed yet
- :type enterPlaceholder: bool
- :rtype: PyQt4.QtGui.QLineEdit or a box
- """
- if box or label:
- b = widgetBox(widget, box, orientation, addToLayout=False)
- if label is not None:
- widgetLabel(b, label, labelWidth)
- hasHBox = orientation == 'horizontal' or not orientation
- else:
- b = widget
- hasHBox = False
- baseClass = misc.pop("baseClass", None)
- if baseClass:
- ledit = baseClass(b)
- ledit.enterButton = None
- if b is not widget:
- b.layout().addWidget(ledit)
- elif focusInCallback or callback and not callbackOnType:
- if not hasHBox:
- outer = widgetBox(b, "", 0, addToLayout=(b is not widget))
- else:
- outer = b
- ledit = LineEditWFocusOut(outer, callback, focusInCallback,
- enterPlaceholder)
- else:
- ledit = QtGui.QLineEdit(b)
- ledit.enterButton = None
- if b is not widget:
- b.layout().addWidget(ledit)
- if value:
- ledit.setText(str(getdeepattr(master, value)))
- if controlWidth:
- ledit.setFixedWidth(controlWidth)
- if validator:
- ledit.setValidator(validator)
- if value:
- ledit.cback = connectControl(
- master, value,
- callbackOnType and callback, ledit.textChanged[str],
- CallFrontLineEdit(ledit), fvcb=value and valueType)[1]
- miscellanea(ledit, b, widget, **misc)
- return ledit
- def button(widget, master, label, callback=None, width=None, height=None,
- toggleButton=False, value="", default=False, autoDefault=True,
- buttonType=QtGui.QPushButton, **misc):
- """
- Insert a button (QPushButton, by default)
- :param widget: the widget into which the button is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param master: master widget
- :type master: OWWidget or OWComponent
- :param label: label
- :type label: str
- :param callback: a function that is called when the button is pressed
- :type callback: function
- :param width: the width of the button
- :type width: int
- :param height: the height of the button
- :type height: int
- :param toggleButton: if set to `True`, the button is checkable, but it is
- not synchronized with any attribute unless the `value` is given
- :type toggleButton: bool
- :param value: the master's attribute with which the value is synchronized
- (the argument is optional; if present, it makes the button "checkable",
- even if `toggleButton` is not set)
- :type value: str
- :param default: if `True` it makes the button the default button; this is
- the button that is activated when the user presses Enter unless some
- auto default button has current focus
- :type default: bool
- :param autoDefault: all buttons are auto default: they are activated if
- they have focus (or are the next in the focus chain) when the user
- presses enter. By setting `autoDefault` to `False`, the button is not
- activated on pressing Return.
- :type autoDefault: bool
- :param buttonType: the button type (default: `QPushButton`)
- :type buttonType: PyQt4.QtGui.QAbstractButton
- :rtype: PyQt4.QtGui.QAbstractButton
- """
- button = buttonType(widget)
- if label:
- button.setText(label)
- if width:
- button.setFixedWidth(width)
- if height:
- button.setFixedHeight(height)
- if toggleButton or value:
- button.setCheckable(True)
- if buttonType == QtGui.QPushButton:
- button.setDefault(default)
- button.setAutoDefault(autoDefault)
- if value:
- button.setChecked(getdeepattr(master, value))
- connectControl(
- master, value, None, button.toggled[bool],
- CallFrontButton(button),
- cfunc=callback and FunctionCallback(master, callback,
- widget=button))
- elif callback:
- button.clicked.connect(callback)
- miscellanea(button, None, widget, **misc)
- return button
- def toolButton(widget, master, label="", callback=None,
- width=None, height=None, tooltip=None):
- """
- Insert a tool button. Calls :obj:`button`
- :param widget: the widget into which the button is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param master: master widget
- :type master: OWWidget or OWComponent
- :param label: label
- :type label: str
- :param callback: a function that is called when the button is pressed
- :type callback: function
- :param width: the width of the button
- :type width: int
- :param height: the height of the button
- :type height: int
- :rtype: PyQt4.QtGui.QToolButton
- """
- return button(widget, master, label, callback, width, height,
- buttonType=QtGui.QToolButton, tooltip=tooltip)
- def createAttributePixmap(char, background=Qt.black, color=Qt.white):
- """
- Create a QIcon with a given character. The icon is 13 pixels high and wide.
- :param char: The character that is printed in the icon
- :type char: str
- :param background: the background color (default: black)
- :type background: PyQt4.QtGui.QColor
- :param color: the character color (default: white)
- :type color: PyQt4.QtGui.QColor
- :rtype: PyQt4.QtGui.QIcon
- """
- pixmap = QtGui.QPixmap(13, 13)
- pixmap.fill(QtGui.QColor(0, 0, 0, 0))
- painter = QtGui.QPainter()
- painter.begin(pixmap)
- painter.setRenderHints(painter.Antialiasing | painter.TextAntialiasing |
- painter.SmoothPixmapTransform)
- painter.setPen(background)
- painter.setBrush(background)
- rect = QtCore.QRectF(0, 0, 13, 13)
- painter.drawRoundedRect(rect, 4, 4)
- painter.setPen(color)
- painter.drawText(2, 11, char)
- painter.end()
- return QtGui.QIcon(pixmap)
- class __AttributeIconDict(dict):
- def __getitem__(self, key):
- if not self:
- for tpe, char, col in ((vartype(ContinuousVariable()),
- "C", (202, 0, 32)),
- (vartype(DiscreteVariable()),
- "D", (26, 150, 65)),
- (vartype(StringVariable()),
- "S", (0, 0, 0)),
- (vartype(TimeVariable()),
- "T", (68, 170, 255)),
- (-1, "?", (128, 128, 128))):
- self[tpe] = createAttributePixmap(char, QtGui.QColor(*col))
- if key not in self:
- key = vartype(key) if isinstance(key, Variable) else -1
- return super().__getitem__(key)
- #: A dict that returns icons for different attribute types. The dict is
- #: constructed on first use since icons cannot be created before initializing
- #: the application.
- #:
- #: Accepted keys are variable type codes and instances
- #: of :obj:`Orange.data.variable`: `attributeIconDict[var]` will give the
- #: appropriate icon for variable `var` or a question mark if the type is not
- #: recognized
- attributeIconDict = __AttributeIconDict()
- def attributeItem(var):
- """
- Construct a pair (icon, name) for inserting a variable into a combo or
- list box
- :param var: variable
- :type var: Orange.data.Variable
- :rtype: tuple with PyQt4.QtGui.QIcon and str
- """
- return attributeIconDict[var], var.name
- def listBox(widget, master, value=None, labels=None, box=None, callback=None,
- selectionMode=QtGui.QListWidget.SingleSelection,
- enableDragDrop=False, dragDropCallback=None,
- dataValidityCallback=None, sizeHint=None, **misc):
- """
- Insert a list box.
- The value with which the box's value synchronizes (`master.<value>`)
- is a list of indices of selected items.
- :param widget: the widget into which the box is inserted
- :type widget: PyQt4.QtGui.QWidget or None
- :param master: master widget
- :type master: OWWidget or OWComponent
- :param value: the name of the master's attribute with which the value is
- synchronized (list of ints - indices of selected items)
- :type value: str
- :param labels: the name of the master's attribute with the list of items
- (as strings or tuples with icon and string)
- :type labels: str
- :param box: tells whether the widget has a border, and its label
- :type box: int or str or None
- :param callback: a f