1''' 2 3.. index:: plot 4 5###################################### 6GUI elements for plots (``owplotgui``) 7###################################### 8 9This module contains functions and classes for creating GUI elements commonly used for plots. 10 11.. autoclass:: OrientedWidget 12 :show-inheritance: 13 14.. autoclass:: StateButtonContainer 15 :show-inheritance: 16 17.. autoclass:: OWToolbar 18 :show-inheritance: 19 20.. autoclass:: OWButton 21 :show-inheritance: 22 23.. autoclass:: OrangeWidgets.plot.OWPlotGUI 24 :members: 25 26''' 27 28import os 29 30from AnyQt.QtWidgets import ( 31 QWidget, QToolButton, QVBoxLayout, QHBoxLayout, QGridLayout, QMenu, 32 QAction, QSizePolicy, QLabel, QStyledItemDelegate, QStyle, QListView 33) 34from AnyQt.QtGui import QIcon, QColor, QFont 35from AnyQt.QtCore import Qt, pyqtSignal, QSize, QRect, QPoint, QMimeData 36 37from Orange.data import ContinuousVariable, DiscreteVariable 38from Orange.widgets import gui 39from Orange.widgets.gui import OrangeUserRole 40from Orange.widgets.utils.listfilter import variables_filter 41from Orange.widgets.utils.itemmodels import DomainModel, VariableListModel 42 43from .owconstants import ( 44 NOTHING, ZOOMING, SELECT, SELECT_POLYGON, PANNING, SELECTION_ADD,\ 45 SELECTION_REMOVE, SELECTION_TOGGLE, SELECTION_REPLACE) 46 47__all__ = ["variables_selection", 48 "OrientedWidget", "OWToolbar", "StateButtonContainer", 49 "OWAction", "OWButton", "OWPlotGUI"] 50 51 52SIZE_POLICY_ADAPTING = (QSizePolicy.Expanding, QSizePolicy.Ignored) 53SIZE_POLICY_FIXED = (QSizePolicy.Minimum, QSizePolicy.Maximum) 54 55 56class VariableSelectionModel(VariableListModel): 57 IsSelected = next(OrangeUserRole) 58 SortRole = next(OrangeUserRole) 59 selection_changed = pyqtSignal() 60 61 def __init__(self, selected_vars, max_vars=None): 62 super().__init__(enable_dnd=True) 63 self.selected_vars = selected_vars 64 self.max_vars = max_vars 65 66 def is_selected(self, index): 67 return self[index.row()] in self.selected_vars 68 69 def is_full(self): 70 if self.max_vars is None: 71 return False 72 else: 73 return len(self.selected_vars) >= self.max_vars 74 75 def data(self, index, role): 76 if role == self.IsSelected: 77 return self.is_selected(index) 78 elif role == Qt.FontRole: 79 font = QFont() 80 font.setBold(self.is_selected(index)) 81 return font 82 elif role == self.SortRole: 83 if self.is_selected(index): 84 return self.selected_vars.index(self[index.row()]) 85 else: 86 return len(self.selected_vars) + index.row() 87 else: 88 return super().data(index, role) 89 90 def toggle_item(self, index): 91 var = self[index.row()] 92 if var in self.selected_vars: 93 self.selected_vars.remove(var) 94 elif not self.is_full(): 95 self.selected_vars.append(var) 96 self.selection_changed.emit() 97 98 def mimeData(self, indexlist): 99 if len(indexlist) != 1: 100 return None 101 mime = QMimeData() 102 mime.setData(self.MIME_TYPE, b'see properties: item_index') 103 mime.setProperty('item_index', indexlist[0]) 104 return mime 105 106 def dropMimeData(self, mime, action, row, column, parent): 107 if action == Qt.IgnoreAction: 108 return True # pragma: no cover 109 if not mime.hasFormat(self.MIME_TYPE): 110 return False # pragma: no cover 111 prev_index = mime.property('item_index') 112 if prev_index is None: 113 return False 114 var = self[prev_index.row()] 115 if self.is_selected(prev_index): 116 self.selected_vars.remove(var) 117 if row < len(self) and self.is_selected(self.index(row)): 118 postpos = self.selected_vars.index(self[row]) 119 self.selected_vars.insert(postpos, var) 120 elif row == 0 or self.is_selected(self.index(row - 1)): 121 self.selected_vars.append(var) 122 self.selection_changed.emit() 123 return True 124 125 # The following methods are disabled to prevent their use by dragging 126 def removeRows(self, *_): 127 return False 128 129 def moveRows(self, *_): 130 return False 131 132 def insertRows(self, *_): 133 return False 134 135 136class VariablesDelegate(QStyledItemDelegate): 137 def paint(self, painter, option, index): 138 rect = QRect(option.rect) 139 140 is_selected = index.data(VariableSelectionModel.IsSelected) 141 full_selection = index.model().sourceModel().is_full() 142 if option.state & QStyle.State_MouseOver: 143 if not full_selection or (full_selection and is_selected): 144 txt = [" Add ", " Remove "][is_selected] 145 txtw = painter.fontMetrics().horizontalAdvance(txt) 146 painter.save() 147 painter.setPen(Qt.NoPen) 148 painter.setBrush(QColor("#ccc")) 149 brect = QRect(rect.x() + rect.width() - 8 - txtw, rect.y(), 150 txtw, rect.height()) 151 painter.drawRoundedRect(brect, 4, 4) 152 painter.restore() 153 painter.drawText(brect, Qt.AlignCenter, txt) 154 155 painter.save() 156 double_pen = painter.pen() 157 double_pen.setWidth(2 * double_pen.width()) 158 if is_selected: 159 next = index.sibling(index.row() + 1, index.column()) 160 if not next.isValid(): 161 painter.setPen(double_pen) 162 painter.drawLine(rect.bottomLeft(), rect.bottomRight()) 163 elif not next.data(VariableSelectionModel.IsSelected): 164 painter.drawLine(rect.bottomLeft(), rect.bottomRight()) 165 elif not index.row(): 166 down = QPoint(0, painter.pen().width()) 167 painter.setPen(double_pen) 168 painter.drawLine(rect.topLeft() + down, rect.topRight() + down) 169 else: 170 prev = index.sibling(index.row() - 1, index.column()) 171 if prev.data(VariableSelectionModel.IsSelected): 172 painter.drawLine(rect.topLeft(), rect.topRight()) 173 painter.restore() 174 175 super().paint(painter, option, index) 176 177 178class VariableSelectionView(QListView): 179 def __init__(self, *args, acceptedType=None, **kwargs): 180 super().__init__(*args, **kwargs) 181 self.setSizePolicy( 182 QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) 183 self.setMinimumHeight(10) 184 self.setMouseTracking(True) 185 self.setAttribute(Qt.WA_Hover) 186 187 self.setSelectionMode(self.SingleSelection) 188 self.setAutoScroll(False) # Prevent scrolling to removed item 189 self.setDragEnabled(True) 190 self.setDropIndicatorShown(True) 191 self.setDragDropMode(self.InternalMove) 192 self.setDefaultDropAction(Qt.MoveAction) 193 self.setDragDropOverwriteMode(False) 194 self.setUniformItemSizes(True) 195 196 self.setItemDelegate(VariablesDelegate()) 197 self.setMinimumHeight(50) 198 199 def sizeHint(self): 200 return QSize(1, 150) 201 202 def mouseMoveEvent(self, e): 203 super().mouseMoveEvent(e) 204 self.update() 205 206 def leaveEvent(self, e): 207 super().leaveEvent(e) 208 self.update() # BUG: This update has no effect, at least not on macOs 209 210 def startDrag(self, supportedActions): 211 super().startDrag(supportedActions) 212 self.selectionModel().clearSelection() 213 214 215def variables_selection(widget, master, model): 216 def update_list(): 217 proxy.sort(0) 218 proxy.invalidate() 219 view.selectionModel().clearSelection() 220 221 filter_edit, view = variables_filter( 222 model=model, parent=master, view_type=VariableSelectionView) 223 proxy = view.model() 224 proxy.setSortRole(model.SortRole) 225 model.selection_changed.connect(update_list) 226 model.dataChanged.connect(update_list) 227 model.modelReset.connect(update_list) 228 model.rowsInserted.connect(update_list) 229 view.clicked.connect( 230 lambda index: model.toggle_item(proxy.mapToSource(index))) 231 master.contextOpened.connect(update_list) 232 widget.layout().addWidget(filter_edit) 233 widget.layout().addSpacing(4) 234 widget.layout().addWidget(view) 235 236 237class OrientedWidget(QWidget): 238 ''' 239 A simple QWidget with a box layout that matches its ``orientation``. 240 ''' 241 def __init__(self, orientation, parent): 242 QWidget.__init__(self, parent) 243 if orientation == Qt.Vertical: 244 self._layout = QVBoxLayout() 245 else: 246 self._layout = QHBoxLayout() 247 self.setLayout(self._layout) 248 249class OWToolbar(OrientedWidget): 250 ''' 251 A toolbar is a container that can contain any number of buttons. 252 253 :param gui: Used to create containers and buttons 254 :type gui: :obj:`.OWPlotGUI` 255 256 :param text: The name of this toolbar 257 :type text: str 258 259 :param orientation: The orientation of this toolbar, either Qt.Vertical or Qt.Horizontal 260 :type tex: int 261 262 :param buttons: A list of button identifiers to be added to this toolbar 263 :type buttons: list of (int or tuple) 264 265 :param parent: The toolbar's parent widget 266 :type parent: :obj:`.QWidget` 267 ''' 268 def __init__(self, gui, text, orientation, buttons, parent, nomargin=False): 269 OrientedWidget.__init__(self, orientation, parent) 270 self.buttons = {} 271 self.groups = {} 272 i = 0 273 n = len(buttons) 274 while i < n: 275 if buttons[i] == gui.StateButtonsBegin: 276 state_buttons = [] 277 for j in range(i+1, n): 278 if buttons[j] == gui.StateButtonsEnd: 279 s = gui.state_buttons(orientation, state_buttons, self, nomargin) 280 self.buttons.update(s.buttons) 281 self.groups[buttons[i+1]] = s 282 i = j 283 self.layout().addStretch() 284 break 285 else: 286 state_buttons.append(buttons[j]) 287 elif buttons[i] == gui.Spacing: 288 self.layout().addSpacing(10) 289 elif type(buttons[i] == int): 290 self.buttons[buttons[i]] = gui.tool_button(buttons[i], self) 291 elif len(buttons[i] == 4): 292 gui.tool_button(buttons[i], self) 293 else: 294 self.buttons[buttons[i][0]] = gui.tool_button(buttons[i], self) 295 i = i + 1 296 297 def select_state(self, state): 298 # SELECT_RECTANGLE = SELECT 299 # SELECT_RIGHTCLICK = SELECT 300 state_buttons = {NOTHING: 11, ZOOMING: 11, SELECT: 13, SELECT_POLYGON: 13, PANNING: 12} 301 self.buttons[state_buttons[state]].click() 302 303 def select_selection_behaviour(self, selection_behaviour): 304 # SelectionAdd = 21 305 # SelectionRemove = 22 306 # SelectionToggle = 23 307 # SelectionOne = 24 308 self.buttons[13]._actions[21 + selection_behaviour].trigger() 309 310 311class StateButtonContainer(OrientedWidget): 312 ''' 313 This class can contain any number of checkable buttons, of which only one can be selected 314 at any time. 315 316 :param gui: Used to create containers and buttons 317 :type gui: :obj:`.OWPlotGUI` 318 319 :param buttons: A list of button identifiers to be added to this toolbar 320 :type buttons: list of (int or tuple) 321 322 :param orientation: The orientation of this toolbar, either Qt.Vertical or Qt.Horizontal 323 :type tex: int 324 325 :param parent: The toolbar's parent widget 326 :type parent: :obj:`.QWidget` 327 ''' 328 def __init__(self, gui, orientation, buttons, parent, nomargin=False): 329 OrientedWidget.__init__(self, orientation, parent) 330 self.buttons = {} 331 if nomargin: 332 self.layout().setContentsMargins(0, 0, 0, 0) 333 self._clicked_button = None 334 for i in buttons: 335 b = gui.tool_button(i, self) 336 b.triggered.connect(self.button_clicked) 337 self.buttons[i] = b 338 self.layout().addWidget(b) 339 340 def button_clicked(self, checked): 341 sender = self.sender() 342 self._clicked_button = sender 343 for button in self.buttons.values(): 344 button.setDown(button is sender) 345 346 def button(self, id): 347 return self.buttons[id] 348 349 def setEnabled(self, enabled): 350 OrientedWidget.setEnabled(self, enabled) 351 if enabled and self._clicked_button: 352 self._clicked_button.click() 353 354class OWAction(QAction): 355 ''' 356 A :obj:`QAction` with convenience methods for calling a callback or 357 setting an attribute of the plot. 358 ''' 359 def __init__(self, plot, icon_name=None, attr_name='', attr_value=None, callback=None, 360 parent=None): 361 QAction.__init__(self, parent) 362 363 if type(callback) == str: 364 callback = getattr(plot, callback, None) 365 if callback: 366 self.triggered.connect(callback) 367 if attr_name: 368 self._plot = plot 369 self.attr_name = attr_name 370 self.attr_value = attr_value 371 self.triggered.connect(self.set_attribute) 372 if icon_name: 373 self.setIcon( 374 QIcon(os.path.join(os.path.dirname(__file__), 375 "../../icons", icon_name + '.png'))) 376 self.setIconVisibleInMenu(True) 377 378 def set_attribute(self, clicked): 379 setattr(self._plot, self.attr_name, self.attr_value) 380 381 382class OWButton(QToolButton): 383 ''' 384 A custom tool button which signal when its down state changes 385 ''' 386 downChanged = pyqtSignal(bool) 387 388 def __init__(self, action=None, parent=None): 389 QToolButton.__init__(self, parent) 390 self.setMinimumSize(30, 30) 391 if action: 392 self.setDefaultAction(action) 393 394 def setDown(self, down): 395 if self.isDown() != down: 396 self.downChanged[bool].emit(down) 397 QToolButton.setDown(self, down) 398 399 400class OWPlotGUI: 401 ''' 402 This class contains functions to create common user interface elements (QWidgets) 403 for configuration and interaction with the ``plot``. 404 405 It provides shorter versions of some methods in :obj:`.gui` that are directly related to an 406 :obj:`.OWPlot` object. 407 408 Normally, you don't have to construct this class manually. Instead, first create the plot, 409 then use the :attr:`.OWPlot.gui` attribute. 410 411 Most methods in this class have similar arguments, so they are explaned here in a single 412 place. 413 414 :param widget: The parent widget which will contain the newly created widget. 415 :type widget: QWidget 416 417 :param id: If ``id`` is an ``int``, a button is constructed from the default table. 418 Otherwise, ``id`` must be tuple with 5 or 6 elements. These elements 419 are explained in the next table. 420 :type id: int or tuple 421 422 :param ids: A list of widget identifiers 423 :type ids: list of id 424 425 :param text: The text displayed on the widget 426 :type text: str 427 428 When using widgets that are specific to your visualization and not included here, you have 429 to provide your 430 own widgets id's. They are a tuple with the following members: 431 432 :param id: An optional unique identifier for the widget. 433 This is only needed if you want to retrive this widget using 434 :obj:`.OWToolbar.buttons`. 435 :type id: int or str 436 437 :param text: The text to be displayed on or next to the widget 438 :type text: str 439 440 :param attr_name: Name of attribute which will be set when the button is clicked. 441 If this widget is checkable, its check state will be set 442 according to the current value of this attribute. 443 If this parameter is empty or None, no attribute will be read or set. 444 :type attr_name: str 445 446 :param attr_value: The value that will be assigned to the ``attr_name`` when the button is 447 clicked. 448 :type attr: any 449 450 :param callback: Function to be called when the button is clicked. 451 If a string is passed as ``callback``, a method by that name of ``plot`` 452 will be called. 453 If this parameter is empty or ``None``, no function will be called 454 :type callback: str or function 455 456 :param icon_name: The filename of the icon for this widget, without the '.png' suffix. 457 :type icon_name: str 458 459 ''' 460 461 JITTER_SIZES = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] 462 463 def __init__(self, master): 464 self._master = master 465 self._plot = master.graph 466 self.color_model = DomainModel( 467 placeholder="(Same color)", valid_types=DomainModel.PRIMITIVE) 468 self.shape_model = DomainModel( 469 placeholder="(Same shape)", valid_types=DiscreteVariable) 470 self.size_model = DomainModel( 471 placeholder="(Same size)", valid_types=ContinuousVariable) 472 self.label_model = DomainModel(placeholder="(No labels)") 473 self.points_models = [self.color_model, self.shape_model, 474 self.size_model, self.label_model] 475 476 Spacing = 0 477 478 ShowLegend = 2 479 ShowFilledSymbols = 3 480 ShowGridLines = 4 481 PointSize = 5 482 AlphaValue = 6 483 Color = 7 484 Shape = 8 485 Size = 9 486 Label = 10 487 488 Zoom = 11 489 Pan = 12 490 Select = 13 491 492 ZoomSelection = 15 493 ZoomReset = 16 494 495 ToolTipShowsAll = 17 496 ClassDensity = 18 497 RegressionLine = 19 498 LabelOnlySelected = 20 499 500 SelectionAdd = 21 501 SelectionRemove = 22 502 SelectionToggle = 23 503 SelectionOne = 24 504 SimpleSelect = 25 505 506 SendSelection = 31 507 ClearSelection = 32 508 ShufflePoints = 33 509 510 StateButtonsBegin = 35 511 StateButtonsEnd = 36 512 513 AnimatePlot = 41 514 AnimatePoints = 42 515 AntialiasPlot = 43 516 AntialiasPoints = 44 517 AntialiasLines = 45 518 DisableAnimationsThreshold = 48 519 AutoAdjustPerformance = 49 520 521 JitterSizeSlider = 51 522 JitterNumericValues = 52 523 524 UserButton = 100 525 526 default_zoom_select_buttons = [ 527 StateButtonsBegin, 528 Zoom, 529 Pan, 530 Select, 531 StateButtonsEnd, 532 Spacing, 533 SendSelection, 534 ClearSelection 535 ] 536 537 _buttons = { 538 Zoom: ('Zoom', 'state', ZOOMING, None, 'Dlg_zoom'), 539 ZoomReset: ('Reset zoom', None, None, None, 'Dlg_zoom_reset'), 540 Pan: ('Pan', 'state', PANNING, None, 'Dlg_pan_hand'), 541 SimpleSelect: ('Select', 'state', SELECT, None, 'Dlg_arrow'), 542 Select: ('Select', 'state', SELECT, None, 'Dlg_arrow'), 543 SelectionAdd: ('Add to selection', 'selection_behavior', SELECTION_ADD, None, 544 'Dlg_select_add'), 545 SelectionRemove: ('Remove from selection', 'selection_behavior', SELECTION_REMOVE, None, 546 'Dlg_select_remove'), 547 SelectionToggle: ('Toggle selection', 'selection_behavior', SELECTION_TOGGLE, None, 548 'Dlg_select_toggle'), 549 SelectionOne: ('Replace selection', 'selection_behavior', SELECTION_REPLACE, None, 550 'Dlg_arrow'), 551 SendSelection: ('Send selection', None, None, 'send_selection', 'Dlg_send'), 552 ClearSelection: ('Clear selection', None, None, 'clear_selection', 'Dlg_clear'), 553 ShufflePoints: ('ShufflePoints', None, None, 'shuffle_points', 'Dlg_sort') 554 } 555 556 _check_boxes = { 557 AnimatePlot : ('Animate plot', 'animate_plot', 'update_animations'), 558 AnimatePoints : ('Animate points', 'animate_points', 'update_animations'), 559 AntialiasPlot : ('Antialias plot', 'antialias_plot', 'update_antialiasing'), 560 AntialiasPoints : ('Antialias points', 'antialias_points', 'update_antialiasing'), 561 AntialiasLines : ('Antialias lines', 'antialias_lines', 'update_antialiasing'), 562 AutoAdjustPerformance : ('Disable effects for large datasets', 'auto_adjust_performance', 563 'update_performance') 564 } 565 566 ''' 567 The list of built-in buttons. It is a map of 568 id : (name, attr_name, attr_value, callback, icon_name) 569 570 .. seealso:: :meth:`.tool_button` 571 ''' 572 573 def _get_callback(self, name, master=None): 574 if type(name) == str: 575 return getattr(master or self._plot, name) 576 else: 577 return name 578 579 def _check_box(self, widget, value, label, cb_name, stateWhenDisabled=None): 580 ''' 581 Adds a :obj:`.QCheckBox` to ``widget``. 582 When the checkbox is toggled, the attribute ``value`` of the plot object is set to 583 the checkbox' check state, and the callback ``cb_name`` is called. 584 ''' 585 args = dict(master=self._plot, value=value, label=label, 586 callback=self._get_callback(cb_name, self._plot), 587 stateWhenDisabled=stateWhenDisabled) 588 if isinstance(widget.layout(), QGridLayout): 589 widget = widget.layout() 590 if isinstance(widget, QGridLayout): 591 checkbox = gui.checkBox(None, **args) 592 widget.addWidget(checkbox, widget.rowCount(), 1) 593 return checkbox 594 else: 595 return gui.checkBox(widget, **args) 596 597 def antialiasing_check_box(self, widget): 598 ''' 599 Creates a check box that toggles the Antialiasing of the plot 600 ''' 601 self._check_box(widget, 'use_antialiasing', 'Use antialiasing', 'update_antialiasing') 602 603 def jitter_size_slider(self, widget, label="Jittering: "): 604 return self.add_control( 605 widget, gui.valueSlider, label, 606 master=self._plot, value='jitter_size', 607 values=getattr(self._plot, "jitter_sizes", self.JITTER_SIZES), 608 callback=self._plot.update_jittering) 609 610 def jitter_numeric_check_box(self, widget): 611 self._check_box( 612 widget=widget, 613 value="jitter_continuous", label="Jitter numeric values", 614 cb_name="update_jittering") 615 616 def show_legend_check_box(self, widget): 617 ''' 618 Creates a check box that shows and hides the plot legend 619 ''' 620 self._check_box(widget, 'show_legend', 'Show legend', 621 'update_legend_visibility') 622 623 def tooltip_shows_all_check_box(self, widget): 624 gui.checkBox( 625 widget=widget, master=self._master, value="tooltip_shows_all", 626 label='Show all data on mouse hover') 627 628 def class_density_check_box(self, widget): 629 self._master.cb_class_density = \ 630 self._check_box(widget=widget, value="class_density", 631 label="Show color regions", 632 cb_name=self._plot.update_density, 633 stateWhenDisabled=False) 634 635 def regression_line_check_box(self, widget): 636 self._master.cb_reg_line = \ 637 self._check_box(widget=widget, value="show_reg_line", 638 label="Show regression line", 639 cb_name=self._plot.update_regression_line) 640 641 def label_only_selected_check_box(self, widget): 642 self._check_box(widget=widget, value="label_only_selected", 643 label="Label only selection and subset", 644 cb_name=self._plot.update_labels) 645 646 def filled_symbols_check_box(self, widget): 647 self._check_box(widget, 'show_filled_symbols', 'Show filled symbols', 648 'update_filled_symbols') 649 650 def grid_lines_check_box(self, widget): 651 self._check_box(widget, 'show_grid', 'Show gridlines', 652 'update_grid_visibility') 653 654 def animations_check_box(self, widget): 655 ''' 656 Creates a check box that enabled or disables animations 657 ''' 658 self._check_box(widget, 'use_animations', 'Use animations', 'update_animations') 659 660 def add_control(self, widget, control, label, **args): 661 if isinstance(widget.layout(), QGridLayout): 662 widget = widget.layout() 663 if isinstance(widget, QGridLayout): 664 row = widget.rowCount() 665 element = control(None, **args) 666 widget.addWidget(QLabel(label), row, 0) 667 widget.addWidget(element, row, 1) 668 return element 669 else: 670 return control(widget, label=label, **args) 671 672 def _slider(self, widget, value, label, min_value, max_value, step, cb_name, 673 show_number=False): 674 return self.add_control( 675 widget, gui.hSlider, label, 676 master=self._plot, value=value, minValue=min_value, 677 maxValue=max_value, step=step, createLabel=show_number, 678 callback=self._get_callback(cb_name, self._master)) 679 680 def point_size_slider(self, widget, label="Symbol size: "): 681 ''' 682 Creates a slider that controls point size 683 ''' 684 return self._slider(widget, 'point_width', label, 1, 20, 1, 'sizes_changed') 685 686 def alpha_value_slider(self, widget, label="Opacity: "): 687 ''' 688 Creates a slider that controls point transparency 689 ''' 690 return self._slider(widget, 'alpha_value', label, 0, 255, 10, 'colors_changed') 691 692 def _combo(self, widget, value, label, cb_name, items=(), model=None): 693 return self.add_control( 694 widget, gui.comboBox, label, 695 master=self._master, value=value, items=items, model=model, 696 callback=self._get_callback(cb_name, self._master), 697 orientation=Qt.Horizontal, 698 sendSelectedValue=True, contentsLength=12, 699 labelWidth=50, searchable=True) 700 701 def color_value_combo(self, widget, label="Color: "): 702 """Creates a combo box that controls point color""" 703 self._combo(widget, "attr_color", label, "colors_changed", 704 model=self.color_model) 705 706 def shape_value_combo(self, widget, label="Shape: "): 707 """Creates a combo box that controls point shape""" 708 self._combo(widget, "attr_shape", label, "shapes_changed", 709 model=self.shape_model) 710 711 def size_value_combo(self, widget, label="Size: "): 712 """Creates a combo box that controls point size""" 713 self._combo(widget, "attr_size", label, "sizes_changed", 714 model=self.size_model) 715 716 def label_value_combo(self, widget, label="Label: "): 717 """Creates a combo box that controls point label""" 718 self._combo(widget, "attr_label", label, "labels_changed", 719 model=self.label_model) 720 721 def box_spacing(self, widget): 722 if isinstance(widget.layout(), QGridLayout): 723 widget = widget.layout() 724 if isinstance(widget, QGridLayout): 725 space = QWidget() 726 space.setFixedSize(12, 12) 727 widget.addWidget(space, widget.rowCount(), 0) 728 else: 729 gui.separator(widget) 730 731 def point_properties_box(self, widget, box='Attributes'): 732 ''' 733 Creates a box with controls for common point properties. 734 Currently, these properties are point size and transparency. 735 ''' 736 box = self.create_gridbox(widget, box) 737 self.add_widgets([ 738 self.Color, 739 self.Shape, 740 self.Size, 741 self.Label, 742 self.LabelOnlySelected], box) 743 return box 744 745 def effects_box(self, widget, box=False): 746 """ 747 Create a box with controls for common plot settings 748 """ 749 box = self.create_gridbox(widget, box) 750 self.add_widgets([ 751 self.PointSize, 752 self.AlphaValue, 753 self.JitterSizeSlider], box) 754 return box 755 756 def plot_properties_box(self, widget, box=None): 757 """ 758 Create a box with controls for common plot settings 759 """ 760 return self.create_box([ 761 self.ClassDensity, 762 self.ShowLegend], widget, box, False) 763 764 _functions = { 765 ShowFilledSymbols: filled_symbols_check_box, 766 JitterSizeSlider: jitter_size_slider, 767 JitterNumericValues: jitter_numeric_check_box, 768 ShowLegend: show_legend_check_box, 769 ShowGridLines: grid_lines_check_box, 770 ToolTipShowsAll: tooltip_shows_all_check_box, 771 ClassDensity: class_density_check_box, 772 RegressionLine: regression_line_check_box, 773 LabelOnlySelected: label_only_selected_check_box, 774 PointSize: point_size_slider, 775 AlphaValue: alpha_value_slider, 776 Color: color_value_combo, 777 Shape: shape_value_combo, 778 Size: size_value_combo, 779 Label: label_value_combo, 780 Spacing: box_spacing 781 } 782 783 def add_widget(self, id, widget): 784 if id in self._functions: 785 self._functions[id](self, widget) 786 elif id in self._check_boxes: 787 label, attr, cb = self._check_boxes[id] 788 self._check_box(widget, attr, label, cb) 789 790 def add_widgets(self, ids, widget): 791 for id in ids: 792 self.add_widget(id, widget) 793 794 def create_box(self, ids, widget, box, name): 795 ''' 796 Creates a :obj:`.QGroupBox` with text ``name`` and adds it to ``widget``. 797 The ``ids`` argument is a list of widget ID's that will be added to this box 798 ''' 799 if box is None: 800 kwargs = {} 801 box = gui.vBox(widget, name, margin=True, 802 contentsMargins=(8, 4, 8, 4)) 803 self.add_widgets(ids, box) 804 return box 805 806 def create_gridbox(self, widget, box=True): 807 grid = QGridLayout() 808 grid.setColumnMinimumWidth(0, 50) 809 grid.setColumnStretch(1, 1) 810 b = gui.widgetBox(widget, box=box, orientation=grid) 811 if not box: 812 b.setContentsMargins(8, 4, 8, 4) 813 # This must come after calling widgetBox, since widgetBox overrides it 814 grid.setVerticalSpacing(8) 815 return b 816 817 def _expand_id(self, id): 818 if type(id) == int: 819 name, attr_name, attr_value, callback, icon_name = self._buttons[id] 820 elif len(id) == 4: 821 name, attr_name, attr_value, callback, icon_name = id 822 id = -1 823 else: 824 id, name, attr_name, attr_value, callback, icon_name = id 825 return id, name, attr_name, attr_value, callback, icon_name 826 827 def tool_button(self, id, widget): 828 ''' 829 Creates an :obj:`.OWButton` and adds it to the parent ``widget``. 830 ''' 831 id, name, attr_name, attr_value, callback, icon_name = self._expand_id(id) 832 if id == OWPlotGUI.Select: 833 b = self.menu_button(self.Select, 834 [self.SelectionOne, self.SelectionAdd, 835 self.SelectionRemove, self.SelectionToggle], 836 widget) 837 else: 838 b = OWButton(parent=widget) 839 ac = OWAction(self._plot, icon_name, attr_name, attr_value, callback, parent=b) 840 b.setDefaultAction(ac) 841 b.setToolTip(name) 842 if widget.layout() is not None: 843 widget.layout().addWidget(b) 844 return b 845 846 def menu_button(self, main_action_id, ids, widget): 847 ''' 848 Creates an :obj:`.OWButton` with a popup-menu and adds it to the parent ``widget``. 849 ''' 850 id, _, attr_name, attr_value, callback, icon_name = self._expand_id(main_action_id) 851 b = OWButton(parent=widget) 852 m = QMenu(b) 853 b.setMenu(m) 854 b._actions = {} 855 856 m.triggered[QAction].connect(b.setDefaultAction) 857 858 if main_action_id: 859 main_action = OWAction(self._plot, icon_name, attr_name, attr_value, callback, 860 parent=b) 861 m.triggered.connect(main_action.trigger) 862 863 for id in ids: 864 id, _, attr_name, attr_value, callback, icon_name = self._expand_id(id) 865 a = OWAction(self._plot, icon_name, attr_name, attr_value, callback, parent=m) 866 m.addAction(a) 867 b._actions[id] = a 868 869 if m.actions(): 870 b.setDefaultAction(m.actions()[0]) 871 elif main_action_id: 872 b.setDefaultAction(main_action) 873 874 875 b.setPopupMode(QToolButton.MenuButtonPopup) 876 b.setMinimumSize(40, 30) 877 return b 878 879 def state_buttons(self, orientation, buttons, widget, nomargin=False): 880 ''' 881 This function creates a set of checkable buttons and connects them so that only one 882 may be checked at a time. 883 ''' 884 c = StateButtonContainer(self, orientation, buttons, widget, nomargin) 885 if widget.layout() is not None: 886 widget.layout().addWidget(c) 887 return c 888 889 def toolbar(self, widget, text, orientation, buttons, nomargin=False): 890 ''' 891 Creates an :obj:`.OWToolbar` with the specified ``text``, ``orientation`` 892 and ``buttons`` and adds it to ``widget``. 893 894 .. seealso:: :obj:`.OWToolbar` 895 ''' 896 t = OWToolbar(self, text, orientation, buttons, widget, nomargin) 897 if nomargin: 898 t.layout().setContentsMargins(0, 0, 0, 0) 899 if widget.layout() is not None: 900 widget.layout().addWidget(t) 901 return t 902 903 def zoom_select_toolbar(self, widget, text='Zoom / Select', orientation=Qt.Horizontal, 904 buttons=default_zoom_select_buttons, nomargin=False): 905 t = self.toolbar(widget, text, orientation, buttons, nomargin) 906 t.buttons[self.SimpleSelect].click() 907 return t 908 909 def theme_combo_box(self, widget): 910 c = gui.comboBox(widget, self._plot, "theme_name", "Theme", 911 callback=self._plot.update_theme, sendSelectedValue=1) 912 c.addItem('Default') 913 c.addItem('Light') 914 c.addItem('Dark') 915 return c 916 917 def box_zoom_select(self, parent): 918 box_zoom_select = gui.vBox(parent, "Zoom/Select") 919 zoom_select_toolbar = self.zoom_select_toolbar( 920 box_zoom_select, nomargin=True, 921 buttons=[self.StateButtonsBegin, 922 self.SimpleSelect, self.Pan, self.Zoom, 923 self.StateButtonsEnd, 924 self.ZoomReset] 925 ) 926 buttons = zoom_select_toolbar.buttons 927 buttons[self.Zoom].clicked.connect(self._plot.zoom_button_clicked) 928 buttons[self.Pan].clicked.connect(self._plot.pan_button_clicked) 929 buttons[self.SimpleSelect].clicked.connect(self._plot.select_button_clicked) 930 buttons[self.ZoomReset].clicked.connect(self._plot.reset_button_clicked) 931 return box_zoom_select 932