1""" 2Wrappers for controls used in widgets 3""" 4import math 5 6import logging 7import sys 8import warnings 9import weakref 10from collections.abc import Sequence 11 12from AnyQt import QtWidgets, QtCore, QtGui 13from AnyQt.QtCore import Qt, QSize, QItemSelection 14from AnyQt.QtGui import QColor, QWheelEvent 15from AnyQt.QtWidgets import QWidget, QListView, QComboBox 16 17from orangewidget.utils.itemdelegates import ( 18 BarItemDataDelegate as _BarItemDataDelegate 19) 20# re-export relevant objects 21from orangewidget.gui import ( 22 OWComponent, OrangeUserRole, TableView, resource_filename, 23 miscellanea, setLayout, separator, rubber, widgetBox, hBox, vBox, 24 comboBox as gui_comboBox, 25 indentedBox, widgetLabel, label, spin, doubleSpin, checkBox, lineEdit, 26 button, toolButton, radioButtons, radioButtonsInBox, appendRadioButton, 27 hSlider, labeledSlider, valueSlider, auto_commit, auto_send, auto_apply, 28 29 # ItemDataRole's 30 BarRatioRole, BarBrushRole, SortOrderRole, LinkRole, 31 32 IndicatorItemDelegate, BarItemDelegate, LinkStyledItemDelegate, 33 ColoredBarItemDelegate, HorizontalGridDelegate, VerticalItemDelegate, 34 VerticalLabel, tabWidget, createTabPage, table, tableItem, 35 VisibleHeaderSectionContextEventFilter, 36 checkButtonOffsetHint, toolButtonSizeHint, FloatSlider, 37 CalendarWidgetWithTime, DateTimeEditWCalendarTime, 38 ControlGetter, VerticalScrollArea, ProgressBar, 39 ControlledCallback, ControlledCallFront, ValueCallback, connectControl, 40 is_macstyle 41) 42 43 44try: 45 # Some Orange widgets might expect this here 46 # pylint: disable=unused-import 47 from Orange.widgets.utils.webview import WebviewWidget 48except ImportError: 49 pass # Neither WebKit nor WebEngine are available 50 51import Orange.data 52from Orange.widgets.utils import getdeepattr 53from Orange.data import \ 54 ContinuousVariable, StringVariable, TimeVariable, DiscreteVariable, \ 55 Variable, Value 56from Orange.widgets.utils import vartype 57 58__all__ = [ 59 # Re-exported 60 "OWComponent", "OrangeUserRole", "TableView", "resource_filename", 61 "miscellanea", "setLayout", "separator", "rubber", 62 "widgetBox", "hBox", "vBox", "indentedBox", 63 "widgetLabel", "label", "spin", "doubleSpin", 64 "checkBox", "lineEdit", "button", "toolButton", "comboBox", 65 "radioButtons", "radioButtonsInBox", "appendRadioButton", 66 "hSlider", "labeledSlider", "valueSlider", 67 "auto_commit", "auto_send", "auto_apply", "ProgressBar", 68 "VerticalLabel", "tabWidget", "createTabPage", "table", "tableItem", 69 "VisibleHeaderSectionContextEventFilter", "checkButtonOffsetHint", 70 "toolButtonSizeHint", "FloatSlider", "ControlGetter", "VerticalScrollArea", 71 "CalendarWidgetWithTime", "DateTimeEditWCalendarTime", 72 "BarRatioRole", "BarBrushRole", "SortOrderRole", "LinkRole", 73 "BarItemDelegate", "IndicatorItemDelegate", "LinkStyledItemDelegate", 74 "ColoredBarItemDelegate", "HorizontalGridDelegate", "VerticalItemDelegate", 75 "ValueCallback", 'is_macstyle', 76 # Defined here 77 "createAttributePixmap", "attributeIconDict", "attributeItem", 78 "listView", "ListViewWithSizeHint", "listBox", "OrangeListBox", 79 "TableValueRole", "TableClassValueRole", "TableDistribution", 80 "TableVariable", "TableBarItem", "palette_combo_box" 81] 82 83 84log = logging.getLogger(__name__) 85 86 87def palette_combo_box(initial_palette): 88 from Orange.widgets.utils import itemmodels 89 cb = QComboBox() 90 model = itemmodels.ContinuousPalettesModel() 91 cb.setModel(model) 92 cb.setCurrentIndex(model.indexOf(initial_palette)) 93 cb.setIconSize(QSize(64, 16)) 94 return cb 95 96 97def createAttributePixmap(char, background=Qt.black, color=Qt.white): 98 """ 99 Create a QIcon with a given character. The icon is 13 pixels high and wide. 100 101 :param char: The character that is printed in the icon 102 :type char: str 103 :param background: the background color (default: black) 104 :type background: QColor 105 :param color: the character color (default: white) 106 :type color: QColor 107 :rtype: QIcon 108 """ 109 icon = QtGui.QIcon() 110 for size in (13, 16, 18, 20, 22, 24, 28, 32, 64): 111 pixmap = QtGui.QPixmap(size, size) 112 pixmap.fill(Qt.transparent) 113 painter = QtGui.QPainter() 114 painter.begin(pixmap) 115 painter.setRenderHints(painter.Antialiasing | painter.TextAntialiasing | 116 painter.SmoothPixmapTransform) 117 painter.setPen(background) 118 painter.setBrush(background) 119 margin = 1 + size // 16 120 text_margin = size // 20 121 rect = QtCore.QRectF(margin, margin, 122 size - 2 * margin, size - 2 * margin) 123 painter.drawRoundedRect(rect, 30.0, 30.0, Qt.RelativeSize) 124 painter.setPen(color) 125 font = painter.font() # type: QtGui.QFont 126 font.setPixelSize(size - 2 * margin - 2 * text_margin) 127 painter.setFont(font) 128 painter.drawText(rect, Qt.AlignCenter, char) 129 painter.end() 130 icon.addPixmap(pixmap) 131 return icon 132 133 134class __AttributeIconDict(dict): 135 def __getitem__(self, key): 136 if not self: 137 for tpe, char, col in ((vartype(ContinuousVariable("c")), 138 "N", (202, 0, 32)), 139 (vartype(DiscreteVariable("d")), 140 "C", (26, 150, 65)), 141 (vartype(StringVariable("s")), 142 "S", (0, 0, 0)), 143 (vartype(TimeVariable("t")), 144 "T", (68, 170, 255)), 145 (-1, "?", (128, 128, 128))): 146 self[tpe] = createAttributePixmap(char, QtGui.QColor(*col)) 147 if key not in self: 148 key = vartype(key) if isinstance(key, Variable) else -1 149 return super().__getitem__(key) 150 151 152#: A dict that returns icons for different attribute types. The dict is 153#: constructed on first use since icons cannot be created before initializing 154#: the application. 155#: 156#: Accepted keys are variable type codes and instances 157#: of :obj:`Orange.data.Variable`: `attributeIconDict[var]` will give the 158#: appropriate icon for variable `var` or a question mark if the type is not 159#: recognized 160attributeIconDict = __AttributeIconDict() 161 162 163def attributeItem(var): 164 """ 165 Construct a pair (icon, name) for inserting a variable into a combo or 166 list box 167 168 :param var: variable 169 :type var: Orange.data.Variable 170 :rtype: tuple with QIcon and str 171 """ 172 return attributeIconDict[var], var.name 173 174 175class ListViewWithSizeHint(QListView): 176 def __init__(self, *args, preferred_size=None, **kwargs): 177 super().__init__(*args, **kwargs) 178 if isinstance(preferred_size, tuple): 179 preferred_size = QSize(*preferred_size) 180 self.preferred_size = preferred_size 181 182 def sizeHint(self): 183 return self.preferred_size if self.preferred_size is not None \ 184 else super().sizeHint() 185 186 187def listView(widget, master, value=None, model=None, box=None, callback=None, 188 sizeHint=None, *, viewType=ListViewWithSizeHint, **misc): 189 if box: 190 bg = vBox(widget, box, addToLayout=False) 191 else: 192 bg = widget 193 view = viewType(preferred_size=sizeHint) 194 view.setModel(model) 195 if value is not None: 196 connectControl(master, value, callback, 197 view.selectionModel().selectionChanged, 198 CallFrontListView(view), 199 CallBackListView(model, view, master, value)) 200 misc.setdefault('uniformItemSizes', True) 201 miscellanea(view, bg, widget, **misc) 202 return view 203 204 205def listBox(widget, master, value=None, labels=None, box=None, callback=None, 206 selectionMode=QtWidgets.QListWidget.SingleSelection, 207 enableDragDrop=False, dragDropCallback=None, 208 dataValidityCallback=None, sizeHint=None, **misc): 209 """ 210 Insert a list box. 211 212 The value with which the box's value synchronizes (`master.<value>`) 213 is a list of indices of selected items. 214 215 :param widget: the widget into which the box is inserted 216 :type widget: QWidget or None 217 :param master: master widget 218 :type master: OWWidget or OWComponent 219 :param value: the name of the master's attribute with which the value is 220 synchronized (list of ints - indices of selected items) 221 :type value: str 222 :param labels: the name of the master's attribute with the list of items 223 (as strings or tuples with icon and string) 224 :type labels: str 225 :param box: tells whether the widget has a border, and its label 226 :type box: int or str or None 227 :param callback: a function that is called when the selection state is 228 changed 229 :type callback: function 230 :param selectionMode: selection mode - single, multiple etc 231 :type selectionMode: QAbstractItemView.SelectionMode 232 :param enableDragDrop: flag telling whether drag and drop is available 233 :type enableDragDrop: bool 234 :param dragDropCallback: callback function on drop event 235 :type dragDropCallback: function 236 :param dataValidityCallback: function that check the validity on enter 237 and move event; it should return either `ev.accept()` or `ev.ignore()`. 238 :type dataValidityCallback: function 239 :param sizeHint: size hint 240 :type sizeHint: QSize 241 :rtype: OrangeListBox 242 """ 243 if box: 244 bg = hBox(widget, box, addToLayout=False) 245 else: 246 bg = widget 247 lb = OrangeListBox(master, enableDragDrop, dragDropCallback, 248 dataValidityCallback, sizeHint, bg) 249 lb.setSelectionMode(selectionMode) 250 lb.ogValue = value 251 lb.ogLabels = labels 252 lb.ogMaster = master 253 254 if labels is not None: 255 setattr(master, labels, getdeepattr(master, labels)) 256 master.connect_control(labels, CallFrontListBoxLabels(lb)) 257 if value is not None: 258 clist = getdeepattr(master, value) 259 if not isinstance(clist, (int, ControlledList)): 260 clist = ControlledList(clist, lb) 261 master.__setattr__(value, clist) 262 setattr(master, value, clist) 263 connectControl(master, value, callback, lb.itemSelectionChanged, 264 CallFrontListBox(lb), CallBackListBox(lb, master)) 265 266 miscellanea(lb, bg, widget, **misc) 267 return lb 268 269 270class OrangeListBox(QtWidgets.QListWidget): 271 """ 272 List box with drag and drop functionality. Function :obj:`listBox` 273 constructs instances of this class; do not use the class directly. 274 275 .. attribute:: master 276 277 The widget into which the listbox is inserted. 278 279 .. attribute:: ogLabels 280 281 The name of the master's attribute that holds the strings with items 282 in the list box. 283 284 .. attribute:: ogValue 285 286 The name of the master's attribute that holds the indices of selected 287 items. 288 289 .. attribute:: enableDragDrop 290 291 A flag telling whether drag-and-drop is enabled. 292 293 .. attribute:: dragDropCallback 294 295 A callback that is called at the end of drop event. 296 297 .. attribute:: dataValidityCallback 298 299 A callback that is called on dragEnter and dragMove events and returns 300 either `ev.accept()` or `ev.ignore()`. 301 302 .. attribute:: defaultSizeHint 303 304 The size returned by the `sizeHint` method. 305 """ 306 def __init__(self, master, enableDragDrop=False, dragDropCallback=None, 307 dataValidityCallback=None, sizeHint=None, *args): 308 """ 309 :param master: the master widget 310 :type master: OWWidget or OWComponent 311 :param enableDragDrop: flag telling whether drag and drop is enabled 312 :type enableDragDrop: bool 313 :param dragDropCallback: callback for the end of drop event 314 :type dragDropCallback: function 315 :param dataValidityCallback: callback that accepts or ignores dragEnter 316 and dragMove events 317 :type dataValidityCallback: function with one argument (event) 318 :param sizeHint: size hint 319 :type sizeHint: QSize 320 :param args: optional arguments for the inherited constructor 321 """ 322 self.master = master 323 super().__init__(*args) 324 self.drop_callback = dragDropCallback 325 self.valid_data_callback = dataValidityCallback 326 if not sizeHint: 327 self.size_hint = QtCore.QSize(150, 100) 328 else: 329 self.size_hint = sizeHint 330 if enableDragDrop: 331 self.setDragEnabled(True) 332 self.setAcceptDrops(True) 333 self.setDropIndicatorShown(True) 334 335 def sizeHint(self): 336 return self.size_hint 337 338 def minimumSizeHint(self): 339 return self.size_hint 340 341 def dragEnterEvent(self, event): 342 super().dragEnterEvent(event) 343 if self.valid_data_callback: 344 self.valid_data_callback(event) 345 elif isinstance(event.source(), OrangeListBox): 346 event.setDropAction(Qt.MoveAction) 347 event.accept() 348 else: 349 event.ignore() 350 351 def dropEvent(self, event): 352 event.setDropAction(Qt.MoveAction) 353 super().dropEvent(event) 354 355 items = self.update_master() 356 if event.source() is not self: 357 event.source().update_master(exclude=items) 358 359 if self.drop_callback: 360 self.drop_callback() 361 362 def update_master(self, exclude=()): 363 control_list = [self.item(i).data(Qt.UserRole) 364 for i in range(self.count()) 365 if self.item(i).data(Qt.UserRole) not in exclude] 366 if self.ogLabels: 367 master_list = getattr(self.master, self.ogLabels) 368 369 if master_list != control_list: 370 setattr(self.master, self.ogLabels, control_list) 371 return control_list 372 373 def updateGeometries(self): 374 # A workaround for a bug in Qt 375 # (see: http://bugreports.qt.nokia.com/browse/QTBUG-14412) 376 if getattr(self, "_updatingGeometriesNow", False): 377 return 378 self._updatingGeometriesNow = True 379 try: 380 return super().updateGeometries() 381 finally: 382 self._updatingGeometriesNow = False 383 384 385class ControlledList(list): 386 """ 387 A class derived from a list that is connected to a 388 :obj:`QListBox`: the list contains indices of items that are 389 selected in the list box. Changing the list content changes the 390 selection in the list box. 391 """ 392 def __init__(self, content, listBox=None): 393 super().__init__(content if content is not None else []) 394 # Controlled list is created behind the back by gui.listBox and 395 # commonly used as a setting which gets synced into a GLOBAL 396 # SettingsHandler and which keeps the OWWidget instance alive via a 397 # reference in listBox (see gui.listBox) 398 if listBox is not None: 399 self.listBox = weakref.ref(listBox) 400 else: 401 self.listBox = lambda: None 402 403 def __reduce__(self): 404 # cannot pickle self.listBox, but can't discard it 405 # (ControlledList may live on) 406 import copyreg 407 return copyreg._reconstructor, (list, list, ()), None, self.__iter__() 408 409 # TODO ControllgedList.item2name is probably never used 410 def item2name(self, item): 411 item = self.listBox().labels[item] 412 if isinstance(item, tuple): 413 return item[1] 414 else: 415 return item 416 417 def __setitem__(self, index, item): 418 def unselect(i): 419 try: 420 item = self.listBox().item(i) 421 except RuntimeError: # Underlying C/C++ object has been deleted 422 item = None 423 if item is None: 424 # Labels changed before clearing the selection: clear everything 425 self.listBox().selectionModel().clear() 426 else: 427 item.setSelected(0) 428 429 if isinstance(index, int): 430 unselect(self[index]) 431 item.setSelected(1) 432 else: 433 for i in self[index]: 434 unselect(i) 435 for i in item: 436 self.listBox().item(i).setSelected(1) 437 super().__setitem__(index, item) 438 439 def __delitem__(self, index): 440 if isinstance(index, int): 441 self.listBox().item(self[index]).setSelected(0) 442 else: 443 for i in self[index]: 444 self.listBox().item(i).setSelected(0) 445 super().__delitem__(index) 446 447 def append(self, item): 448 super().append(item) 449 item.setSelected(1) 450 451 def extend(self, items): 452 super().extend(items) 453 for i in items: 454 self.listBox().item(i).setSelected(1) 455 456 def insert(self, index, item): 457 item.setSelected(1) 458 super().insert(index, item) 459 460 def pop(self, index=-1): 461 i = super().pop(index) 462 self.listBox().item(i).setSelected(0) 463 464 def remove(self, item): 465 item.setSelected(0) 466 super().remove(item) 467 468 469def comboBox(*args, **kwargs): 470 if "valueType" in kwargs: 471 del kwargs["valueType"] 472 warnings.warn("Argument 'valueType' is deprecated and ignored", 473 DeprecationWarning) 474 return gui_comboBox(*args, **kwargs) 475 476 477class CallBackListView(ControlledCallback): 478 def __init__(self, model, view, widget, attribute): 479 super().__init__(widget, attribute) 480 self.model = model 481 self.view = view 482 483 # triggered by selectionModel().selectionChanged() 484 def __call__(self, *_): 485 # This must be imported locally to avoid circular imports 486 from Orange.widgets.utils.itemmodels import PyListModel 487 values = [i.row() 488 for i in self.view.selectionModel().selection().indexes()] 489 if values: 490 # FIXME: irrespective of PyListModel check, this might/should always 491 # callback with values! 492 if isinstance(self.model, PyListModel): 493 values = [self.model[i] for i in values] 494 if self.view.selectionMode() == self.view.SingleSelection: 495 values = values[0] 496 self.acyclic_setattr(values) 497 498 499class CallBackListBox: 500 def __init__(self, control, widget): 501 self.control = control 502 self.widget = widget 503 self.disabled = 0 504 505 def __call__(self, *_): # triggered by selectionChange() 506 if not self.disabled and self.control.ogValue is not None: 507 clist = getdeepattr(self.widget, self.control.ogValue) 508 control = self.control 509 selection = [i for i in range(control.count()) 510 if control.item(i).isSelected()] 511 if isinstance(clist, int): 512 self.widget.__setattr__( 513 self.control.ogValue, selection[0] if selection else None) 514 else: 515 list.__setitem__(clist, slice(0, len(clist)), selection) 516 self.widget.__setattr__(self.control.ogValue, clist) 517 518 519############################################################################## 520# call fronts (change of the attribute value changes the related control) 521 522 523class CallFrontListView(ControlledCallFront): 524 def action(self, values): 525 view = self.control 526 model = view.model() 527 sel_model = view.selectionModel() 528 529 if not isinstance(values, Sequence): 530 values = [values] 531 532 selection = QItemSelection() 533 for value in values: 534 index = None 535 if not isinstance(value, int): 536 if isinstance(value, Variable): 537 search_role = TableVariable 538 else: 539 search_role = Qt.DisplayRole 540 value = str(value) 541 for i in range(model.rowCount()): 542 if model.data(model.index(i), search_role) == value: 543 index = i 544 break 545 else: 546 index = value 547 if index is not None: 548 selection.select(model.index(index), model.index(index)) 549 sel_model.select(selection, sel_model.ClearAndSelect) 550 551 552class CallFrontListBox(ControlledCallFront): 553 def action(self, value): 554 if value is not None: 555 if isinstance(value, int): 556 for i in range(self.control.count()): 557 self.control.item(i).setSelected(i == value) 558 else: 559 if not isinstance(value, ControlledList): 560 setattr(self.control.ogMaster, self.control.ogValue, 561 ControlledList(value, self.control)) 562 for i in range(self.control.count()): 563 shouldBe = i in value 564 if shouldBe != self.control.item(i).isSelected(): 565 self.control.item(i).setSelected(shouldBe) 566 567 568class CallFrontListBoxLabels(ControlledCallFront): 569 unknownType = None 570 571 def action(self, values): 572 self.control.clear() 573 if values: 574 for value in values: 575 if isinstance(value, tuple): 576 text, icon = value 577 if isinstance(icon, int): 578 item = QtWidgets.QListWidgetItem(attributeIconDict[icon], text) 579 else: 580 item = QtWidgets.QListWidgetItem(icon, text) 581 elif isinstance(value, Variable): 582 item = QtWidgets.QListWidgetItem(*attributeItem(value)) 583 else: 584 item = QtWidgets.QListWidgetItem(value) 585 586 item.setData(Qt.UserRole, value) 587 self.control.addItem(item) 588 589 590#: Role to retrieve Orange.data.Value 591TableValueRole = next(OrangeUserRole) 592#: Role to retrieve class value for a row 593TableClassValueRole = next(OrangeUserRole) 594# Role to retrieve distribution of a column 595TableDistribution = next(OrangeUserRole) 596#: Role to retrieve the column's variable 597TableVariable = next(OrangeUserRole) 598 599 600class TableBarItem(_BarItemDataDelegate): 601 BarRole = next(OrangeUserRole) 602 BarColorRole = next(OrangeUserRole) 603 __slots__ = ("color_schema",) 604 605 def __init__( 606 self, parent=None, color=QColor(255, 170, 127), width=5, 607 barFillRatioRole=BarRole, barColorRole=BarColorRole, 608 color_schema=None, 609 **kwargs 610 ): 611 """ 612 :param QObject parent: Parent object. 613 :param QColor color: Default color of the distribution bar. 614 :param color_schema: 615 If not None it must be an instance of 616 :class:`OWColorPalette.ColorPaletteGenerator` (note: this 617 parameter, if set, overrides the ``color``) 618 :type color_schema: :class:`OWColorPalette.ColorPaletteGenerator` 619 """ 620 super().__init__( 621 parent, color=color, penWidth=width, 622 barFillRatioRole=barFillRatioRole, barColorRole=barColorRole, 623 **kwargs 624 ) 625 self.color_schema = color_schema 626 627 def barColorData(self, index): 628 class_ = self.cachedData(index, TableClassValueRole) 629 if self.color_schema is not None \ 630 and isinstance(class_, Value) \ 631 and isinstance(class_.variable, DiscreteVariable) \ 632 and not math.isnan(class_): 633 return self.color_schema[int(class_)] 634 return self.cachedData(index, self.BarColorRole) 635 636 637from Orange.widgets.utils.colorpalettes import patch_variable_colors 638patch_variable_colors() 639 640 641class HScrollStepMixin: 642 """ 643 Overrides default TableView horizontal behavior (scrolls 1 page at a time) 644 to a friendlier scroll speed akin to that of vertical scrolling. 645 """ 646 647 def __init__(self, *args, **kwargs): 648 super().__init__(*args, **kwargs) 649 self.horizontalScrollBar().setSingleStep(20) 650 651 def wheelEvent(self, event: QWheelEvent): 652 if event.source() == Qt.MouseEventNotSynthesized and \ 653 (event.modifiers() & Qt.ShiftModifier and sys.platform == 'darwin' or 654 event.modifiers() & Qt.AltModifier and sys.platform != 'darwin'): 655 new_event = QWheelEvent( 656 event.pos(), event.globalPos(), event.pixelDelta(), 657 event.angleDelta(), event.buttons(), Qt.NoModifier, 658 event.phase(), event.inverted(), Qt.MouseEventSynthesizedByApplication 659 ) 660 event.accept() 661 super().wheelEvent(new_event) 662 else: 663 super().wheelEvent(event) 664