class progress(QObject): advance = Signal()
class SchemeLink(QObject): """ A instantiation of a link between two :class:`.SchemeNode` instances in a :class:`.Scheme`. Parameters ---------- source_node : :class:`.SchemeNode` Source node. source_channel : :class:`OutputSignal` The source widget's signal. sink_node : :class:`.SchemeNode` The sink node. sink_channel : :class:`InputSignal` The sink widget's input signal. properties : `dict` Additional link properties. """ #: The link enabled state has changed enabled_changed = Signal(bool) #: The link dynamic enabled state has changed. dynamic_enabled_changed = Signal(bool) #: Runtime link state has changed state_changed = Signal(int) class State(enum.IntEnum): """ Flags indicating the runtime state of a link """ #: The link has no associated state. NoState = 0 #: A link is empty when it has no value on it Empty = 1 #: A link is active when the source node provides a value on output Active = 2 #: A link is pending when it's sink node has not yet been notified #: of a change (note that Empty|Pending is a valid state) Pending = 4 NoState, Empty, Active, Pending = State def __init__( self, source_node, source_channel, sink_node, sink_channel, enabled=True, properties=None, parent=None, ): QObject.__init__(self, parent) self.source_node = source_node if isinstance(source_channel, str): source_channel = source_node.output_channel(source_channel) elif source_channel not in source_node.output_channels(): raise ValueError("%r not in in nodes output channels." % source_channel) self.source_channel = source_channel self.sink_node = sink_node if isinstance(sink_channel, str): sink_channel = sink_node.input_channel(sink_channel) elif sink_channel not in sink_node.input_channels(): raise ValueError("%r not in in nodes input channels." % source_channel) self.sink_channel = sink_channel if not compatible_channels(source_channel, sink_channel): raise IncompatibleChannelTypeError( "Cannot connect %r to %r" % (source_channel.type, sink_channel.type)) self.__enabled = enabled self.__dynamic_enabled = False self.__state = SchemeLink.NoState self.__tool_tip = "" self.properties = properties or {} def source_type(self): """ Return the type of the source channel. """ return name_lookup(self.source_channel.type) def sink_type(self): """ Return the type of the sink channel. """ return name_lookup(self.sink_channel.type) def is_dynamic(self): """ Is this link dynamic. """ return (self.source_channel.dynamic and issubclass(self.sink_type(), self.source_type()) and not (self.sink_type() is self.source_type())) def set_enabled(self, enabled): """ Enable/disable the link. """ if self.__enabled != enabled: self.__enabled = enabled self.enabled_changed.emit(enabled) def enabled(self): """ Is this link enabled. """ return self.__enabled enabled = Property(bool, fget=enabled, fset=set_enabled) def set_dynamic_enabled(self, enabled): """ Enable/disable the dynamic link. Has no effect if the link is not dynamic. """ if self.is_dynamic() and self.__dynamic_enabled != enabled: self.__dynamic_enabled = enabled self.dynamic_enabled_changed.emit(enabled) def dynamic_enabled(self): """ Is this a dynamic link and is `dynamic_enabled` set to `True` """ return self.is_dynamic() and self.__dynamic_enabled dynamic_enabled = Property(bool, fget=dynamic_enabled, fset=set_dynamic_enabled) def set_runtime_state(self, state): """ Set the link's runtime state. Parameters ---------- state : SchemeLink.State """ if self.__state != state: self.__state = state self.state_changed.emit(state) def runtime_state(self): """ Returns ------- state : SchemeLink.State """ return self.__state def set_tool_tip(self, tool_tip): """ Set the link tool tip. """ if self.__tool_tip != tool_tip: self.__tool_tip = tool_tip def tool_tip(self): """ Link tool tip. """ return self.__tool_tip tool_tip = Property(str, fget=tool_tip, fset=set_tool_tip) def __str__(self): return "{0}(({1}, {2}) -> ({3}, {4}))".format( type(self).__name__, self.source_node.title, self.source_channel.name, self.sink_node.title, self.sink_channel.name, )
class CanvasApplication(QApplication): fileOpenRequest = Signal(QUrl) __args = None def __init__(self, argv): CanvasApplication.__args, argv_ = self.parseArguments(argv) ns = CanvasApplication.__args fix_qt_plugins_path() self.__fileOpenUrls = [] self.__in_exec = False if ns.enable_high_dpi_scaling \ and hasattr(Qt, "AA_EnableHighDpiScaling"): # Turn on HighDPI support when available QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) if ns.use_high_dpi_pixmaps \ and hasattr(Qt, "AA_UseHighDpiPixmaps"): QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) if ns.style: argv_ = argv_ + ["-style", self.__args.style] super().__init__(argv_) # Make sure there is an asyncio event loop that runs on the # Qt event loop. _ = get_event_loop() argv[:] = argv_ self.setAttribute(Qt.AA_DontShowIconsInMenus, True) if hasattr(self, "styleHints"): sh = self.styleHints() if hasattr(sh, 'setShowShortcutsInContextMenus'): # PyQt5.13 and up sh.setShowShortcutsInContextMenus(True) if QT_VERSION_INFO < (5, 15): # QTBUG-61707 macos_set_nswindow_tabbing(False) self.configureStyle() def event(self, event): if event.type() == QEvent.FileOpen: if not self.__in_exec: self.__fileOpenUrls.append(event.url()) else: self.fileOpenRequest.emit(event.url()) elif event.type() == QEvent.PolishRequest: self.configureStyle() return super().event(event) def exec(self) -> int: while self.__fileOpenUrls: self.fileOpenRequest.emit(self.__fileOpenUrls.pop(0)) self.__in_exec = True try: return super().exec() finally: self.__in_exec = False exec_ = exec @staticmethod def argumentParser(): parser = argparse.ArgumentParser() parser.add_argument("-style", type=str, default=None) parser.add_argument("-colortheme", type=str, default=None) parser.add_argument("-enable-high-dpi-scaling", type=bool, default=True) parser.add_argument("-use-high-dpi-pixmaps", type=bool, default=True) return parser @staticmethod def parseArguments(argv): parser = CanvasApplication.argumentParser() ns, rest = parser.parse_known_args(argv) if ns.style is not None: if ":" in ns.style: ns.style, colortheme = ns.style.split(":", 1) if ns.colortheme is None: ns.colortheme = colortheme return ns, rest @staticmethod def configureStyle(): from orangecanvas import styles args = CanvasApplication.__args settings = QSettings() settings.beginGroup("application-style") name = settings.value("style-name", "", type=str) if args is not None and args.style: # command line params take precedence name = args.style if name != "": inst = QApplication.instance() if inst is not None: if inst.style().objectName().lower() != name.lower(): QApplication.setStyle(name) theme = settings.value("palette", "", type=str) if args is not None and args.colortheme: theme = args.colortheme if theme and theme in styles.colorthemes: palette = styles.colorthemes[theme]() QApplication.setPalette(palette)
class DataTool(QObject): """ A base class for data tools that operate on PaintViewBox. """ #: Tool mouse cursor has changed cursorChanged = Signal(QCursor) #: User started an editing operation. editingStarted = Signal() #: User ended an editing operation. editingFinished = Signal() #: Emits a data transformation command issueCommand = Signal(object) # Makes for a checkable push-button checkable = True # The tool only works if (at least) two dimensions only2d = True def __init__(self, parent, plot): super().__init__(parent) self._cursor = Qt.ArrowCursor self._plot = plot def cursor(self): return QCursor(self._cursor) def setCursor(self, cursor): if self._cursor != cursor: self._cursor = QCursor(cursor) self.cursorChanged.emit() def mousePressEvent(self, event): return False def mouseMoveEvent(self, event): return False def mouseReleaseEvent(self, event): return False def mouseClickEvent(self, event): return False def mouseDragEvent(self, event): return False def hoverEnterEvent(self, event): return False def hoverLeaveEvent(self, event): return False def mapToPlot(self, point): """Map a point in ViewBox local coordinates into plot coordinates. """ box = self._plot.getViewBox() return box.mapToView(point) def activate(self, ): """Activate the tool""" pass def deactivate(self, ): """Deactivate a tool""" pass
class VariableEditor(QWidget): """ An editor widget for a variable. Can edit the variable name, and its attributes dictionary. """ variable_changed = Signal() def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.var = None # type: Optional[Variable] layout = QVBoxLayout() self.setLayout(layout) self.form = form = QFormLayout( fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow, objectName="editor-form-layout" ) layout.addLayout(self.form) self.name_edit = QLineEdit(objectName="name-editor") self.name_edit.editingFinished.connect( lambda: self.name_edit.isModified() and self.on_name_changed() ) form.addRow("名称:", self.name_edit) vlayout = QVBoxLayout(margin=0, spacing=1) self.labels_edit = view = QTreeView( objectName="annotation-pairs-edit", rootIsDecorated=False, editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed, ) self.labels_model = model = DictItemsModel() view.setModel(model) view.selectionModel().selectionChanged.connect( self.on_label_selection_changed) agrp = QActionGroup(view, objectName="annotate-action-group") action_add = QAction( "+", self, objectName="action-add-label", toolTip="添加新标签。", shortcut=QKeySequence(QKeySequence.New), shortcutContext=Qt.WidgetShortcut ) action_delete = QAction( "\N{MINUS SIGN}", self, objectName="action-delete-label", toolTip="删除选中标签。", shortcut=QKeySequence(QKeySequence.Delete), shortcutContext=Qt.WidgetShortcut ) agrp.addAction(action_add) agrp.addAction(action_delete) view.addActions([action_add, action_delete]) def add_label(): row = [QStandardItem(), QStandardItem()] model.appendRow(row) idx = model.index(model.rowCount() - 1, 0) view.setCurrentIndex(idx) view.edit(idx) def remove_label(): rows = view.selectionModel().selectedRows(0) if rows: assert len(rows) == 1 idx = rows[0].row() model.removeRow(idx) action_add.triggered.connect(add_label) action_delete.triggered.connect(remove_label) agrp.setEnabled(False) self.add_label_action = action_add self.remove_label_action = action_delete # Necessary signals to know when the labels change model.dataChanged.connect(self.on_labels_changed) model.rowsInserted.connect(self.on_labels_changed) model.rowsRemoved.connect(self.on_labels_changed) vlayout.addWidget(self.labels_edit) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) button = FixedSizeButton( self, defaultAction=self.add_label_action, accessibleName="Add", ) hlayout.addWidget(button) button = FixedSizeButton( self, defaultAction=self.remove_label_action, accessibleName="Remove", ) hlayout.addWidget(button) hlayout.addStretch(10) vlayout.addLayout(hlayout) form.addRow("标签:", vlayout) def set_data(self, var, transform=()): # type: (Optional[Variable], Sequence[Transform]) -> None """ Set the variable to edit. """ self.clear() self.var = var if var is not None: name = var.name annotations = var.annotations for tr in transform: if isinstance(tr, Rename): name = tr.name elif isinstance(tr, Annotate): annotations = tr.annotations self.name_edit.setText(name) self.labels_model.set_dict(dict(annotations)) self.add_label_action.actionGroup().setEnabled(True) else: self.add_label_action.actionGroup().setEnabled(False) def get_data(self): """Retrieve the modified variable. """ if self.var is None: return None, [] name = self.name_edit.text().strip() labels = tuple(sorted(self.labels_model.get_dict().items())) tr = [] if self.var.name != name: tr.append(Rename(name)) if self.var.annotations != labels: tr.append(Annotate(labels)) return self.var, tr def clear(self): """Clear the editor state. """ self.var = None self.name_edit.setText("") self.labels_model.setRowCount(0) @Slot() def on_name_changed(self): self.variable_changed.emit() @Slot() def on_labels_changed(self): self.variable_changed.emit() @Slot() def on_label_selection_changed(self): selected = self.labels_edit.selectionModel().selectedRows() self.remove_label_action.setEnabled(bool(len(selected)))
class OWWidget(QDialog, OWComponent, Report, ProgressBarMixin, WidgetMessagesMixin, WidgetSignalsMixin, metaclass=WidgetMetaClass): """Base widget class""" # Global widget count widget_id = 0 # Widget Meta Description # ----------------------- #: Widget name (:class:`str`) as presented in the Canvas name = None id = None category = None version = None #: Short widget description (:class:`str` optional), displayed in #: canvas help tooltips. description = "" #: Widget icon path relative to the defining module icon = "icons/Unknown.png" #: Widget priority used for sorting within a category #: (default ``sys.maxsize``). priority = sys.maxsize help = None help_ref = None url = None keywords = [] background = None replaces = None #: A list of published input definitions inputs = [] #: A list of published output definitions outputs = [] # Default widget GUI layout settings # ---------------------------------- #: Should the widget have basic layout #: (If this flag is false then the `want_main_area` and #: `want_control_area` are ignored). want_basic_layout = True #: Should the widget construct a `mainArea` (this is a resizable #: area to the right of the `controlArea`). want_main_area = True #: Should the widget construct a `controlArea`. want_control_area = True #: Orientation of the buttonsArea box; valid only if #: `want_control_area` is `True`. Possible values are Qt.Horizontal, #: Qt.Vertical and None for no buttons area buttons_area_orientation = Qt.Horizontal #: Specify whether the default message bar widget should be created #: and placed into the default layout. If False then clients are #: responsible for displaying messages within the widget in an #: appropriate manner. want_message_bar = True #: Widget painted by `Save graph` button graph_name = None graph_writers = FileFormat.img_writers save_position = True #: If false the widget will receive fixed size constraint #: (derived from it's layout). Use for widgets which have simple #: static size contents. resizing_enabled = True blockingStateChanged = Signal(bool) processingStateChanged = Signal(int) # Signals have to be class attributes and cannot be inherited, # say from a mixin. This has something to do with the way PyQt binds them progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) settingsHandler = None """:type: SettingsHandler""" #: Version of the settings representation #: Subclasses should increase this number when they make breaking #: changes to settings representation (a settings that used to store #: int now stores string) and handle migrations in migrate and #: migrate_context settings. settings_version = 1 savedWidgetGeometry = settings.Setting(None) #: A list of advice messages (:class:`Message`) to display to the user. #: When a widget is first shown a message from this list is selected #: for display. If a user accepts (clicks 'Ok. Got it') the choice is #: recorded and the message is never shown again (closing the message #: will not mark it as seen). Messages can be displayed again by pressing #: Shift + F1 #: #: :type: list of :class:`Message` UserAdviceMessages = [] contextAboutToBeOpened = Signal(object) contextOpened = Signal() contextClosed = Signal() # pylint: disable=protected-access def __new__(cls, *args, captionTitle=None, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) OWComponent.__init__(self) WidgetMessagesMixin.__init__(self) WidgetSignalsMixin.__init__(self) stored_settings = kwargs.get('stored_settings', None) if self.settingsHandler: self.settingsHandler.initialize(self, stored_settings) self.signalManager = kwargs.get('signal_manager', None) self.__env = _asmappingproxy(kwargs.get("env", {})) self.graphButton = None self.report_button = None OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id captionTitle = self.name if captionTitle is None else captionTitle # must be set without invoking setCaption self.captionTitle = captionTitle self.setWindowTitle(captionTitle) self.setFocusPolicy(Qt.StrongFocus) self.__blocking = False # flag indicating if the widget's position was already restored self.__was_restored = False self.__statusMessage = "" self.__msgwidget = None self.__msgchoice = 0 self.__help_action = QAction("Help", self, objectName="action-help", toolTip="Show help", enabled=False, visible=False, shortcut=QKeySequence(Qt.Key_F1)) self.addAction(self.__help_action) self.left_side = None self.controlArea = self.mainArea = self.buttonsArea = None self.splitter = None if self.want_basic_layout: self.set_basic_layout() sc = QShortcut(QKeySequence(Qt.ShiftModifier | Qt.Key_F1), self) sc.activated.connect(self.__quicktip) sc = QShortcut(QKeySequence.Copy, self) sc.activated.connect(self.copy_to_clipboard) if self.controlArea is not None: # Otherwise, the first control has focus self.controlArea.setFocus(Qt.ActiveWindowFocusReason) if self.splitter is not None: sc = QShortcut( QKeySequence(Qt.ControlModifier | Qt.ShiftModifier | Qt.Key_D), self) sc.activated.connect(self.splitter.flip) return self # pylint: disable=super-init-not-called def __init__(self, *args, **kwargs): """__init__s are called in __new__; don't call them from here""" @classmethod def get_widget_description(cls): if not cls.name: return properties = { name: getattr(cls, name) for name in ("name", "icon", "description", "priority", "keywords", "help", "help_ref", "url", "version", "background", "replaces") } properties["id"] = cls.id or cls.__module__ properties["inputs"] = cls.get_signals("inputs") properties["outputs"] = cls.get_signals("outputs") properties["qualified_name"] = \ "{}.{}".format(cls.__module__, cls.__name__) return properties @classmethod def get_flags(cls): return (Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) class _Splitter(QSplitter): controlAreaVisibilityChanged = Signal(int) def _adjusted_size(self, size_method): size = size_method(super())() height = max((size_method(self.widget(i))().height() for i in range(self.count()) if self.sizes()[i]), default=0) size.setHeight(height) return size def sizeHint(self): return self._adjusted_size(attrgetter("sizeHint")) def minimumSizeHint(self): return self._adjusted_size(attrgetter("minimumSizeHint")) def createHandle(self): """Create splitter handle""" return self._Handle(self.orientation(), self, cursor=Qt.PointingHandCursor) def flip(self): if self.count() == 1: # Prevent hiding control area by shortcut return new_state = int(self.sizes()[0] == 0) self.setSizes([new_state, 100000]) self.controlAreaVisibilityChanged.emit(new_state) self.updateGeometry() class _Handle(QSplitterHandle): def mouseReleaseEvent(self, event): """Resize on left button""" if event.button() == Qt.LeftButton: self.splitter().flip() super().mouseReleaseEvent(event) def mouseMoveEvent(self, event): """Prevent moving; just show/hide""" return def _insert_splitter(self): self.splitter = self._Splitter(Qt.Horizontal, self) self.layout().addWidget(self.splitter) def _insert_control_area(self): self.left_side = gui.vBox(self.splitter, spacing=0) self.splitter.setSizes([1]) # Smallest size allowed by policy if self.buttons_area_orientation is not None: self.controlArea = gui.vBox(self.left_side, addSpace=0) self._insert_buttons_area() else: self.controlArea = self.left_side if self.want_main_area: self.controlArea.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) m = 0 else: m = 4 self.controlArea.layout().setContentsMargins(m, m, m, m) def _insert_buttons_area(self): self.buttonsArea = gui.widgetBox( self.left_side, addSpace=0, spacing=9, orientation=self.buttons_area_orientation) def _insert_main_area(self): self.mainArea = gui.vBox(self.splitter, margin=4, sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding)) self.splitter.addWidget(self.mainArea) self.splitter.setCollapsible(self.splitter.indexOf(self.mainArea), False) self.mainArea.layout().setContentsMargins( 0 if self.want_control_area else 4, 4, 4, 4) def _create_default_buttons(self): # These buttons are inserted in buttons_area, if it exists # Otherwise it is up to the widget to add them to some layout if self.graph_name is not None: self.graphButton = QPushButton("&Save Image", autoDefault=False) self.graphButton.clicked.connect(self.save_graph) if hasattr(self, "send_report"): self.report_button = QPushButton("&Report", autoDefault=False) self.report_button.clicked.connect(self.show_report) def set_basic_layout(self): """Provide the basic widget layout Which parts are created is regulated by class attributes `want_main_area`, `want_control_area`, `want_message_bar` and `buttons_area_orientation`, the presence of method `send_report` and attribute `graph_name`. """ self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(2, 2, 2, 2) if not self.resizing_enabled: self.layout().setSizeConstraint(QVBoxLayout.SetFixedSize) self.want_main_area = self.want_main_area or self.graph_name self._create_default_buttons() self._insert_splitter() if self.want_control_area: self._insert_control_area() if self.want_main_area: self._insert_main_area() if self.want_message_bar: # Use a OverlayWidget for status bar positioning. c = OverlayWidget(self, alignment=Qt.AlignBottom) c.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) c.setWidget(self) c.setLayout(QVBoxLayout()) c.layout().setContentsMargins(0, 0, 0, 0) sb = QStatusBar() sb.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) sb.setSizeGripEnabled(self.resizing_enabled) c.layout().addWidget(sb) help = self.__help_action help_button = SimpleButton( icon=QIcon(gui.resource_filename("icons/help.svg")), toolTip="Show widget help", visible=help.isVisible(), ) @help.changed.connect def _(): help_button.setVisible(help.isVisible()) help_button.setEnabled(help.isEnabled()) help_button.clicked.connect(help.trigger) sb.addWidget(help_button) if self.graph_name is not None: b = SimpleButton( icon=QIcon(gui.resource_filename("icons/chart.svg")), toolTip="Save Image", ) b.clicked.connect(self.save_graph) sb.addWidget(b) if hasattr(self, "send_report"): b = SimpleButton(icon=QIcon( gui.resource_filename("icons/report.svg")), toolTip="Report") b.clicked.connect(self.show_report) sb.addWidget(b) self.message_bar = MessagesWidget(self) self.message_bar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) pb = QProgressBar(maximumWidth=120, minimum=0, maximum=100) pb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored) pb.setAttribute(Qt.WA_LayoutUsesWidgetRect) pb.setAttribute(Qt.WA_MacMiniSize) pb.hide() sb.addPermanentWidget(pb) sb.addPermanentWidget(self.message_bar) def statechanged(): pb.setVisible(bool(self.processingState) or self.isBlocking()) if self.isBlocking() and not self.processingState: pb.setRange(0, 0) # indeterminate pb elif self.processingState: pb.setRange(0, 100) # determinate pb self.processingStateChanged.connect(statechanged) self.blockingStateChanged.connect(statechanged) @self.progressBarValueChanged.connect def _(val): pb.setValue(int(val)) # Reserve the bottom margins for the status bar margins = self.layout().contentsMargins() margins.setBottom(sb.sizeHint().height()) self.setContentsMargins(margins) def save_graph(self): """Save the graph with the name given in class attribute `graph_name`. The method is called by the *Save graph* button, which is created automatically if the `graph_name` is defined. """ graph_obj = getdeepattr(self, self.graph_name, None) if graph_obj is None: return saveplot.save_plot(graph_obj, self.graph_writers) def copy_to_clipboard(self): if self.graph_name: graph_obj = getdeepattr(self, self.graph_name, None) if graph_obj is None: return ClipboardFormat.write_image(None, graph_obj) def __restoreWidgetGeometry(self): def _fullscreen_to_maximized(geometry): """Don't restore windows into full screen mode because it loses decorations and can't be de-fullscreened at least on some platforms. Use Maximized state insted.""" w = QWidget(visible=False) w.restoreGeometry(QByteArray(geometry)) if w.isFullScreen(): w.setWindowState(w.windowState() & ~Qt.WindowFullScreen | Qt.WindowMaximized) return w.saveGeometry() restored = False if self.save_position: geometry = self.savedWidgetGeometry if geometry is not None: geometry = _fullscreen_to_maximized(geometry) restored = self.restoreGeometry(geometry) if restored and not self.windowState() & \ (Qt.WindowMaximized | Qt.WindowFullScreen): space = QApplication.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() #Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) # Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) # Mark as explicitly moved/resized if not already. QDialog would # otherwise adjust position/size on subsequent hide/show # (move/resize events coming from the window manager do not set # these flags). if not self.testAttribute(Qt.WA_Moved): self.setAttribute(Qt.WA_Moved) if not self.testAttribute(Qt.WA_Resized): self.setAttribute(Qt.WA_Resized) return restored def __updateSavedGeometry(self): if self.__was_restored and self.isVisible(): # Update the saved geometry only between explicit show/hide # events (i.e. changes initiated by the user not by Qt's default # window management). # Note: This should always be stored as bytes and not QByteArray. self.savedWidgetGeometry = bytes(self.saveGeometry()) # when widget is resized, save the new width and height def resizeEvent(self, event): """Overloaded to save the geometry (width and height) when the widget is resized. """ QDialog.resizeEvent(self, event) # Don't store geometry if the widget is not visible # (the widget receives a resizeEvent (with the default sizeHint) # before first showEvent and we must not overwrite the the # savedGeometry with it) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def moveEvent(self, event): """Overloaded to save the geometry when the widget is moved """ QDialog.moveEvent(self, event) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def hideEvent(self, event): """Overloaded to save the geometry when the widget is hidden """ if self.save_position: self.__updateSavedGeometry() QDialog.hideEvent(self, event) def closeEvent(self, event): """Overloaded to save the geometry when the widget is closed """ if self.save_position and self.isVisible(): self.__updateSavedGeometry() QDialog.closeEvent(self, event) def showEvent(self, event): """Overloaded to restore the geometry when the widget is shown """ QDialog.showEvent(self, event) if self.save_position and not self.__was_restored: # Restore saved geometry on (first) show self.__restoreWidgetGeometry() self.__was_restored = True self.__quicktipOnce() def wheelEvent(self, event): """Silently accept the wheel event. This is to ensure combo boxes and other controls that have focus don't receive this event unless the cursor is over them. """ event.accept() def setCaption(self, caption): # save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def openContext(self, *a): """Open a new context corresponding to the given data. The settings handler first checks the stored context for a suitable match. If one is found, it becomes the current contexts and the widgets settings are initialized accordingly. If no suitable context exists, a new context is created and data is copied from the widget's settings into the new context. Widgets that have context settings must call this method after reinitializing the user interface (e.g. combo boxes) with the new data. The arguments given to this method are passed to the context handler. Their type depends upon the handler. For instance, `DomainContextHandler` expects `Orange.data.Table` or `Orange.data.Domain`. """ self.contextAboutToBeOpened.emit(a) self.settingsHandler.open_context(self, *a) self.contextOpened.emit() def closeContext(self): """Save the current settings and close the current context. Widgets that have context settings must call this method before reinitializing the user interface (e.g. combo boxes) with the new data. """ self.settingsHandler.close_context(self) self.contextClosed.emit() def retrieveSpecificSettings(self): """ Retrieve data that is not registered as setting. This method is called by `Orange.widgets.settings.ContextHandler.settings_to_widget`. Widgets may define it to retrieve any data that is not stored in widget attributes. See :obj:`Orange.widgets.data.owcolor.OWColor` for an example. """ pass def storeSpecificSettings(self): """ Store data that is not registered as setting. This method is called by `Orange.widgets.settings.ContextHandler.settings_from_widget`. Widgets may define it to store any data that is not stored in widget attributes. See :obj:`Orange.widgets.data.owcolor.OWColor` for an example. """ pass def saveSettings(self): """ Writes widget instance's settings to class defaults. Usually called when the widget is deleted. """ self.settingsHandler.update_defaults(self) def onDeleteWidget(self): """ Invoked by the canvas to notify the widget it has been deleted from the workflow. If possible, subclasses should gracefully cancel any currently executing tasks. """ pass def handleNewSignals(self): """ Invoked by the workflow signal propagation manager after all signals handlers have been called. Reimplement this method in order to coalesce updates from multiple updated inputs. """ pass #: Widget's status message has changed. statusMessageChanged = Signal(str) def setStatusMessage(self, text): """ Set widget's status message. This is a short status string to be displayed inline next to the instantiated widget icon in the canvas. """ if self.__statusMessage != text: self.__statusMessage = text self.statusMessageChanged.emit(text) def statusMessage(self): """ Return the widget's status message. """ return self.__statusMessage def keyPressEvent(self, e): """Handle default key actions or pass the event to the inherited method """ if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible()) } def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the workflow signal manager. This is useful for instance if the widget does it's work in a separate thread or schedules processing from the event queue. In this case it can set the blocking flag in it's processNewSignals method schedule the task and return immediately. After the task has completed the widget can clear the flag and send the updated outputs. .. note:: Failure to clear this flag will block dependent nodes forever. """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """Is this widget blocking signal processing.""" return self.__blocking def resetSettings(self): """Reset the widget settings to default""" self.settingsHandler.reset_settings(self) def workflowEnv(self): """ Return (a view to) the workflow runtime environment. Returns ------- env : types.MappingProxyType """ return self.__env def workflowEnvChanged(self, key, value, oldvalue): """ A workflow environment variable `key` has changed to value. Called by the canvas framework to notify widget of a change in the workflow runtime environment. The default implementation does nothing. """ pass def __showMessage(self, message): if self.__msgwidget is not None: self.__msgwidget.hide() self.__msgwidget.deleteLater() self.__msgwidget = None if message is None: return buttons = MessageOverlayWidget.Ok | MessageOverlayWidget.Close if message.moreurl is not None: buttons |= MessageOverlayWidget.Help if message.icon is not None: icon = message.icon else: icon = Message.Information self.__msgwidget = MessageOverlayWidget(parent=self, text=message.text, icon=icon, wordWrap=True, standardButtons=buttons) btn = self.__msgwidget.button(MessageOverlayWidget.Ok) btn.setText("Ok, got it") self.__msgwidget.setStyleSheet(""" MessageOverlayWidget { background: qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #666, stop:0.3 #6D6D6D, stop:1 #666) } MessageOverlayWidget QLabel#text-label { color: white; }""") if message.moreurl is not None: helpbutton = self.__msgwidget.button(MessageOverlayWidget.Help) helpbutton.setText("Learn more\N{HORIZONTAL ELLIPSIS}") self.__msgwidget.helpRequested.connect( lambda: QDesktopServices.openUrl(QUrl(message.moreurl))) self.__msgwidget.setWidget(self) self.__msgwidget.show() def __quicktip(self): messages = list(self.UserAdviceMessages) if messages: message = messages[self.__msgchoice % len(messages)] self.__msgchoice += 1 self.__showMessage(message) def __quicktipOnce(self): filename = os.path.join(settings.widget_settings_dir(), "user-session-state.ini") namespace = ( "user-message-history/{0.__module__}.{0.__qualname__}".format( type(self))) session_hist = QSettings(filename, QSettings.IniFormat) session_hist.beginGroup(namespace) messages = self.UserAdviceMessages def _ispending(msg): return not session_hist.value("{}/confirmed".format( msg.persistent_id), defaultValue=False, type=bool) messages = [msg for msg in messages if _ispending(msg)] if not messages: return message = messages[self.__msgchoice % len(messages)] self.__msgchoice += 1 self.__showMessage(message) def _userconfirmed(): session_hist = QSettings(filename, QSettings.IniFormat) session_hist.beginGroup(namespace) session_hist.setValue("{}/confirmed".format(message.persistent_id), True) session_hist.sync() self.__msgwidget.accepted.connect(_userconfirmed) @classmethod def migrate_settings(cls, settings, version): """Fix settings to work with the current version of widgets Parameters ---------- settings : dict dict of name - value mappings version : Optional[int] version of the saved settings or None if settings were created before migrations """ @classmethod def migrate_context(cls, context, version): """Fix contexts to work with the current version of widgets
class AddonManagerWidget(QWidget): statechanged = Signal() def __init__(self, parent=None, **kwargs): super(AddonManagerWidget, self).__init__(parent, **kwargs) self.__items = [] self.setLayout(QVBoxLayout()) self.__header = QLabel(wordWrap=True, textFormat=Qt.RichText) self.__search = QLineEdit(placeholderText=self.tr("Filter")) self.layout().addWidget(self.__search) self.__view = view = QTreeView(rootIsDecorated=False, editTriggers=QTreeView.NoEditTriggers, selectionMode=QTreeView.SingleSelection, alternatingRowColors=True) self.__view.setItemDelegateForColumn(0, TristateCheckItemDelegate()) self.layout().addWidget(view) self.__model = model = QStandardItemModel() model.setHorizontalHeaderLabels(["", "Name", "Version", "Action"]) model.dataChanged.connect(self.__data_changed) proxy = QSortFilterProxyModel(filterKeyColumn=1, filterCaseSensitivity=Qt.CaseInsensitive) proxy.setSourceModel(model) self.__search.textChanged.connect(proxy.setFilterFixedString) view.setModel(proxy) view.selectionModel().selectionChanged.connect(self.__update_details) header = self.__view.header() header.setSectionResizeMode(0, QHeaderView.Fixed) header.setSectionResizeMode(2, QHeaderView.ResizeToContents) self.__details = QTextBrowser( frameShape=QTextBrowser.NoFrame, readOnly=True, lineWrapMode=QTextBrowser.WidgetWidth, openExternalLinks=True, ) self.__details.setWordWrapMode(QTextOption.WordWrap) palette = QPalette(self.palette()) palette.setColor(QPalette.Base, Qt.transparent) self.__details.setPalette(palette) self.layout().addWidget(self.__details) def set_items(self, items): self.__items = items model = self.__model model.clear() model.setHorizontalHeaderLabels(["", "Name", "Version", "Action"]) for item in items: if isinstance(item, Installed): installed = True ins, dist = item name = dist.project_name summary = get_dist_meta(dist).get("Summary", "") version = ins.version if ins is not None else dist.version else: installed = False (ins, ) = item dist = None name = ins.name summary = ins.summary version = ins.version updatable = is_updatable(item) item1 = QStandardItem() item1.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | (Qt.ItemIsTristate if updatable else 0)) if installed and updatable: item1.setCheckState(Qt.PartiallyChecked) elif installed: item1.setCheckState(Qt.Checked) else: item1.setCheckState(Qt.Unchecked) item2 = QStandardItem(name) item2.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) item2.setToolTip(summary) item2.setData(item, Qt.UserRole) if updatable: version = "{} < {}".format(dist.version, ins.version) item3 = QStandardItem(version) item3.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) item4 = QStandardItem() item4.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) model.appendRow([item1, item2, item3, item4]) self.__view.resizeColumnToContents(0) self.__view.setColumnWidth(1, max(150, self.__view.sizeHintForColumn(1))) self.__view.setColumnWidth(2, max(150, self.__view.sizeHintForColumn(2))) if self.__items: self.__view.selectionModel().select( self.__view.model().index(0, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows) def item_state(self): steps = [] for i, item in enumerate(self.__items): modelitem = self.__model.item(i, 0) state = modelitem.checkState() if modelitem.flags() & Qt.ItemIsTristate and state == Qt.Checked: steps.append((Upgrade, item)) elif isinstance(item, Available) and state == Qt.Checked: steps.append((Install, item)) elif isinstance(item, Installed) and state == Qt.Unchecked: steps.append((Uninstall, item)) return steps def __selected_row(self): indices = self.__view.selectedIndexes() if indices: proxy = self.__view.model() indices = [proxy.mapToSource(index) for index in indices] return indices[0].row() else: return -1 def set_install_projects(self, names): """Mark for installation the add-ons that match any of names""" model = self.__model for row in range(model.rowCount()): item = model.item(row, 1) if item.text() in names: model.item(row, 0).setCheckState(Qt.Checked) def __data_changed(self, topleft, bottomright): rows = range(topleft.row(), bottomright.row() + 1) for i in rows: modelitem = self.__model.item(i, 0) actionitem = self.__model.item(i, 3) item = self.__items[i] state = modelitem.checkState() flags = modelitem.flags() if flags & Qt.ItemIsTristate and state == Qt.Checked: actionitem.setText("Update") elif isinstance(item, Available) and state == Qt.Checked: actionitem.setText("Install") elif isinstance(item, Installed) and state == Qt.Unchecked: actionitem.setText("Uninstall") else: actionitem.setText("") self.statechanged.emit() def __update_details(self): index = self.__selected_row() if index == -1: self.__details.setText("") else: item = self.__model.item(index, 1) item = item.data(Qt.UserRole) assert isinstance(item, (Installed, Available)) # if isinstance(item, Available): # self.__installed_label.setText("") # self.__available_label.setText(str(item.available.version)) # elif item.installable is not None: # self.__installed_label.setText(str(item.local.version)) # self.__available_label.setText(str(item.available.version)) # else: # self.__installed_label.setText(str(item.local.version)) # self.__available_label.setText("") text = self._detailed_text(item) self.__details.setText(text) def _detailed_text(self, item): if isinstance(item, Installed): remote, dist = item if remote is None: meta = get_dist_meta(dist) description = meta.get("Description") or meta.get('Summary') else: description = remote.description else: description = item[0].description if docutils is not None: try: html = docutils.core.publish_string( trim(description), writer_name="html", settings_overrides={ "output-encoding": "utf-8", # "embed-stylesheet": False, # "stylesheet": [], # "stylesheet_path": [] }).decode("utf-8") except docutils.utils.SystemMessage: html = "<pre>{}<pre>".format(escape(description)) except Exception: html = "<pre>{}<pre>".format(escape(description)) else: html = "<pre>{}<pre>".format(escape(description)) return html def sizeHint(self): return QSize(480, 420)
class OWDataProjectionWidget(OWProjectionWidgetBase, openclass=True): """ Base widget for widgets that get Data and Data Subset (both Orange.data.Table) on the input, and output Selected Data and Data (both Orange.data.Table). Beside that the widget displays data as two-dimensional projection of points. """ class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) class Warning(OWProjectionWidgetBase.Warning): too_many_labels = Msg( "Too many labels to show (zoom in or label only selected)") subset_not_subset = Msg( "Subset data contains some instances that do not appear in " "input data") subset_independent = Msg( "No subset data instances appear in input data") transparent_subset = Msg( "Increase opacity if subset is difficult to see") settingsHandler = DomainContextHandler() selection = Setting(None, schema_only=True) visual_settings = Setting({}, schema_only=True) auto_commit = Setting(True) GRAPH_CLASS = OWScatterPlotBase graph = SettingProvider(OWScatterPlotBase) graph_name = "graph.plot_widget.plotItem" embedding_variables_names = ("proj-x", "proj-y") buttons_area_orientation = Qt.Vertical input_changed = Signal(object) output_changed = Signal(object) def __init__(self): super().__init__() self.subset_data = None self.subset_indices = None self.__pending_selection = self.selection self._invalidated = True self._domain_invalidated = True self.setup_gui() VisualSettingsDialog(self, self.graph.parameter_setter.initial_settings) # GUI def setup_gui(self): self._add_graph() self._add_controls() self._add_buttons() self.input_changed.emit(None) self.output_changed.emit(None) def _add_graph(self): box = gui.vBox(self.mainArea, True, margin=0) self.graph = self.GRAPH_CLASS(self, box) box.layout().addWidget(self.graph.plot_widget) self.graph.too_many_labels.connect( lambda too_many: self.Warning.too_many_labels(shown=too_many)) def _add_controls(self): self.gui = OWPlotGUI(self) area = self.controlArea self._point_box = self.gui.point_properties_box(area) self._effects_box = self.gui.effects_box(area) self._plot_box = self.gui.plot_properties_box(area) def _add_buttons(self): gui.rubber(self.controlArea) self.gui.box_zoom_select(self.buttonsArea) gui.auto_send(self.buttonsArea, self, "auto_commit") @property def effective_variables(self): return self.data.domain.attributes @property def effective_data(self): return self.data.transform( Domain(self.effective_variables, self.data.domain.class_vars, self.data.domain.metas)) # Input @Inputs.data @check_sql_input def set_data(self, data): data_existed = self.data is not None effective_data = self.effective_data if data_existed else None self.closeContext() self.data = data self.check_data() self.init_attr_values() self.openContext(self.data) self._invalidated = not (data_existed and self.data is not None and array_equal( effective_data.X, self.effective_data.X)) self._domain_invalidated = not ( data_existed and self.data is not None and effective_data.domain.checksum() == self.effective_data.domain.checksum()) if self._invalidated: self.clear() self.input_changed.emit(data) self.enable_controls() def check_data(self): self.clear_messages() def enable_controls(self): self.cb_class_density.setEnabled(self.can_draw_density()) @Inputs.data_subset @check_sql_input def set_subset_data(self, subset): self.subset_data = subset def handleNewSignals(self): self._handle_subset_data() if self._invalidated: self._invalidated = False self.setup_plot() else: self.graph.update_point_props() self._update_opacity_warning() self.unconditional_commit() def _handle_subset_data(self): self.Warning.subset_independent.clear() self.Warning.subset_not_subset.clear() if self.data is None or self.subset_data is None: self.subset_indices = set() else: self.subset_indices = set(self.subset_data.ids) ids = set(self.data.ids) if not self.subset_indices & ids: self.Warning.subset_independent() elif self.subset_indices - ids: self.Warning.subset_not_subset() def _update_opacity_warning(self): self.Warning.transparent_subset( shown=self.subset_indices and self.graph.alpha_value < 128) def get_subset_mask(self): if not self.subset_indices: return None valid_data = self.data[self.valid_data] return np.fromiter((ex.id in self.subset_indices for ex in valid_data), dtype=bool, count=len(valid_data)) # Plot def get_embedding(self): """A get embedding method. Derived classes must override this method. The overridden method should return embedding for all data (valid and invalid). Invalid data embedding coordinates should be set to 0 (in some cases to Nan). The method should also set self.valid_data. Returns: np.array: Array of embedding coordinates with shape len(self.data) x 2 """ raise NotImplementedError def get_coordinates_data(self): embedding = self.get_embedding() if embedding is not None and len(embedding[self.valid_data]): return embedding[self.valid_data].T return None, None def setup_plot(self): self.graph.reset_graph() self.__pending_selection = self.selection or self.__pending_selection self.apply_selection() # Selection def apply_selection(self): pending = self.__pending_selection if self.data is not None and pending is not None and len(pending) \ and max(i for i, _ in pending) < self.graph.n_valid: index_group = np.array(pending).T selection = np.zeros(self.graph.n_valid, dtype=np.uint8) selection[index_group[0]] = index_group[1] self.selection = self.__pending_selection self.__pending_selection = None self.graph.selection = selection self.graph.update_selection_colors() if self.graph.label_only_selected: self.graph.update_labels() def selection_changed(self): sel = None if self.data and isinstance(self.data, SqlTable) \ else self.graph.selection self.selection = [(i, x) for i, x in enumerate(sel) if x] \ if sel is not None else None self.commit() # Output def commit(self): self.send_data() def send_data(self): group_sel, data, graph = None, self._get_projection_data(), self.graph if graph.selection is not None: group_sel = np.zeros(len(data), dtype=int) group_sel[self.valid_data] = graph.selection selected = self._get_selected_data(data, graph.get_selection(), group_sel) self.output_changed.emit(selected) self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send( self._get_annotated_data(data, group_sel, graph.selection)) def _get_projection_data(self): if self.data is None or self.embedding_variables_names is None: return self.data variables = self._get_projection_variables() data = self.data.transform( Domain(self.data.domain.attributes, self.data.domain.class_vars, self.data.domain.metas + variables)) data.metas[:, -2:] = self.get_embedding() return data def _get_projection_variables(self): names = get_unique_names(self.data.domain, self.embedding_variables_names) return ContinuousVariable(names[0]), ContinuousVariable(names[1]) @staticmethod def _get_selected_data(data, selection, group_sel): return create_groups_table(data, group_sel, False, "Group") \ if len(selection) else None @staticmethod def _get_annotated_data(data, group_sel, graph_sel): if data is None: return None if graph_sel is not None and np.max(graph_sel) > 1: return create_groups_table(data, group_sel) else: if group_sel is None: mask = np.full((len(data), ), False) else: mask = np.nonzero(group_sel)[0] return create_annotated_table(data, mask) # Report def send_report(self): if self.data is None: return caption = self._get_send_report_caption() self.report_plot() if caption: self.report_caption(caption) def _get_send_report_caption(self): return report.render_items_vert( (("Color", self._get_caption_var_name(self.attr_color)), ("Label", self._get_caption_var_name(self.attr_label)), ("Shape", self._get_caption_var_name(self.attr_shape)), ("Size", self._get_caption_var_name(self.attr_size)), ("Jittering", self.graph.jitter_size != 0 and "{} %".format(self.graph.jitter_size)))) # Customize plot def set_visual_settings(self, key, value): self.graph.parameter_setter.set_parameter(key, value) self.visual_settings[key] = value @staticmethod def _get_caption_var_name(var): return var.name if isinstance(var, Variable) else var # Misc def sizeHint(self): return QSize(1132, 708) def clear(self): self.selection = None self.graph.selection = None def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() self.graph.clear()
class LabelSelectionWidget(QWidget): """ A widget for selection of label values. The widget displays the contents of the model with two widgets: * The top level model items are displayed in a combo box. * The children of the current selected top level item are displayed in a subordinate list view. .. note:: This is not a QAbstractItemView subclass. """ # Current group/root index has changed. groupChanged = Signal(int) # Selection for the current group/root has changed. groupSelectionChanged = Signal(int) def __init__(self): super().__init__() self.__model = None self.__selectionMode = QListView.ExtendedSelection self.__currentIndex = -1 self.__selections = {} self.__parent = None self.targets = [] layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) def group_box(title): box = QGroupBox(title) box.setFlat(True) lay = QVBoxLayout() lay.setContentsMargins(0, 0, 0, 0) box.setLayout(lay) return box self.labels_combo = QComboBox() self.values_view = QListView(selectionMode=self.__selectionMode) self.labels_combo.currentIndexChanged.connect( self.__onCurrentIndexChanged) l_box = group_box(self.tr("Label")) v_box = group_box(self.tr("Values")) l_box.layout().addWidget(self.labels_combo) v_box.layout().addWidget(self.values_view) layout.addWidget(l_box) layout.addWidget(v_box) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def set_selection(self): model = self.model() # FIX: This assumes fixed target key/values order. indices = [ model.index(i, 0, model.index(keyind, 0)) for keyind, selected in enumerate(self.__parent.stored_selections) for i in selected ] selection = itemselection(indices) self.setCurrentGroupIndex(self.__parent.current_group_index) self.setSelection(selection) def selected_split(self): group_index = self.currentGroupIndex() if not (0 <= group_index < len(self.targets)): return None, [] group = self.targets[group_index] selection = [ ind.row() for ind in self.currentGroupSelection().indexes() ] return group, selection def set_data(self, parent, data): """ Initialize widget state after receiving new data. """ self.__parent = parent # self.__currentIndex = parent.current_group_index if data is not None: column_groups, row_groups = group_candidates(data) self.targets = column_groups + row_groups model = QStandardItemModel() for item in [standarditem_from(desc) for desc in self.targets]: model.appendRow(item) self.setModel(model) else: self.targets = [] self.setModel(None) def clear(self): """ Clear the widget/model (same as ``setModel(None)``). """ if self.__model is not None: self.values_view.selectionModel().clearSelection() self.values_view.selectionModel().selectionChanged.disconnect( self.__onSelectionChanged) self.values_view.setModel(None) self.labels_combo.setModel(QStandardItemModel(self.labels_combo)) self.__currentIndex = -1 self.__selections = {} self.__model = None def setModel(self, model): """ Set the source model for display. The model should be a tree model with depth 2. Parameters ---------- model : QtCore.QAbstractItemModel """ if model is self.__model: return self.clear() if model is None: return self.__model = model self.values_view.setModel(model) self.values_view.setRootIndex(model.index(0, 0)) self.values_view.selectionModel().selectionChanged.connect( self.__onSelectionChanged) # will emit the currentIndexChanged (if the model is not empty) self.labels_combo.setModel(model) def model(self): """ Return the current model. Returns ------- model : QtCore.QAbstractItemModel """ return self.__model def setCurrentGroupIndex(self, index): """ Set the current selected group/root index row. Parameters ---------- index : int Group index. """ self.labels_combo.setCurrentIndex(index) def currentGroupIndex(self): """ Return the current selected group/root index row. Returns ------- row : index Current group row index (-1 if there is no current index) """ return self.labels_combo.currentIndex() def setSelection(self, selection): """ Set the model item selection. Parameters ---------- selection : QtCore.QItemSelection Item selection. """ if self.values_view.selectionModel() is not None: indices = selection.indexes() pind = defaultdict(list) for index in indices: parent = index.parent() if parent.isValid(): if parent == self.__model.index(parent.row(), parent.column()): pind[parent.row()].append(QPersistentModelIndex(index)) else: warnings.warn("Die Die Die") else: # top level index pass self.__selections = pind self.__restoreSelection() def selection(self): """ Return the item selection. Returns ------- selection : QtCore.QItemSelection """ selection = QItemSelection() if self.__model is None: return selection for pind in chain(*self.__selections.values()): ind = self.__model.index(pind.row(), pind.column(), pind.parent()) if ind.isValid(): selection.select(ind, ind) return selection def currentGroupSelection(self): """ Return the item selection for the current group only. """ if self.values_view.selectionModel() is not None: return self.values_view.selectionModel().selection() else: return QItemSelection() def __onCurrentIndexChanged(self, index): self.__storeSelection(self.__currentIndex, self.values_view.selectedIndexes()) self.__currentIndex = index if self.__model is not None: root = self.__model.index(index, 0) self.values_view.setRootIndex(root) self.__restoreSelection() self.groupChanged.emit(index) def __onSelectionChanged(self, old, new): self.__storeSelection(self.__currentIndex, self.values_view.selectedIndexes()) self.groupSelectionChanged.emit(self.__currentIndex) def __storeSelection(self, groupind, indices): # Store current values selection for the current group groupind = self.__currentIndex indices = [ QPersistentModelIndex(ind) for ind in self.values_view.selectedIndexes() ] self.__selections[groupind] = indices def __restoreSelection(self): # Restore previous selection for root (if available) assert self.__model is not None groupind = self.__currentIndex root = self.__model.index(groupind, 0) sel = self.__selections.get(groupind, []) indices = [ self.__model.index(pind.row(), pind.column(), root) for pind in sel if pind.isValid() and pind.parent() == root ] selection = QItemSelection() for ind in indices: selection.select(ind, ind) self.values_view.selectionModel().select( selection, QItemSelectionModel.ClearAndSelect) def sizeHint(self): """Reimplemented from QWidget.sizeHint""" return QSize(100, 200)
class ControlPointRect(QGraphicsObject): class Constraint(enum.IntEnum): Free = 0 KeepAspectRatio = 1 KeepCenter = 2 Free = Constraint.Free KeepAspectRatio = Constraint.KeepAspectRatio KeepCenter = Constraint.KeepCenter rectChanged = Signal(QRectF) rectEdited = Signal(QRectF) def __init__(self, parent=None, rect=QRectF(), constraints=Free, **kwargs): # type: (Optional[QGraphicsItem], QRectF, Constraint, Any) -> None super().__init__(parent, **kwargs) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setFlag(QGraphicsItem.ItemIsFocusable) self.__rect = QRectF(rect) if rect is not None else QRectF() self.__margins = QMargins() points = \ [ControlPoint(self, ControlPoint.Left, constraint=Qt.Horizontal), ControlPoint(self, ControlPoint.Top, constraint=Qt.Vertical), ControlPoint(self, ControlPoint.TopLeft), ControlPoint(self, ControlPoint.Right, constraint=Qt.Horizontal), ControlPoint(self, ControlPoint.TopRight), ControlPoint(self, ControlPoint.Bottom, constraint=Qt.Vertical), ControlPoint(self, ControlPoint.BottomLeft), ControlPoint(self, ControlPoint.BottomRight) ] assert (points == sorted(points, key=lambda p: p.anchor())) self.__points = dict((p.anchor(), p) for p in points) if self.scene(): self.__installFilter() for p in points: p.setFlag(QGraphicsItem.ItemIsFocusable) p.setFocusProxy(self) self.__constraints = constraints self.__activeControl = None # type: Optional[ControlPoint] self.__pointsLayout() def controlPoint(self, anchor): # type: (ControlPoint.Anchor) -> ControlPoint """ Return the anchor point (:class:`ControlPoint`) for anchor position. """ return self.__points[anchor] def setRect(self, rect): # type: (QRectF) -> None """ Set the control point rectangle (:class:`QRectF`) """ if self.__rect != rect: self.__rect = QRectF(rect) self.__pointsLayout() self.prepareGeometryChange() self.rectChanged.emit(rect.normalized()) def rect(self): # type: () -> QRectF """ Return the control point rectangle. """ # Return the rect normalized. During the control point move the # rect can change to an invalid size, but the layout must still # know to which point does an unnormalized rect side belong, # so __rect is left unnormalized. # NOTE: This means all signal emits (rectChanged/Edited) must # also emit normalized rects return self.__rect.normalized() rect_ = Property(QRectF, fget=rect, fset=setRect, user=True) def setControlMargins(self, *margins): # type: (int) -> None """Set the controls points on the margins around `rect` """ if len(margins) > 1: margins = QMargins(*margins) elif len(margins) == 1: margin = margins[0] margins = QMargins(margin, margin, margin, margin) else: raise TypeError if self.__margins != margins: self.__margins = margins self.__pointsLayout() def controlMargins(self): # type: () -> QMargins return QMargins(self.__margins) def setConstraints(self, constraints): raise NotImplementedError def isControlActive(self): # type: () -> bool """Return the state of the control. True if the control is active (user is dragging one of the points) False otherwise. """ return self.__activeControl is not None def itemChange(self, change, value): # type: (QGraphicsItem.GraphicsItemChange, Any) -> Any if change == QGraphicsItem.ItemSceneHasChanged and self.scene(): self.__installFilter() return super().itemChange(change, value) def sceneEventFilter(self, obj, event): # type: (QGraphicsItem, QEvent) -> bool obj = toGraphicsObjectIfPossible(obj) if isinstance(obj, ControlPoint): etype = event.type() if etype in (QEvent.GraphicsSceneMousePress, QEvent.GraphicsSceneMouseDoubleClick) and \ event.button() == Qt.LeftButton: self.__setActiveControl(obj) elif etype == QEvent.GraphicsSceneMouseRelease and \ event.button() == Qt.LeftButton: self.__setActiveControl(None) return super().sceneEventFilter(obj, event) def __installFilter(self): # type: () -> None # Install filters on the control points. for p in self.__points.values(): p.installSceneEventFilter(self) def __pointsLayout(self): # type: () -> None """Layout the control points """ rect = self.__rect margins = self.__margins rect = rect.adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()) center = rect.center() cx, cy = center.x(), center.y() left, top, right, bottom = \ rect.left(), rect.top(), rect.right(), rect.bottom() self.controlPoint(ControlPoint.Left).setPos(left, cy) self.controlPoint(ControlPoint.Right).setPos(right, cy) self.controlPoint(ControlPoint.Top).setPos(cx, top) self.controlPoint(ControlPoint.Bottom).setPos(cx, bottom) self.controlPoint(ControlPoint.TopLeft).setPos(left, top) self.controlPoint(ControlPoint.TopRight).setPos(right, top) self.controlPoint(ControlPoint.BottomLeft).setPos(left, bottom) self.controlPoint(ControlPoint.BottomRight).setPos(right, bottom) def __setActiveControl(self, control): # type: (Optional[ControlPoint]) -> None if self.__activeControl != control: if self.__activeControl is not None: self.__activeControl.positionChanged[QPointF].disconnect( self.__activeControlMoved) self.__activeControl = control if control is not None: control.positionChanged[QPointF].connect( self.__activeControlMoved) def __activeControlMoved(self, pos): # type: (QPointF) -> None # The active control point has moved, update the control # rectangle control = self.__activeControl assert control is not None pos = control.pos() rect = QRectF(self.__rect) margins = self.__margins # TODO: keyboard modifiers and constraints. anchor = control.anchor() if anchor & ControlPoint.Top: rect.setTop(pos.y() + margins.top()) elif anchor & ControlPoint.Bottom: rect.setBottom(pos.y() - margins.bottom()) if anchor & ControlPoint.Left: rect.setLeft(pos.x() + margins.left()) elif anchor & ControlPoint.Right: rect.setRight(pos.x() - margins.right()) changed = self.__rect != rect self.blockSignals(True) self.setRect(rect) self.blockSignals(False) if changed: self.rectEdited.emit(rect.normalized()) def boundingRect(self): # type: () -> QRectF return QRectF()
class ControlPointLine(QGraphicsObject): lineChanged = Signal(QLineF) lineEdited = Signal(QLineF) def __init__(self, parent=None, **kwargs): # type: (Optional[QGraphicsItem], Any) -> None super().__init__(parent, **kwargs) self.setFlag(QGraphicsItem.ItemHasNoContents) self.setFlag(QGraphicsItem.ItemIsFocusable) self.__line = QLineF() self.__points = \ [ControlPoint(self, ControlPoint.TopLeft), # TopLeft is line start ControlPoint(self, ControlPoint.BottomRight) # line end ] self.__activeControl = None # type: Optional[ControlPoint] if self.scene(): self.__installFilter() for p in self.__points: p.setFlag(QGraphicsItem.ItemIsFocusable) p.setFocusProxy(self) def setLine(self, line): # type: (QLineF) -> None if not isinstance(line, QLineF): raise TypeError() if line != self.__line: self.__line = QLineF(line) self.__pointsLayout() self.lineChanged.emit(line) def line(self): # type: () -> QLineF return QLineF(self.__line) def isControlActive(self): # type: () -> bool """Return the state of the control. True if the control is active (user is dragging one of the points) False otherwise. """ return self.__activeControl is not None def __installFilter(self): # type: () -> None for p in self.__points: p.installSceneEventFilter(self) def itemChange(self, change, value): # type: (QGraphicsItem.GraphicsItemChange, Any) -> Any if change == QGraphicsItem.ItemSceneHasChanged: if self.scene(): self.__installFilter() return super().itemChange(change, value) def sceneEventFilter(self, obj, event): # type: (QGraphicsItem, QEvent) -> bool obj = toGraphicsObjectIfPossible(obj) if isinstance(obj, ControlPoint): etype = event.type() if etype in (QEvent.GraphicsSceneMousePress, QEvent.GraphicsSceneMouseDoubleClick): self.__setActiveControl(obj) elif etype == QEvent.GraphicsSceneMouseRelease: self.__setActiveControl(None) return super().sceneEventFilter(obj, event) def __pointsLayout(self): # type: () -> None self.__points[0].setPos(self.__line.p1()) self.__points[1].setPos(self.__line.p2()) def __setActiveControl(self, control): # type: (Optional[ControlPoint]) -> None if self.__activeControl != control: if self.__activeControl is not None: self.__activeControl.positionChanged[QPointF].disconnect( self.__activeControlMoved) self.__activeControl = control if control is not None: control.positionChanged[QPointF].connect( self.__activeControlMoved) def __activeControlMoved(self, pos): # type: (QPointF) -> None line = QLineF(self.__line) control = self.__activeControl assert control is not None if control.anchor() == ControlPoint.TopLeft: line.setP1(pos) elif control.anchor() == ControlPoint.BottomRight: line.setP2(pos) if self.__line != line: self.blockSignals(True) self.setLine(line) self.blockSignals(False) self.lineEdited.emit(line) def boundingRect(self): # type: () -> QRectF return QRectF()
class CanvasView(QGraphicsView): """Canvas View handles the zooming. """ def __init__(self, *args): super().__init__(*args) self.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.grabGesture(Qt.PinchGesture) self.__backgroundIcon = QIcon() self.__autoScroll = False self.__autoScrollMargin = 16 self.__autoScrollTimer = QTimer(self) self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance) # scale factor accumulating partial increments from wheel events self.__zoomLevel = 100 # effective scale level(rounded to whole integers) self.__effectiveZoomLevel = 100 self.__zoomInAction = QAction( self.tr("Zoom in"), self, objectName="action-zoom-in", shortcut=QKeySequence.ZoomIn, triggered=self.zoomIn, ) self.__zoomOutAction = QAction(self.tr("Zoom out"), self, objectName="action-zoom-out", shortcut=QKeySequence.ZoomOut, triggered=self.zoomOut) self.__zoomResetAction = QAction( self.tr("Reset Zoom"), self, objectName="action-zoom-reset", triggered=self.zoomReset, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0)) def setScene(self, scene): super().setScene(scene) self._ensureSceneRect(scene) def _ensureSceneRect(self, scene): r = scene.addRect(QRectF(0, 0, 400, 400)) scene.sceneRect() scene.removeItem(r) def setAutoScrollMargin(self, margin): self.__autoScrollMargin = margin def autoScrollMargin(self): return self.__autoScrollMargin def setAutoScroll(self, enable): self.__autoScroll = enable def autoScroll(self): return self.__autoScroll def mousePressEvent(self, event): super().mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: if not self.__autoScrollTimer.isActive() and \ self.__shouldAutoScroll(event.pos()): self.__startAutoScroll() super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() & Qt.LeftButton: self.__stopAutoScroll() return super().mouseReleaseEvent(event) def wheelEvent(self, event: QWheelEvent): if event.modifiers() & Qt.ControlModifier \ and event.buttons() == Qt.NoButton: # Zoom delta = event.angleDelta().y() # use mouse position as anchor while zooming anchor = self.transformationAnchor() self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120) self.setTransformationAnchor(anchor) event.accept() elif event.source() == Qt.MouseEventNotSynthesized \ and (event.angleDelta().x() == 0 \ and not self.verticalScrollBar().isVisible()) \ or (sys.platform == 'darwin' and event.modifiers() & Qt.ShiftModifier or sys.platform != 'darwin' and event.modifiers() & Qt.AltModifier): # Scroll horizontally x, y = event.angleDelta().x(), event.angleDelta().y() sign_value = x if x != 0 else y sign = 1 if sign_value >= 0 else -1 new_angle_delta = QPoint(sign * max(abs(x), abs(y), sign_value), 0) new_pixel_delta = QPoint(0, 0) new_modifiers = event.modifiers() & ~(Qt.ShiftModifier | Qt.AltModifier) new_event = QWheelEvent(event.pos(), event.globalPos(), new_pixel_delta, new_angle_delta, event.buttons(), new_modifiers, event.phase(), event.inverted(), event.source()) event.accept() super().wheelEvent(new_event) else: super().wheelEvent(event) def gestureEvent(self, event: QGestureEvent): gesture = event.gesture(Qt.PinchGesture) if gesture is None: return if gesture.state() == Qt.GestureStarted: event.accept(gesture) elif gesture.changeFlags() & QPinchGesture.ScaleFactorChanged: anchor = gesture.centerPoint().toPoint() anchor = self.mapToScene(anchor) self.__setZoomLevel(self.__zoomLevel * gesture.scaleFactor(), anchor=anchor) event.accept() elif gesture.state() == Qt.GestureFinished: event.accept() def event(self, event: QEvent) -> bool: if event.type() == QEvent.Gesture: self.gestureEvent(cast(QGestureEvent, event)) return super().event(event) def zoomIn(self): self.__setZoomLevel(self.__zoomLevel + 10) def zoomOut(self): self.__setZoomLevel(self.__zoomLevel - 10) def zoomReset(self): """ Reset the zoom level. """ self.__setZoomLevel(100) def zoomLevel(self): # type: () -> float """ Return the current zoom level. Level is expressed in percentages; 100 is unscaled, 50 is half size, ... """ return self.__effectiveZoomLevel def setZoomLevel(self, level): self.__setZoomLevel(level) def __setZoomLevel(self, scale, anchor=None): # type: (float, Optional[QPointF]) -> None self.__zoomLevel = max(30, min(scale, 300)) scale = round(self.__zoomLevel) self.__zoomOutAction.setEnabled(scale != 30) self.__zoomInAction.setEnabled(scale != 300) if self.__effectiveZoomLevel != scale: self.__effectiveZoomLevel = scale transform = QTransform() transform.scale(scale / 100, scale / 100) if anchor is not None: anchor = self.mapFromScene(anchor) self.setTransform(transform) if anchor is not None: center = self.viewport().rect().center() diff = self.mapToScene(center) - self.mapToScene(anchor) self.centerOn(anchor + diff) self.zoomLevelChanged.emit(scale) zoomLevelChanged = Signal(float) zoomLevel_ = Property(float, zoomLevel, setZoomLevel, notify=zoomLevelChanged) def __shouldAutoScroll(self, pos): if self.__autoScroll: margin = self.__autoScrollMargin viewrect = self.contentsRect() rect = viewrect.adjusted(margin, margin, -margin, -margin) # only do auto scroll when on the viewport's margins return not rect.contains(pos) and viewrect.contains(pos) else: return False def __startAutoScroll(self): self.__autoScrollTimer.start(10) log.debug("Auto scroll timer started") def __stopAutoScroll(self): if self.__autoScrollTimer.isActive(): self.__autoScrollTimer.stop() log.debug("Auto scroll timer stopped") def __autoScrollAdvance(self): """Advance the auto scroll """ pos = QCursor.pos() pos = self.mapFromGlobal(pos) margin = self.__autoScrollMargin vvalue = self.verticalScrollBar().value() hvalue = self.horizontalScrollBar().value() vrect = QRect(0, 0, self.width(), self.height()) # What should be the speed advance = 10 # We only do auto scroll if the mouse is inside the view. if vrect.contains(pos): if pos.x() < vrect.left() + margin: self.horizontalScrollBar().setValue(hvalue - advance) if pos.y() < vrect.top() + margin: self.verticalScrollBar().setValue(vvalue - advance) if pos.x() > vrect.right() - margin: self.horizontalScrollBar().setValue(hvalue + advance) if pos.y() > vrect.bottom() - margin: self.verticalScrollBar().setValue(vvalue + advance) if self.verticalScrollBar().value() == vvalue and \ self.horizontalScrollBar().value() == hvalue: self.__stopAutoScroll() else: self.__stopAutoScroll() log.debug("Auto scroll advance") def setBackgroundIcon(self, icon): if not isinstance(icon, QIcon): raise TypeError("A QIcon expected.") if self.__backgroundIcon != icon: self.__backgroundIcon = icon self.viewport().update() def backgroundIcon(self): return QIcon(self.__backgroundIcon) def drawBackground(self, painter, rect): super().drawBackground(painter, rect) if not self.__backgroundIcon.isNull(): painter.setClipRect(rect) vrect = QRect(QPoint(0, 0), self.viewport().size()) vrect = self.mapToScene(vrect).boundingRect() pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo( QSize(200, 200))) pmrect = QRect(QPoint(0, 0), pm.size()) pmrect.moveCenter(vrect.center().toPoint()) if rect.toRect().intersects(pmrect): painter.drawPixmap(pmrect, pm)
class OWSieveDiagram(OWWidget): name = "筛图" description = "可视化观察到的和期望的频率对于值的组合" icon = "icons/SieveDiagram.svg" priority = 200 keywords = [] class Inputs: data = Input("数据", Table, default=True) features = Input("特征", AttributeList) class Outputs: selected_data = Output("被选数据", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) graph_name = "画布" want_control_area = False settings_version = 1 settingsHandler = DomainContextHandler() attr_x = ContextSetting(None) attr_y = ContextSetting(None) selection = ContextSetting(set()) xy_changed_manually = Signal(Variable, Variable) def __init__(self): # pylint: disable=missing-docstring super().__init__() self.data = self.discrete_data = None self.attrs = [] self.input_features = None self.areas = [] self.selection = set() self.attr_box = gui.hBox(self.mainArea) self.domain_model = DomainModel(valid_types=DomainModel.PRIMITIVE) combo_args = dict(widget=self.attr_box, master=self, contentsLength=12, callback=self.attr_changed, sendSelectedValue=True, valueType=str, model=self.domain_model) fixed_size = (QSizePolicy.Fixed, QSizePolicy.Fixed) gui.comboBox(value="attr_x", **combo_args) gui.widgetLabel(self.attr_box, "\u2715", sizePolicy=fixed_size) gui.comboBox(value="attr_y", **combo_args) self.vizrank, self.vizrank_button = SieveRank.add_vizrank( self.attr_box, self, "分数组合", self.set_attr) self.vizrank_button.setSizePolicy(*fixed_size) self.canvas = QGraphicsScene() self.canvasView = ViewWithPress(self.canvas, self.mainArea, handler=self.reset_selection) self.mainArea.layout().addWidget(self.canvasView) self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) def sizeHint(self): return QSize(450, 550) def resizeEvent(self, event): super().resizeEvent(event) self.update_graph() def showEvent(self, event): super().showEvent(event) self.update_graph() @classmethod def migrate_context(cls, context, version): if not version: settings.rename_setting(context, "attrX", "attr_x") settings.rename_setting(context, "attrY", "attr_y") settings.migrate_str_to_variable(context) @Inputs.data def set_data(self, data): """ Discretize continuous attributes, and put all attributes and discrete metas into self.attrs. Select the first two attributes unless context overrides this. Method `resolve_shown_attributes` is called to use the attributes from the input, if it exists and matches the attributes in the data. Remove selection; again let the context override this. Initialize the vizrank dialog, but don't show it. Args: data (Table): input data """ if isinstance(data, SqlTable) and data.approx_len() > LARGE_TABLE: data = data.sample_time(DEFAULT_SAMPLE_TIME) self.closeContext() self.data = data self.areas = [] self.selection = set() if self.data is None: self.attrs[:] = [] self.domain_model.set_domain(None) self.discrete_data = None else: self.domain_model.set_domain(data.domain) self.attrs = [x for x in self.domain_model if isinstance(x, Variable)] if self.attrs: self.attr_x = self.attrs[0] self.attr_y = self.attrs[len(self.attrs) > 1] else: self.attr_x = self.attr_y = None self.areas = [] self.selection = set() self.openContext(self.data) if self.data: self.discrete_data = self.sparse_to_dense(data, True) self.resolve_shown_attributes() self.update_graph() self.update_selection() self.vizrank.initialize() self.vizrank_button.setEnabled(self.data is not None and len(self.data) > 1 and len(self.data.domain.attributes) > 1 and not self.data.is_sparse()) def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def attr_changed(self): self.update_attr() self.xy_changed_manually.emit(self.attr_x, self.attr_y) def update_attr(self): """Update the graph and selection.""" self.selection = set() self.discrete_data = self.sparse_to_dense(self.data) self.update_graph() self.update_selection() def sparse_to_dense(self, data, init=False): """ Extracts two selected columns from sparse matrix. GH-2260 """ def discretizer(data): if any(attr.is_continuous for attr in chain( data.domain.variables, data.domain.metas)): discretize = Discretize(method=EqualFreq(n=4), remove_const=False, discretize_classes=True, discretize_metas=True) return discretize(data).to_dense() return data if not data.is_sparse() and not init: return self.discrete_data if data.is_sparse(): attrs = {self.attr_x, self.attr_y} new_domain = data.domain.select_columns(attrs) data = Table.from_table(new_domain, data) return discretizer(data) @Inputs.features def set_input_features(self, attr_list): """ Handler for the Features signal. The method stores the attributes and calls `resolve_shown_attributes` Args: attr_list (AttributeList): data from the signal """ self.input_features = attr_list self.resolve_shown_attributes() self.update_selection() def resolve_shown_attributes(self): """ Use the attributes from the input signal if the signal is present and at least two attributes appear in the domain. If there are multiple, use the first two. Combos are disabled if inputs are used. """ self.warning() self.attr_box.setEnabled(True) if not self.input_features: # None or empty return features = [f for f in self.input_features if f in self.domain_model] if not features: self.warning( "Features from the input signal are not present in the data") return old_attrs = self.attr_x, self.attr_y self.attr_x, self.attr_y = [f for f in (features * 2)[:2]] self.attr_box.setEnabled(False) if (self.attr_x, self.attr_y) != old_attrs: self.selection = set() self.update_graph() def reset_selection(self): self.selection = set() self.update_selection() def select_area(self, area, event): """ Add or remove the clicked area from the selection Args: area (QRect): the area that is clicked event (QEvent): event description """ if event.button() != Qt.LeftButton: return index = self.areas.index(area) if event.modifiers() & Qt.ControlModifier: self.selection ^= {index} else: self.selection = {index} self.update_selection() def update_selection(self): """ Update the graph (pen width) to show the current selection. Filter and output the data. """ if self.areas is None or not self.selection: self.Outputs.selected_data.send(None) self.Outputs.annotated_data.send( create_annotated_table(self.data, [])) return filts = [] for i, area in enumerate(self.areas): if i in self.selection: width = 4 val_x, val_y = area.value_pair filts.append( filter.Values([ filter.FilterDiscrete(self.attr_x.name, [val_x]), filter.FilterDiscrete(self.attr_y.name, [val_y]) ])) else: width = 1 pen = area.pen() pen.setWidth(width) area.setPen(pen) if len(filts) == 1: filts = filts[0] else: filts = filter.Values(filts, conjunction=False) selection = filts(self.discrete_data) idset = set(selection.ids) sel_idx = [i for i, id in enumerate(self.data.ids) if id in idset] if self.discrete_data is not self.data: selection = self.data[sel_idx] self.Outputs.selected_data.send(selection) self.Outputs.annotated_data.send( create_annotated_table(self.data, sel_idx)) def update_graph(self): # Function uses weird names like r, g, b, but it does it with utmost # caution, hence # pylint: disable=invalid-name """Update the graph.""" def text(txt, *args, **kwargs): return CanvasText(self.canvas, "", html_text=to_html(txt), *args, **kwargs) def width(txt): return text(txt, 0, 0, show=False).boundingRect().width() def height(txt): return text(txt, 0, 0, show=False).boundingRect().height() def fmt(val): return str(int(val)) if val % 1 == 0 else "{:.2f}".format(val) def show_pearson(rect, pearson, pen_width): """ Color the given rectangle according to its corresponding standardized Pearson residual. Args: rect (QRect): the rectangle being drawn pearson (float): signed standardized pearson residual pen_width (int): pen width (bolder pen is used for selection) """ r = rect.rect() x, y, w, h = r.x(), r.y(), r.width(), r.height() if w == 0 or h == 0: return r = b = 255 if pearson > 0: r = g = max(255 - 20 * pearson, 55) elif pearson < 0: b = g = max(255 + 20 * pearson, 55) else: r = g = b = 224 rect.setBrush(QBrush(QColor(r, g, b))) pen_color = QColor(255 * (r == 255), 255 * (g == 255), 255 * (b == 255)) pen = QPen(pen_color, pen_width) rect.setPen(pen) if pearson > 0: pearson = min(pearson, 10) dist = 20 - 1.6 * pearson else: pearson = max(pearson, -10) dist = 20 - 8 * pearson pen.setWidth(1) def _offseted_line(ax, ay): r = QGraphicsLineItem(x + ax, y + ay, x + (ax or w), y + (ay or h)) self.canvas.addItem(r) r.setPen(pen) ax = dist while ax < w: _offseted_line(ax, 0) ax += dist ay = dist while ay < h: _offseted_line(0, ay) ay += dist def make_tooltip(): """Create the tooltip. The function uses local variables from the enclosing scope.""" # pylint: disable=undefined-loop-variable def _oper(attr, txt): if self.data.domain[attr.name] is ddomain[attr.name]: return "=" return " " if txt[0] in "<≥" else " in " return ( "<b>{attr_x}{xeq}{xval_name}</b>: {obs_x}/{n} ({p_x:.0f} %)". format(attr_x=to_html(attr_x.name), xeq=_oper(attr_x, xval_name), xval_name=to_html(xval_name), obs_x=fmt(chi.probs_x[x] * n), n=int(n), p_x=100 * chi.probs_x[x]) + "<br/>" + "<b>{attr_y}{yeq}{yval_name}</b>: {obs_y}/{n} ({p_y:.0f} %)". format(attr_y=to_html(attr_y.name), yeq=_oper(attr_y, yval_name), yval_name=to_html(yval_name), obs_y=fmt(chi.probs_y[y] * n), n=int(n), p_y=100 * chi.probs_y[y]) + "<hr/>" + """<b>combination of values: </b><br/> expected {exp} ({p_exp:.0f} %)<br/> observed {obs} ({p_obs:.0f} %)""".format( exp=fmt(chi.expected[y, x]), p_exp=100 * chi.expected[y, x] / n, obs=fmt(chi.observed[y, x]), p_obs=100 * chi.observed[y, x] / n)) for item in self.canvas.items(): self.canvas.removeItem(item) if self.data is None or len(self.data) == 0 or \ self.attr_x is None or self.attr_y is None: return ddomain = self.discrete_data.domain attr_x, attr_y = self.attr_x, self.attr_y disc_x, disc_y = ddomain[attr_x.name], ddomain[attr_y.name] view = self.canvasView chi = ChiSqStats(self.discrete_data, disc_x, disc_y) max_ylabel_w = max((width(val) for val in disc_y.values), default=0) max_ylabel_w = min(max_ylabel_w, 200) x_off = height(attr_y.name) + max_ylabel_w y_off = 15 square_size = min(view.width() - x_off - 35, view.height() - y_off - 80) square_size = max(square_size, 10) self.canvasView.setSceneRect(0, 0, view.width(), view.height()) if not disc_x.values or not disc_y.values: text_ = "特征 {} 和 {} 没有值".format(disc_x, disc_y) \ if not disc_x.values and \ not disc_y.values and \ disc_x != disc_y \ else \ "Feature {} has no values".format( disc_x if not disc_x.values else disc_y) text(text_, view.width() / 2 + 70, view.height() / 2, Qt.AlignRight | Qt.AlignVCenter) return n = chi.n curr_x = x_off max_xlabel_h = 0 self.areas = [] for x, (px, xval_name) in enumerate(zip(chi.probs_x, disc_x.values)): if px == 0: continue width = square_size * px curr_y = y_off for y in range(len(chi.probs_y) - 1, -1, -1): # bottom-up order py = chi.probs_y[y] yval_name = disc_y.values[y] if py == 0: continue height = square_size * py selected = len(self.areas) in self.selection rect = CanvasRectangle(self.canvas, curr_x + 2, curr_y + 2, width - 4, height - 4, z=-10, onclick=self.select_area) rect.value_pair = x, y self.areas.append(rect) show_pearson(rect, chi.residuals[y, x], 3 * selected) rect.setToolTip(make_tooltip()) if x == 0: text(yval_name, x_off, curr_y + height / 2, Qt.AlignRight | Qt.AlignVCenter) curr_y += height xl = text(xval_name, curr_x + width / 2, y_off + square_size, Qt.AlignHCenter | Qt.AlignTop) max_xlabel_h = max(int(xl.boundingRect().height()), max_xlabel_h) curr_x += width bottom = y_off + square_size + max_xlabel_h text(attr_y.name, 0, y_off + square_size / 2, Qt.AlignLeft | Qt.AlignVCenter, bold=True, vertical=True) text(attr_x.name, x_off + square_size / 2, bottom, Qt.AlignHCenter | Qt.AlignTop, bold=True) bottom += 30 xl = text("χ²={:.2f}, p={:.3f}".format(chi.chisq, chi.p), 0, bottom) # Assume similar height for both lines text("N = " + fmt(chi.n), 0, bottom - xl.boundingRect().height()) def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) return None def send_report(self): self.report_plot()
class OWScatterPlot(OWDataProjectionWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 keywords = [] class Inputs(OWDataProjectionWidget.Inputs): features = Input("Features", AttributeList) class Outputs(OWDataProjectionWidget.Outputs): features = Output("Features", AttributeList, dynamic=False) settings_version = 4 auto_sample = Setting(True) attr_x = ContextSetting(None) attr_y = ContextSetting(None) tooltip_shows_all = Setting(True) GRAPH_CLASS = OWScatterPlotGraph graph = SettingProvider(OWScatterPlotGraph) embedding_variables_names = None xy_changed_manually = Signal(Variable, Variable) class Warning(OWDataProjectionWidget.Warning): missing_coords = Msg("Plot cannot be displayed because '{}' or '{}' " "is missing for all data points") no_continuous_vars = Msg("Data has no continuous variables") class Information(OWDataProjectionWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") missing_coords = Msg( "Points with missing '{}' or '{}' are not displayed") def __init__(self): self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) super().__init__() # manually register Matplotlib file writers self.graph_writers = self.graph_writers.copy() for w in [MatplotlibFormat, MatplotlibPDFFormat]: for ext in w.EXTENSIONS: self.graph_writers[ext] = w def _add_controls(self): self._add_controls_axis() self._add_controls_sampling() super()._add_controls() self.gui.add_widgets([ self.gui.ShowGridLines, self.gui.ToolTipShowsAll, self.gui.RegressionLine ], self._plot_box) gui.checkBox( gui.indentedBox(self._plot_box), self, value="graph.orthonormal_regression", label="Treat variables as independent", callback=self.graph.update_regression_line, tooltip= "If checked, fit line to group (minimize distance from points);\n" "otherwise fit y as a function of x (minimize vertical distances)") def _add_controls_axis(self): common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str, contentsLength=14) box = gui.vBox(self.controlArea, True) dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=ContinuousVariable) self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.set_attr_from_combo, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.set_attr_from_combo, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) def _add_controls_sampling(self): self.sampling = gui.auto_commit(self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) @property def effective_variables(self): return [self.attr_x, self.attr_y] def _vizrank_color_change(self): self.vizrank.initialize() is_enabled = self.data is not None and not self.data.is_sparse() and \ len(self.xy_model) > 2 and len(self.data[self.valid_data]) > 1 \ and np.all(np.nan_to_num(np.nanstd(self.data.X, 0)) != 0) self.vizrank_button.setEnabled( is_enabled and self.attr_color is not None and not np.isnan( self.data.get_column_view( self.attr_color)[0].astype(float)).all()) text = "Color variable has to be selected." \ if is_enabled and self.attr_color is None else "" self.vizrank_button.setToolTip(text) def set_data(self, data): if self.data and data and self.data.checksum() == data.checksum(): return super().set_data(data) def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.attr_label, str): self.attr_label = findvar(self.attr_label, self.gui.label_model) if isinstance(self.attr_color, str): self.attr_color = findvar(self.attr_color, self.gui.color_model) if isinstance(self.attr_shape, str): self.attr_shape = findvar(self.attr_shape, self.gui.shape_model) if isinstance(self.attr_size, str): self.attr_size = findvar(self.attr_size, self.gui.size_model) def check_data(self): self.clear_messages() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(self.data, SqlTable): if self.data.approx_len() < 4000: self.data = Table(self.data) else: self.Information.sampled_sql() self.sql_data = self.data data_sample = self.data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) self.data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if self.data is not None: if not self.data.domain.has_continuous_attributes(True, True): self.Warning.no_continuous_vars() self.data = None if self.data is not None and (len(self.data) == 0 or len(self.data.domain) == 0): self.data = None def get_embedding(self): self.valid_data = None if self.data is None: return None x_data = self.get_column(self.attr_x, filter_valid=False) y_data = self.get_column(self.attr_y, filter_valid=False) if x_data is None or y_data is None: return None self.Warning.missing_coords.clear() self.Information.missing_coords.clear() self.valid_data = np.isfinite(x_data) & np.isfinite(y_data) if self.valid_data is not None and not np.all(self.valid_data): msg = self.Information if np.any(self.valid_data) else self.Warning msg.missing_coords(self.attr_x.name, self.attr_y.name) return np.vstack((x_data, y_data)).T # Tooltip def _point_tooltip(self, point_id, skip_attrs=()): point_data = self.data[point_id] xy_attrs = (self.attr_x, self.attr_y) text = "<br/>".join( escape('{} = {}'.format(var.name, point_data[var])) for var in xy_attrs) if self.tooltip_shows_all: others = super()._point_tooltip(point_id, skip_attrs=xy_attrs) if others: text = "<b>{}</b><br/><br/>{}".format(text, others) return text def add_data(self, time=0.4): if self.data and len(self.data) > 2000: self.__timer.stop() return data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.handleNewSignals() def init_attr_values(self): super().init_attr_values() data = self.data domain = data.domain if data and len(data) else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None super().set_subset_data(subset_data) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): if self.attribute_selection_list and self.data is not None and \ self.data.domain is not None and \ all(attr in self.data.domain for attr in self.attribute_selection_list): self.attr_x, self.attr_y = self.attribute_selection_list[:2] self.attribute_selection_list = None super().handleNewSignals() self._vizrank_color_change() @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] self._invalidated = self._invalidated \ or self.attr_x != attributes[0] \ or self.attr_y != attributes[1] else: self.attribute_selection_list = None def set_attr(self, attr_x, attr_y): if attr_x != self.attr_x or attr_y != self.attr_y: self.attr_x, self.attr_y = attr_x, attr_y self.attr_changed() def set_attr_from_combo(self): self.attr_changed() self.xy_changed_manually.emit(self.attr_x, self.attr_y) def attr_changed(self): self.setup_plot() self.commit() def get_axes(self): return {"bottom": self.attr_x, "left": self.attr_y} def colors_changed(self): super().colors_changed() self._vizrank_color_change() def commit(self): super().commit() self.send_features() def send_features(self): features = [attr for attr in [self.attr_x, self.attr_y] if attr] self.Outputs.features.send(features or None) def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) return None @classmethod def migrate_settings(cls, settings, version): if version < 2 and "selection" in settings and settings["selection"]: settings["selection_group"] = [(a, 1) for a in settings["selection"]] if version < 3: if "auto_send_selection" in settings: settings["auto_commit"] = settings["auto_send_selection"] if "selection_group" in settings: settings["selection"] = settings["selection_group"] @classmethod def migrate_context(cls, context, version): values = context.values if version < 3: values["attr_color"] = values["graph"]["attr_color"] values["attr_size"] = values["graph"]["attr_size"] values["attr_shape"] = values["graph"]["attr_shape"] values["attr_label"] = values["graph"]["attr_label"] if version < 4: if values["attr_x"][1] % 100 == 1 or values["attr_y"][1] % 100 == 1: raise IncompatibleContext()
class LinePlotViewBox(ViewBox): selection_changed = Signal(np.ndarray) def __init__(self): super().__init__(enableMenu=False) self._profile_items = None self._can_select = True self._graph_state = SELECT self.setMouseMode(self.PanMode) pen = mkPen(LinePlotStyle.SELECTION_LINE_COLOR, width=LinePlotStyle.SELECTION_LINE_WIDTH) self.selection_line = QGraphicsLineItem() self.selection_line.setPen(pen) self.selection_line.setZValue(1e9) self.addItem(self.selection_line, ignoreBounds=True) def update_selection_line(self, button_down_pos, current_pos): p1 = self.childGroup.mapFromParent(button_down_pos) p2 = self.childGroup.mapFromParent(current_pos) self.selection_line.setLine(QLineF(p1, p2)) self.selection_line.resetTransform() self.selection_line.show() def set_graph_state(self, state): self._graph_state = state def enable_selection(self, enable): self._can_select = enable def get_selected(self, p1, p2): if self._profile_items is None: return np.array(False) return line_intersects_profiles(np.array([p1.x(), p1.y()]), np.array([p2.x(), p2.y()]), self._profile_items) def add_profiles(self, y): if sp.issparse(y): y = y.todense() self._profile_items = np.array([ np.vstack((np.full((1, y.shape[0]), i + 1), y[:, i].flatten())).T for i in range(y.shape[1]) ]) def remove_profiles(self): self._profile_items = None def mouseDragEvent(self, event, axis=None): if self._graph_state == SELECT and axis is None and self._can_select: event.accept() if event.button() == Qt.LeftButton: self.update_selection_line(event.buttonDownPos(), event.pos()) if event.isFinish(): self.selection_line.hide() p1 = self.childGroup.mapFromParent( event.buttonDownPos(event.button())) p2 = self.childGroup.mapFromParent(event.pos()) self.selection_changed.emit(self.get_selected(p1, p2)) elif self._graph_state == ZOOMING or self._graph_state == PANNING: event.ignore() super().mouseDragEvent(event, axis=axis) else: event.ignore() def mouseClickEvent(self, event): if event.button() == Qt.RightButton: self.autoRange() self.enableAutoRange() else: event.accept() self.selection_changed.emit(np.array(False)) def reset(self): self._profile_items = None self._can_select = True self._graph_state = SELECT
class CorrelationRank(VizRankDialogAttrPair): """ Correlations rank widget. """ threadStopped = Signal() PValRole = next(gui.OrangeUserRole) def __init__(self, *args): super().__init__(*args) self.heuristic = None self.use_heuristic = False self.sel_feature_index = None def initialize(self): super().initialize() data = self.master.cont_data self.attrs = data and data.domain.attributes self.model_proxy.setFilterKeyColumn(-1) self.heuristic = None self.use_heuristic = False if self.master.feature is not None: self.sel_feature_index = data.domain.index(self.master.feature) else: self.sel_feature_index = None if data: # use heuristic if data is too big self.use_heuristic = len(data) * len(self.attrs) ** 2 > SIZE_LIMIT \ and self.sel_feature_index is None if self.use_heuristic: self.heuristic = KMeansCorrelationHeuristic(data) def compute_score(self, state): (attr1, attr2), corr_type = state, self.master.correlation_type data = self.master.cont_data.X corr = pearsonr if corr_type == CorrelationType.PEARSON else spearmanr r, p_value = corr(data[:, attr1], data[:, attr2]) return -abs(r) if not np.isnan(r) else NAN, r, p_value def row_for_state(self, score, state): attrs = sorted((self.attrs[x] for x in state), key=attrgetter("name")) attr_items = [] for attr in attrs: item = QStandardItem(attr.name) item.setData(attrs, self._AttrRole) item.setData(Qt.AlignLeft + Qt.AlignTop, Qt.TextAlignmentRole) item.setToolTip(attr.name) attr_items.append(item) correlation_item = QStandardItem("{:+.3f}".format(score[1])) correlation_item.setData(score[2], self.PValRole) correlation_item.setData(attrs, self._AttrRole) correlation_item.setData( self.NEGATIVE_COLOR if score[1] < 0 else self.POSITIVE_COLOR, gui.TableBarItem.BarColorRole) return [correlation_item] + attr_items def check_preconditions(self): return self.master.cont_data is not None def iterate_states(self, initial_state): if self.sel_feature_index is not None: return self.iterate_states_by_feature() elif self.use_heuristic: return self.heuristic.get_states(initial_state) else: return super().iterate_states(initial_state) def iterate_states_by_feature(self): for j in range(len(self.attrs)): if j != self.sel_feature_index: yield self.sel_feature_index, j def state_count(self): n = len(self.attrs) return n * (n - 1) / 2 if self.sel_feature_index is None else n - 1 @staticmethod def bar_length(score): return abs(score[1]) def stopped(self): self.threadStopped.emit() header = self.rank_table.horizontalHeader() header.setSectionResizeMode(1, QHeaderView.Stretch) def start(self, task, *args, **kwargs): self._set_empty_status() super().start(task, *args, **kwargs) self.__set_state_busy() def cancel(self): super().cancel() self.__set_state_ready() def _connect_signals(self, state): super()._connect_signals(state) state.progress_changed.connect(self.master.progressBarSet) state.status_changed.connect(self.master.setStatusMessage) def _disconnect_signals(self, state): super()._disconnect_signals(state) state.progress_changed.disconnect(self.master.progressBarSet) state.status_changed.disconnect(self.master.setStatusMessage) def _on_task_done(self, future): super()._on_task_done(future) self.__set_state_ready() def __set_state_ready(self): self._set_empty_status() self.master.setBlocking(False) def __set_state_busy(self): self.master.progressBarInit() self.master.setBlocking(True) def _set_empty_status(self): self.master.progressBarFinished() self.master.setStatusMessage("")
class OWLinePlot(OWWidget): name = "Line Plot" description = "Visualization of data profiles (e.g., time series)." icon = "icons/LinePlot.svg" priority = 180 enable_selection = Signal(bool) class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) settingsHandler = DomainContextHandler() group_var = ContextSetting(None) show_profiles = Setting(False) show_range = Setting(True) show_mean = Setting(True) show_error = Setting(False) auto_commit = Setting(True) selection = Setting(None, schema_only=True) graph_name = "graph.plotItem" class Error(OWWidget.Error): not_enough_attrs = Msg("Need at least one continuous feature.") no_valid_data = Msg("No plot due to no valid data.") class Warning(OWWidget.Warning): no_display_option = Msg("No display option is selected.") class Information(OWWidget.Information): hidden_instances = Msg("Instances with unknown values are not shown.") too_many_features = Msg("Data has too many features. Only first {}" " are shown.".format(MAX_FEATURES)) def __init__(self, parent=None): super().__init__(parent) self.__groups = [] self.data = None self.valid_data = None self.subset_data = None self.subset_indices = None self.__pending_selection = self.selection self.graph_variables = [] self.setup_gui() self.graph.view_box.selection_changed.connect(self.selection_changed) self.enable_selection.connect(self.graph.view_box.enable_selection) def setup_gui(self): self._add_graph() self._add_controls() def _add_graph(self): box = gui.vBox(self.mainArea, True, margin=0) self.graph = LinePlotGraph(self) box.layout().addWidget(self.graph) def _add_controls(self): infobox = gui.widgetBox(self.controlArea, "Info") self.infoLabel = gui.widgetLabel(infobox, "No data on input.") displaybox = gui.widgetBox(self.controlArea, "Display") gui.checkBox(displaybox, self, "show_profiles", "Lines", callback=self.__show_profiles_changed, tooltip="Plot lines") gui.checkBox(displaybox, self, "show_range", "Range", callback=self.__show_range_changed, tooltip="Plot range between 10th and 90th percentile") gui.checkBox(displaybox, self, "show_mean", "Mean", callback=self.__show_mean_changed, tooltip="Plot mean curve") gui.checkBox(displaybox, self, "show_error", "Error bars", callback=self.__show_error_changed, tooltip="Show standard deviation") self.group_vars = DomainModel(placeholder="None", separators=False, valid_types=DiscreteVariable) self.group_view = gui.listView(self.controlArea, self, "group_var", box="Group by", model=self.group_vars, callback=self.__group_var_changed) self.group_view.setEnabled(False) self.group_view.setMinimumSize(QSize(30, 100)) self.group_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) plot_gui = OWPlotGUI(self) plot_gui.box_zoom_select(self.controlArea) gui.rubber(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") def __show_profiles_changed(self): self.check_display_options() self._update_visibility("profiles") def __show_range_changed(self): self.check_display_options() self._update_visibility("range") def __show_mean_changed(self): self.check_display_options() self._update_visibility("mean") def __show_error_changed(self): self._update_visibility("error") def __group_var_changed(self): if self.data is None or not self.graph_variables: return self.plot_groups() self._update_profiles_color() self._update_sel_profiles_and_range() self._update_sel_profiles_color() self._update_sub_profiles() @Inputs.data @check_sql_input def set_data(self, data): self.closeContext() self.data = data self.clear() self.check_data() self.check_display_options() if self.data is not None: self.group_vars.set_domain(self.data.domain) self.group_view.setEnabled(len(self.group_vars) > 1) self.group_var = self.data.domain.class_var \ if self.data.domain.has_discrete_class else None self.openContext(data) self.setup_plot() self.commit() def check_data(self): def error(err): err() self.data = None self.clear_messages() if self.data is not None: self.infoLabel.setText( "%i instances on input\n%i features" % (len(self.data), len(self.data.domain.attributes))) self.graph_variables = [ var for var in self.data.domain.attributes if var.is_continuous ] self.valid_data = ~countnans(self.data.X, axis=1).astype(bool) if len(self.graph_variables) < 1: error(self.Error.not_enough_attrs) elif not np.sum(self.valid_data): error(self.Error.no_valid_data) else: if not np.all(self.valid_data): self.Information.hidden_instances() if len(self.graph_variables) > MAX_FEATURES: self.Information.too_many_features() self.graph_variables = self.graph_variables[:MAX_FEATURES] def check_display_options(self): self.Warning.no_display_option.clear() if self.data is not None: if not (self.show_profiles or self.show_range or self.show_mean): self.Warning.no_display_option() enable = (self.show_profiles or self.show_range) and \ len(self.data[self.valid_data]) < SEL_MAX_INSTANCES self.enable_selection.emit(enable) @Inputs.data_subset @check_sql_input def set_subset_data(self, subset): self.subset_data = subset def handleNewSignals(self): self.set_subset_ids() if self.data is not None: self._update_profiles_color() self._update_sel_profiles_color() self._update_sub_profiles() def set_subset_ids(self): sub_ids = {e.id for e in self.subset_data} \ if self.subset_data is not None else {} self.subset_indices = None if self.data is not None and sub_ids: self.subset_indices = [ x.id for x in self.data[self.valid_data] if x.id in sub_ids ] def setup_plot(self): if self.data is None: return ticks = [a.name for a in self.graph_variables] self.graph.getAxis("bottom").set_ticks(ticks) self.plot_groups() self.apply_selection() self.graph.view_box.enableAutoRange() self.graph.view_box.updateAutoRange() def plot_groups(self): self._remove_groups() data = self.data[self.valid_data, self.graph_variables] if self.group_var is None: self._plot_group(data, np.where(self.valid_data)[0]) else: class_col_data, _ = self.data.get_column_view(self.group_var) for index in range(len(self.group_var.values)): mask = np.logical_and(class_col_data == index, self.valid_data) indices = np.flatnonzero(mask) if not len(indices): continue group_data = self.data[indices, self.graph_variables] self._plot_group(group_data, indices, index) self.graph.update_legend(self.group_var) self.graph.view_box.add_profiles(data.X) def _remove_groups(self): for group in self.__groups: group.remove_items() self.graph.view_box.remove_profiles() self.__groups = [] def _plot_group(self, data, indices, index=None): color = self.__get_group_color(index) group = ProfileGroup(data, indices, color, self.graph) kwargs = self.__get_visibility_flags() group.set_visible_error(**kwargs) group.set_visible_mean(**kwargs) group.set_visible_range(**kwargs) group.set_visible_profiles(**kwargs) self.__groups.append(group) def __get_group_color(self, index): if self.group_var is not None: return QColor(*self.group_var.colors[index]) return QColor(LinePlotStyle.DEFAULT_COLOR) def __get_visibility_flags(self): return { "show_profiles": self.show_profiles, "show_range": self.show_range, "show_mean": self.show_mean, "show_error": self.show_error } def _update_profiles_color(self): # color alpha depends on subset and selection; with selection or # subset profiles color has more opacity if not self.show_profiles: return for group in self.__groups: has_sel = bool(self.subset_indices) or bool(self.selection) group.update_profiles_color(has_sel) def _update_sel_profiles_and_range(self): # mark selected instances and selected range if not (self.show_profiles or self.show_range): return for group in self.__groups: inds = [i for i in group.indices if self.__in(i, self.selection)] table = self.data[inds, self.graph_variables].X if inds else None if self.show_profiles: group.update_sel_profiles(table) if self.show_range: group.update_sel_range(table) def _update_sel_profiles_color(self): # color depends on subset; when subset is present, # selected profiles are black if not self.selection or not self.show_profiles: return for group in self.__groups: group.update_sel_profiles_color(bool(self.subset_indices)) def _update_sub_profiles(self): # mark subset instances if not (self.show_profiles or self.show_range): return for group in self.__groups: inds = [ i for i, _id in zip(group.indices, group.ids) if self.__in(_id, self.subset_indices) ] table = self.data[inds, self.graph_variables].X if inds else None group.update_sub_profiles(table) def _update_visibility(self, obj_name): if not len(self.__groups): return self._update_profiles_color() self._update_sel_profiles_and_range() self._update_sel_profiles_color() kwargs = self.__get_visibility_flags() for group in self.__groups: getattr(group, "set_visible_{}".format(obj_name))(**kwargs) self.graph.view_box.updateAutoRange() def apply_selection(self): if self.data is not None and self.__pending_selection is not None: sel = [i for i in self.__pending_selection if i < len(self.data)] mask = np.zeros(len(self.data), dtype=bool) mask[sel] = True mask = mask[self.valid_data] self.selection_changed(mask) self.__pending_selection = None def selection_changed(self, mask): if self.data is None: return # need indices for self.data: mask refers to self.data[self.valid_data] indices = np.arange(len(self.data))[self.valid_data][mask] self.graph.select(indices) old = self.selection self.selection = None if self.data and isinstance(self.data, SqlTable)\ else list(self.graph.selection) if not old and self.selection or old and not self.selection: self._update_profiles_color() self._update_sel_profiles_and_range() self._update_sel_profiles_color() self.commit() def commit(self): selected = self.data[self.selection] \ if self.data is not None and bool(self.selection) else None annotated = create_annotated_table(self.data, self.selection) self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated) def send_report(self): if self.data is None: return caption = report.render_items_vert((("Group by", self.group_var), )) self.report_plot() if caption: self.report_caption(caption) def sizeHint(self): return QSize(1132, 708) def clear(self): self.valid_data = None self.selection = None self.__groups = [] self.graph_variables = [] self.graph.reset() self.infoLabel.setText("No data on input.") self.group_vars.set_domain(None) self.group_view.setEnabled(False) @staticmethod def __in(obj, collection): return collection is not None and obj in collection
class Settings(QObject, MutableMapping, metaclass=QABCMeta): """ A `dict` like interface to a QSettings store. """ valueChanged = Signal(str, object) valueAdded = Signal(str, object) keyRemoved = Signal(str) def __init__(self, parent=None, defaults=(), path=None, store=None): QObject.__init__(self, parent) if store is None: store = QSettings() path = path = (path or "").rstrip("/") self.__path = path self.__defaults = dict([(slot.key, slot) for slot in defaults]) self.__store = store def __key(self, key): """ Return the full key (including group path). """ if self.__path: return "/".join([self.__path, key]) else: return key def __delitem__(self, key): """ Delete the setting for key. If key is a group remove the whole group. .. note:: defaults cannot be deleted they are instead reverted to their original state. """ if key not in self: raise KeyError(key) if self.isgroup(key): group = self.group(key) for key in group: del group[key] else: fullkey = self.__key(key) oldValue = self.get(key) if self.__store.contains(fullkey): self.__store.remove(fullkey) newValue = None if fullkey in self.__defaults: newValue = self.__defaults[fullkey].default_value etype = SettingChangedEvent.SettingChanged else: etype = SettingChangedEvent.SettingRemoved QCoreApplication.sendEvent( self, SettingChangedEvent(etype, key, newValue, oldValue) ) def __value(self, fullkey, value_type): typesafe = value_type is not None if value_type is None: value = self.__store.value(fullkey) else: try: value = self.__store.value(fullkey, type=value_type) except TypeError: # In case the value was pickled in a type unsafe mode value = self.__store.value(fullkey) typesafe = False if not typesafe: if isinstance(value, _pickledvalue): value = value.value else: log.warning( "value for key %r is not a '_pickledvalue' (%r)," "possible loss of type information.", fullkey, type(value), ) return value def __setValue(self, fullkey, value, value_type=None): typesafe = value_type is not None if not typesafe: # value is stored in a _pickledvalue wrapper to force PyQt # to store it in a pickled format so we don't lose the type # TODO: Could check if QSettings.Format stores type info. value = _pickledvalue(value) self.__store.setValue(fullkey, value) def __getitem__(self, key): """ Get the setting for key. """ if key not in self: raise KeyError(key) if self.isgroup(key): raise KeyError("{0!r} is a group".format(key)) fullkey = self.__key(key) slot = self.__defaults.get(fullkey, None) if self.__store.contains(fullkey): value = self.__value(fullkey, slot.value_type if slot else None) else: value = slot.default_value return value def __setitem__(self, key, value): """ Set the setting for key. """ if not isinstance(key, str): raise TypeError(key) fullkey = self.__key(key) value_type = None if fullkey in self.__defaults: value_type = self.__defaults[fullkey].value_type if not isinstance(value, value_type): value = qt_to_mapped_type(value) if not isinstance(value, value_type): raise TypeError( "Expected {0!r} got {1!r}".format( value_type.__name__, type(value).__name__ ) ) if key in self: oldValue = self.get(key) etype = SettingChangedEvent.SettingChanged else: oldValue = None etype = SettingChangedEvent.SettingAdded self.__setValue(fullkey, value, value_type) QCoreApplication.sendEvent( self, SettingChangedEvent(etype, key, value, oldValue) ) def __contains__(self, key): """ Return `True` if settings contain the `key`, False otherwise. """ fullkey = self.__key(key) return self.__store.contains(fullkey) or (fullkey in self.__defaults) def __iter__(self): """Return an iterator over over all keys. """ keys = list(map(str, self.__store.allKeys())) + list(self.__defaults.keys()) if self.__path: path = self.__path + "/" keys = [key for key in keys if key.startswith(path)] keys = [key[len(path) :] for key in keys] return iter(sorted(set(keys))) def __len__(self): return len(list(iter(self))) def group(self, path): if self.__path: path = "/".join([self.__path, path]) return Settings(self, list(self.__defaults.values()), path, self.__store) def isgroup(self, key): """ Is the `key` a settings group i.e. does it have subkeys. """ if key not in self: raise KeyError("{0!r} is not a valid key".format(key)) return len(self.group(key)) > 0 def isdefault(self, key): """ Is the value for key the default. """ if key not in self: raise KeyError(key) return not self.__store.contains(self.__key(key)) def clear(self): """ Clear the settings and restore the defaults. """ self.__store.clear() def add_default_slot(self, default): """ Add a default slot to the settings This also replaces any previously set value for the key. """ value = default.default_value oldValue = None etype = SettingChangedEvent.SettingAdded key = default.key if key in self: oldValue = self.get(key) etype = SettingChangedEvent.SettingChanged if not self.isdefault(key): # Replacing a default value. self.__store.remove(self.__key(key)) self.__defaults[key] = default event = SettingChangedEvent(etype, key, value, oldValue) QCoreApplication.sendEvent(self, event) def get_default_slot(self, key): return self.__defaults[self.__key(key)] def values(self): """ Return a list over of all values in the settings. """ return MutableMapping.values(self) def customEvent(self, event): QObject.customEvent(self, event) if isinstance(event, SettingChangedEvent): if event.type() == SettingChangedEvent.SettingChanged: self.valueChanged.emit(event.key(), event.value()) elif event.type() == SettingChangedEvent.SettingAdded: self.valueAdded.emit(event.key(), event.value()) elif event.type() == SettingChangedEvent.SettingRemoved: self.keyRemoved.emit(event.key()) parent = self.parent() if isinstance(parent, Settings): # Assumption that the parent is a parent setting group. parent.customEvent( SettingChangedEvent( event.type(), "/".join([self.__path, event.key()]), event.value(), event.oldValue(), ) )
class UnivariateFeatureSelect(QWidget): changed = Signal() edited = Signal() #: Strategy Fixed, Proportion, FDR, FPR, FWE = 1, 2, 3, 4, 5 def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.setLayout(QVBoxLayout()) self.__scoreidx = 0 self.__strategy = UnivariateFeatureSelect.Fixed self.__k = 10 self.__p = 75.0 box = QGroupBox(title="Score", flat=True) box.setLayout(QVBoxLayout()) self.__cb = cb = QComboBox(self, ) self.__cb.currentIndexChanged.connect(self.setScoreIndex) self.__cb.activated.connect(self.edited) box.layout().addWidget(cb) self.layout().addWidget(box) box = QGroupBox(title="Number of features", flat=True) self.__group = group = QButtonGroup(self, exclusive=True) self.__spins = {} form = QFormLayout() fixedrb = QRadioButton("Fixed:", checked=True) group.addButton(fixedrb, UnivariateFeatureSelect.Fixed) kspin = QSpinBox( minimum=1, maximum=1000000, value=self.__k, enabled=self.__strategy == UnivariateFeatureSelect.Fixed) kspin.valueChanged[int].connect(self.setK) kspin.editingFinished.connect(self.edited) self.__spins[UnivariateFeatureSelect.Fixed] = kspin form.addRow(fixedrb, kspin) percrb = QRadioButton("Proportion:") group.addButton(percrb, UnivariateFeatureSelect.Proportion) pspin = QDoubleSpinBox( minimum=1.0, maximum=100.0, singleStep=0.5, value=self.__p, suffix="%", enabled=self.__strategy == UnivariateFeatureSelect.Proportion) pspin.valueChanged[float].connect(self.setP) pspin.editingFinished.connect(self.edited) self.__spins[UnivariateFeatureSelect.Proportion] = pspin form.addRow(percrb, pspin) # form.addRow(QRadioButton("FDR"), QDoubleSpinBox()) # form.addRow(QRadioButton("FPR"), QDoubleSpinBox()) # form.addRow(QRadioButton("FWE"), QDoubleSpinBox()) self.__group.buttonClicked.connect(self.__on_buttonClicked) box.setLayout(form) self.layout().addWidget(box) def setScoreIndex(self, scoreindex): if self.__scoreidx != scoreindex: self.__scoreidx = scoreindex self.__cb.setCurrentIndex(scoreindex) self.changed.emit() def scoreIndex(self): return self.__scoreidx def setStrategy(self, strategy): if self.__strategy != strategy: self.__strategy = strategy b = self.__group.button(strategy) b.setChecked(True) for st, rb in self.__spins.items(): rb.setEnabled(st == strategy) self.changed.emit() def setK(self, k): if self.__k != k: self.__k = k spin = self.__spins[UnivariateFeatureSelect.Fixed] spin.setValue(k) if self.__strategy == UnivariateFeatureSelect.Fixed: self.changed.emit() def setP(self, p): if self.__p != p: self.__p = p spin = self.__spins[UnivariateFeatureSelect.Proportion] spin.setValue(p) if self.__strategy == UnivariateFeatureSelect.Proportion: self.changed.emit() def setItems(self, itemlist): for item in itemlist: self.__cb.addItem(item["text"]) def __on_buttonClicked(self): strategy = self.__group.checkedId() self.setStrategy(strategy) self.edited.emit() def setParameters(self, params): score = params.get("score", 0) strategy = params.get("strategy", UnivariateFeatureSelect.Fixed) self.setScoreIndex(score) self.setStrategy(strategy) if strategy == UnivariateFeatureSelect.Fixed: self.setK(params.get("k", 10)) else: self.setP(params.get("p", 75)) def parameters(self): score = self.__scoreidx strategy = self.__strategy p = self.__p k = self.__k return {"score": score, "strategy": strategy, "p": p, "k": k}
class MessagesWidget(QWidget): """ An iconified multiple message display area. `MessagesWidget` displays a short message along with an icon. If there are multiple messages they are summarized. The user can click on the widget to display the full message text in a popup view. """ #: Signal emitted when an embedded html link is clicked #: (if `openExternalLinks` is `False`). linkActivated = Signal(str) #: Signal emitted when an embedded html link is hovered. linkHovered = Signal(str) Severity = Severity #: General informative message. Information = Severity.Information #: A warning message severity. Warning = Severity.Warning #: An error message severity. Error = Severity.Error Message = Message def __init__(self, parent=None, openExternalLinks=False, **kwargs): kwargs.setdefault( "sizePolicy", QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) super().__init__(parent, **kwargs) self.__openExternalLinks = openExternalLinks # type: bool self.__messages = OrderedDict() # type: Dict[Hashable, Message] #: The full (joined all messages text - rendered as html), displayed #: in a tooltip. self.__fulltext = "" #: The full text displayed in a popup. Is empty if the message is #: short self.__popuptext = "" #: Leading icon self.__iconwidget = IconWidget( sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) #: Inline message text self.__textlabel = QLabel( wordWrap=False, textInteractionFlags=Qt.LinksAccessibleByMouse, openExternalLinks=self.__openExternalLinks, sizePolicy=QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)) #: Indicator that extended contents are accessible with a click on the #: widget. self.__popupicon = QLabel( sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum), text="\N{VERTICAL ELLIPSIS}", visible=False, ) self.__textlabel.linkActivated.connect(self.linkActivated) self.__textlabel.linkHovered.connect(self.linkHovered) self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(2, 1, 2, 1) self.layout().setSpacing(0) self.layout().addWidget(self.__iconwidget) self.layout().addSpacing(4) self.layout().addWidget(self.__textlabel) self.layout().addWidget(self.__popupicon) self.__textlabel.setAttribute(Qt.WA_MacSmallSize) def sizeHint(self): sh = super().sizeHint() h = self.style().pixelMetric(QStyle.PM_SmallIconSize) return sh.expandedTo(QSize(0, h + 2)) def openExternalLinks(self): # type: () -> bool """ If True then linkActivated signal will be emitted when the user clicks on an html link in a message, otherwise links are opened using `QDesktopServices.openUrl` """ return self.__openExternalLinks def setOpenExternalLinks(self, state): # type: (bool) -> None """ """ # TODO: update popup if open self.__openExternalLinks = state self.__textlabel.setOpenExternalLinks(state) def setMessage(self, message_id, message): # type: (Hashable, Message) -> None """ Add a `message` for `message_id` to the current display. Note ---- Set an empty `Message` instance to clear the message display but retain the relative ordering in the display should a message for `message_id` reactivate. """ self.__messages[message_id] = message self.__update() def removeMessage(self, message_id): # type: (Hashable) -> None """ Remove message for `message_id` from the display. Note ---- Setting an empty `Message` instance will also clear the display, however the relative ordering of the messages will be retained, should the `message_id` 'reactivate'. """ del self.__messages[message_id] self.__update() def setMessages(self, messages): # type: (Union[Iterable[Tuple[Hashable, Message]], Dict[Hashable, Message]]) -> None """ Set multiple messages in a single call. """ messages = OrderedDict(messages) self.__messages.update(messages) self.__update() def clear(self): # type: () -> None """ Clear all messages. """ self.__messages.clear() self.__update() def messages(self): # type: () -> List[Message] return list(self.__messages.values()) def summarize(self): # type: () -> Message """ Summarize all the messages into a single message. """ messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: return summarize(messages) else: return Message() def __update(self): """ Update the current display state. """ self.ensurePolished() summary = self.summarize() icon = message_icon(summary) self.__iconwidget.setIcon(icon) self.__iconwidget.setVisible(not (summary.isEmpty() or icon.isNull())) self.__textlabel.setTextFormat(summary.textFormat) self.__textlabel.setText(summary.text) messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: messages = sorted(messages, key=attrgetter("severity"), reverse=True) fulltext = "<hr/>".join(m.asHtml() for m in messages) else: fulltext = "" self.__fulltext = fulltext self.setToolTip(fulltext) def is_short(m): return not (m.informativeText or m.detailedText) if not messages or len(messages) == 1 and is_short(messages[0]): self.__popuptext = "" else: self.__popuptext = fulltext self.__popupicon.setVisible(bool(self.__popuptext)) self.layout().activate() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: if self.__popuptext: popup = QMenu(self) label = QLabel(self, textInteractionFlags=Qt.TextBrowserInteraction, openExternalLinks=self.__openExternalLinks, text=self.__popuptext) label.linkActivated.connect(self.linkActivated) label.linkHovered.connect(self.linkHovered) action = QWidgetAction(popup) action.setDefaultWidget(label) popup.addAction(action) popup.popup(event.globalPos(), action) event.accept() return else: super().mousePressEvent(event) def enterEvent(self, event): super().enterEvent(event) self.update() def leaveEvent(self, event): super().leaveEvent(event) self.update() def changeEvent(self, event): super().changeEvent(event) self.update() def paintEvent(self, event): opt = QStyleOption() opt.initFrom(self) if not self.__popupicon.isVisible(): return if not (opt.state & QStyle.State_MouseOver or opt.state & QStyle.State_HasFocus): return palette = opt.palette # type: QPalette if opt.state & QStyle.State_HasFocus: pen = QPen(palette.color(QPalette.Highlight)) else: pen = QPen(palette.color(QPalette.Dark)) if self.__fulltext and \ opt.state & QStyle.State_MouseOver and \ opt.state & QStyle.State_Active: g = QLinearGradient() g.setCoordinateMode(QLinearGradient.ObjectBoundingMode) base = palette.color(QPalette.Window) base.setAlpha(90) g.setColorAt(0, base.lighter(200)) g.setColorAt(0.6, base) g.setColorAt(1.0, base.lighter(200)) brush = QBrush(g) else: brush = QBrush(Qt.NoBrush) p = QPainter(self) p.setBrush(brush) p.setPen(pen) p.drawRect(opt.rect.adjusted(0, 0, -1, -1))
class Installer(QObject): installStatusChanged = Signal(str) started = Signal() finished = Signal() error = Signal(str, object, int, list) def __init__(self, parent=None, steps=[], user_install=False): QObject.__init__(self, parent) self.__interupt = False self.__queue = deque(steps) self.__user_install = user_install def start(self): QTimer.singleShot(0, self._next) def interupt(self): self.__interupt = True def setStatusMessage(self, message): self.__statusMessage = message self.installStatusChanged.emit(message) @Slot() def _next(self): def fmt_cmd(cmd): return "Command failed: python " + " ".join(map(shlex.quote, cmd)) command, pkg = self.__queue.popleft() if command == Install: inst = pkg.installable inst_name = inst.name if inst.package_url.startswith( "http://") else inst.package_url self.setStatusMessage("Installing {}".format(inst.name)) cmd = (["-m", "pip", "install"] + (["--user"] if self.__user_install else []) + [inst_name]) process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return elif command == Upgrade: inst = pkg.installable inst_name = inst.name if inst.package_url.startswith( "http://") else inst.package_url self.setStatusMessage("Upgrading {}".format(inst.name)) cmd = (["-m", "pip", "install", "--upgrade", "--no-deps"] + (["--user"] if self.__user_install else []) + [inst_name]) process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return # Why is this here twice?? cmd = (["-m", "pip", "install"] + (["--user"] if self.__user_install else []) + [inst_name]) process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return elif command == Uninstall: dist = pkg.local self.setStatusMessage("Uninstalling {}".format(dist.project_name)) cmd = ["-m", "pip", "uninstall", "--yes", dist.project_name] process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if self.__user_install: # Remove the package forcefully; pip doesn't (yet) uninstall # --user packages (or any package outside sys.prefix?) # google: pip "Not uninstalling ?" "outside environment" install_path = os.path.join( USER_SITE, re.sub('[^\w]', '_', dist.project_name)) pip_record = next(iglob(install_path + '*.dist-info/RECORD'), None) if pip_record: with open(pip_record) as f: files = [line.rsplit(',', 2)[0] for line in f] else: files = [ os.path.join(USER_SITE, 'orangecontrib', dist.project_name.split('-')[-1].lower()), ] for match in itertools.chain(files, iglob(install_path + '*')): print('rm -rf', match) if os.path.isdir(match): shutil.rmtree(match) elif os.path.exists(match): os.unlink(match) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return if self.__queue: QTimer.singleShot(0, self._next) else: self.finished.emit() def __subprocessrun(self, process): output = [] while process.poll() is None: try: line = process.stdout.readline() except IOError as ex: if ex.errno != errno.EINTR: raise else: output.append(line) print(line, end="") # Read remaining output if any line = process.stdout.read() if line: output.append(line) print(line, end="") return process.returncode, output
class ToolTree(QWidget): """ A ListView like presentation of a list of actions. """ triggered = Signal(QAction) hovered = Signal(QAction) def __init__(self, parent=None, **kwargs): # type: (Optional[QWidget], Any) -> None super().__init__(parent, **kwargs) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding) self.__model = QStandardItemModel() # type: QAbstractItemModel self.__flattened = False self.__actionRole = Qt.UserRole layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) view = QTreeView(objectName="tool-tree-view") # view.setUniformRowHeights(True) # causes several seconds delay upon first menu open view.setFrameStyle(QTreeView.NoFrame) view.setModel(self.__model) view.setRootIsDecorated(False) view.setHeaderHidden(True) view.setItemsExpandable(True) view.setEditTriggers(QTreeView.NoEditTriggers) view.activated.connect(self.__onActivated) view.clicked.connect(self.__onActivated) view.entered.connect(self.__onEntered) view.installEventFilter(self) self.__view = view # type: QTreeView layout.addWidget(view) self.setLayout(layout) def setFlattened(self, flatten): # type: (bool) -> None """ Show the actions in a flattened view. """ if self.__flattened != flatten: self.__flattened = flatten if flatten: model = FlattenedTreeItemModel() model.setSourceModel(self.__model) else: model = self.__model self.__view.setModel(model) def flattened(self): # type: () -> bool """ Are actions shown in a flattened tree (a list). """ return self.__flattened def setModel(self, model): # type: (QAbstractItemModel) -> None if self.__model is not model: self.__model = model if self.__flattened: model = FlattenedTreeItemModel() model.setSourceModel(self.__model) self.__view.setModel(model) def model(self): # type: () -> QAbstractItemModel return self.__model def setRootIndex(self, index): # type: (QModelIndex) -> None """Set the root index """ self.__view.setRootIndex(index) def rootIndex(self): # type: () -> QModelIndex """Return the root index. """ return self.__view.rootIndex() def view(self): # type: () -> QTreeView """Return the QTreeView instance used. """ return self.__view def setActionRole(self, role): # type: (Qt.ItemDataRole) -> None """Set the action role. By default this is Qt.UserRole """ self.__actionRole = role def actionRole(self): # type: () -> Qt.ItemDataRole return self.__actionRole def __actionForIndex(self, index): # type: (QModelIndex) -> Optional[QAction] val = index.data(self.__actionRole) if isinstance(val, QAction): return val else: return None def __onActivated(self, index): # type: (QModelIndex) -> None """The item was activated, if index has an action we need to trigger it. """ if index.isValid(): action = self.__actionForIndex(index) if action is not None: action.trigger() self.triggered.emit(action) def __onEntered(self, index): # type: (QModelIndex) -> None if index.isValid(): action = self.__actionForIndex(index) if action is not None: action.hover() self.hovered.emit(action) def ensureCurrent(self): # type: () -> None """Ensure the view has a current item if one is available. """ model = self.__view.model() curr = self.__view.currentIndex() if not curr.isValid(): for i in range(model.rowCount()): index = model.index(i, 0) if index.flags() & Qt.ItemIsEnabled: self.__view.setCurrentIndex(index) break def eventFilter(self, obj, event): # type: (QObject, QEvent) -> bool if obj is self.__view and event.type() == QEvent.KeyPress: key = event.key() space_activates = \ self.style().styleHint( QStyle.SH_Menu_SpaceActivatesItem, None, None) if key in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Select] or \ (key == Qt.Key_Space and space_activates): index = self.__view.currentIndex() if index.isValid() and index.flags() & Qt.ItemIsEnabled: # Emit activated on behalf of QTreeView. self.__view.activated.emit(index) return True return super().eventFilter(obj, event)
class ConditionBox(QWidget): vars_changed = Signal(list) RowItems = namedtuple( "RowItems", ("pre_label", "left_combo", "in_label", "right_combo", "remove_button", "add_button")) def __init__(self, parent, model_left, model_right, pre_label, in_label): super().__init__(parent) self.model_left = model_left self.model_right = model_right self.pre_label, self.in_label = pre_label, in_label self.rows = [] self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) self.setMouseTracking(True) def add_row(self): def sync_combos(): combo = self.sender() index = combo.currentIndex() model = combo.model() other = ({row_items.left_combo, row_items.right_combo} - {combo}).pop() other_index = other.currentIndex() other_model = other.model() if 0 <= index < len(combo.model()): var = model[index] if isinstance(var, str): other.setCurrentText(model[index]) elif isinstance(other_model[other_index], str): for other_var in other_model: if isinstance(other_var, Variable) \ and var.name == other_var.name \ and (type(other_var) is type(var) or (not var.is_continuous and not other_var.is_continuous)): other.setCurrentText(var.name) break self.emit_list() def get_combo(model): combo = QComboBox(self) combo.setModel(model) # We use signal activated because it is triggered only on user # interaction, not programmatically. combo.activated.connect(sync_combos) return combo def get_button(label, callback): button = QPushButton(label, self) button.setFlat(True) button.setFixedWidth(12) button.clicked.connect(callback) return button row = self.layout().count() row_items = self.RowItems( QLabel("and" if row else self.pre_label), get_combo(self.model_left), QLabel(self.in_label), get_combo(self.model_right), get_button("×", self.on_remove_row), get_button("+", self.on_add_row) ) layout = QHBoxLayout() layout.setSpacing(10) self.layout().addLayout(layout) layout.addStretch(10) for item in row_items: layout.addWidget(item) self.rows.append(row_items) self._reset_buttons() def remove_row(self, row): self.layout().takeAt(self.rows.index(row)) self.rows.remove(row) for item in row: item.deleteLater() self._reset_buttons() def on_add_row(self, _): self.add_row() self.emit_list() def on_remove_row(self): if len(self.rows) == 1: return button = self.sender() for row in self.rows: if button is row.remove_button: self.remove_row(row) break self.emit_list() def _reset_buttons(self): def endis(button, enable, text): button.setEnabled(enable) button.setText(text if enable else "") self.rows[0].pre_label.setText(self.pre_label) single_row = len(self.rows) == 1 endis(self.rows[0].remove_button, not single_row, "×") endis(self.rows[-1].add_button, True, "+") if not single_row: endis(self.rows[-2].add_button, False, "") def current_state(self): def get_var(model, combo): return model[combo.currentIndex()] return [(get_var(self.model_left, row.left_combo), get_var(self.model_right, row.right_combo)) for row in self.rows] def set_state(self, values): while len(self.rows) > len(values): self.remove_row(self.rows[0]) while len(self.rows) < len(values): self.add_row() for (val_left, val_right), row in zip(values, self.rows): row.left_combo.setCurrentIndex(self.model_left.indexOf(val_left)) row.right_combo.setCurrentIndex(self.model_right.indexOf(val_right)) def emit_list(self): self.vars_changed.emit(self.current_state())
class FutureWatcher(QObject): """ An `QObject` watching the state changes of a `concurrent.futures.Future` Note ---- The state change notification signals (`done`, `finished`, ...) are always emitted when the control flow reaches the event loop (even if the future is already completed when set). Note ---- An event loop must be running, otherwise the notifier signals will not be emitted. Parameters ---------- parent : QObject Parent object. future : Future The future instance to watch. Example ------- >>> app = QCoreApplication.instance() or QCoreApplication([]) >>> f = submit(lambda i, j: i ** j, 10, 3) >>> watcher = FutureWatcher(f) >>> watcher.resultReady.connect(lambda res: print("Result:", res)) >>> watcher.done.connect(app.quit) >>> _ = app.exec() Result: 1000 >>> f.result() 1000 """ #: Signal emitted when the future is done (cancelled or finished) done = Signal(Future) #: Signal emitted when the future is finished (i.e. returned a result #: or raised an exception - but not if cancelled) finished = Signal(Future) #: Signal emitted when the future was cancelled cancelled = Signal(Future) #: Signal emitted with the future's result when successfully finished. resultReady = Signal(object) #: Signal emitted with the future's exception when finished with an #: exception. exceptionReady = Signal(BaseException) # A private event type used to notify the watcher of a Future's completion __FutureDone = QEvent.registerEventType() def __init__(self, future=None, parent=None, **kwargs): super().__init__(parent, **kwargs) self.__future = None if future is not None: self.setFuture(future) def setFuture(self, future): # type: (Future) -> None """ Set the future to watch. Raise a `RuntimeError` if a future is already set. Parameters ---------- future : Future """ if self.__future is not None: raise RuntimeError("Future already set") self.__future = future selfweakref = weakref.ref(self) def on_done(f): assert f is future selfref = selfweakref() if selfref is None: return try: QCoreApplication.postEvent(selfref, QEvent(FutureWatcher.__FutureDone)) except RuntimeError: # Ignore RuntimeErrors (when C++ side of QObject is deleted) # (? Use QObject.destroyed and remove the done callback ?) pass future.add_done_callback(on_done) def future(self): # type: () -> Future """ Return the future instance. """ return self.__future def isCancelled(self): warnings.warn("isCancelled is deprecated", DeprecationWarning, stacklevel=2) return self.__future.cancelled() def isDone(self): warnings.warn("isDone is deprecated", DeprecationWarning, stacklevel=2) return self.__future.done() def result(self): # type: () -> Any """ Return the future's result. Note ---- This method is non-blocking. If the future has not yet completed it will raise an error. """ try: return self.__future.result(timeout=0) except TimeoutError: raise RuntimeError("Future is not yet done") def exception(self): # type: () -> Optional[BaseException] """ Return the future's exception. Note ---- This method is non-blocking. If the future has not yet completed it will raise an error. """ try: return self.__future.exception(timeout=0) except TimeoutError: raise RuntimeError("Future is not yet done") def __emitSignals(self): assert self.__future is not None assert self.__future.done() if self.__future.cancelled(): self.cancelled.emit(self.__future) self.done.emit(self.__future) elif self.__future.done(): self.finished.emit(self.__future) self.done.emit(self.__future) if self.__future.exception(): self.exceptionReady.emit(self.__future.exception()) else: self.resultReady.emit(self.__future.result()) else: assert False def customEvent(self, event): # Reimplemented. if event.type() == FutureWatcher.__FutureDone: self.__emitSignals() super().customEvent(event)
class UserInteraction(QObject): """ Base class for user interaction handlers. Parameters ---------- document : :class:`~.SchemeEditWidget` An scheme editor instance with which the user is interacting. parent : :class:`QObject`, optional A parent QObject deleteOnEnd : bool, optional Should the UserInteraction be deleted when it finishes (``True`` by default). """ # Cancel reason flags #: No specified reason NoReason = 0 #: User canceled the operation (e.g. pressing ESC) UserCancelReason = 1 #: Another interaction was set InteractionOverrideReason = 3 #: An internal error occurred ErrorReason = 4 #: Other (unspecified) reason OtherReason = 5 #: Emitted when the interaction is set on the scene. started = Signal() #: Emitted when the interaction finishes successfully. finished = Signal() #: Emitted when the interaction ends (canceled or finished) ended = Signal() #: Emitted when the interaction is canceled. canceled = Signal([], [int]) def __init__(self, document, parent=None, deleteOnEnd=True): QObject.__init__(self, parent) self.document = document self.scene = document.scene() self.scheme = document.scheme() self.deleteOnEnd = deleteOnEnd self.cancelOnEsc = False self.__finished = False self.__canceled = False self.__cancelReason = self.NoReason def start(self): """ Start the interaction. This is called by the :class:`CanvasScene` when the interaction is installed. .. note:: Must be called from subclass implementations. """ self.started.emit() def end(self): """ Finish the interaction. Restore any leftover state in this method. .. note:: This gets called from the default :func:`cancel` implementation. """ self.__finished = True if self.scene.user_interaction_handler is self: self.scene.set_user_interaction_handler(None) if self.__canceled: self.canceled.emit() self.canceled[int].emit(self.__cancelReason) else: self.finished.emit() self.ended.emit() if self.deleteOnEnd: self.deleteLater() def cancel(self, reason=OtherReason): """ Cancel the interaction with `reason`. """ self.__canceled = True self.__cancelReason = reason self.end() def isFinished(self): """ Is the interaction finished. """ return self.__finished def isCanceled(self): """ Was the interaction canceled. """ return self.__canceled def cancelReason(self): """ Return the reason the interaction was canceled. """ return self.__cancelReason def mousePressEvent(self, event): """ Handle a `QGraphicsScene.mousePressEvent`. """ return False def mouseMoveEvent(self, event): """ Handle a `GraphicsScene.mouseMoveEvent`. """ return False def mouseReleaseEvent(self, event): """ Handle a `QGraphicsScene.mouseReleaseEvent`. """ return False def mouseDoubleClickEvent(self, event): """ Handle a `QGraphicsScene.mouseDoubleClickEvent`. """ return False def keyPressEvent(self, event): """ Handle a `QGraphicsScene.keyPressEvent` """ if self.cancelOnEsc and event.key() == Qt.Key_Escape: self.cancel(self.UserCancelReason) return False def keyReleaseEvent(self, event): """ Handle a `QGraphicsScene.keyPressEvent` """ return False def contextMenuEvent(self, event): """ Handle a `QGraphicsScene.contextMenuEvent` """ return False
class FutureSetWatcher(QObject): """ An `QObject` watching the state changes of a list of `concurrent.futures.Future` instances Note ---- The state change notification signals (`doneAt`, `finishedAt`, ...) are always emitted when the control flow reaches the event loop (even if the future is already completed when set). Note ---- An event loop must be running, otherwise the notifier signals will not be emitted. Parameters ---------- parent : QObject Parent object. futures : List[Future] A list of future instance to watch. Example ------- >>> app = QCoreApplication.instance() or QCoreApplication([]) >>> fs = [submit(lambda i, j: i ** j, 10, 3) for i in range(10)] >>> watcher = FutureSetWatcher(fs) >>> watcher.resultReadyAt.connect( ... lambda i, res: print("Result at {}: {}".format(i, res)) ... ) >>> watcher.doneAll.connect(app.quit) >>> _ = app.exec() Result at 0: 1000 ... """ #: Signal emitted when the future at `index` is done (cancelled or #: finished) doneAt = Signal([int, Future]) #: Signal emitted when the future at index is finished (i.e. returned #: a result) finishedAt = Signal([int, Future]) #: Signal emitted when the future at `index` was cancelled. cancelledAt = Signal([int, Future]) #: Signal emitted with the future's result when successfully #: finished. resultReadyAt = Signal([int, object]) #: Signal emitted with the future's exception when finished with an #: exception. exceptionReadyAt = Signal([int, BaseException]) #: Signal reporting the current completed count progressChanged = Signal([int, int]) #: Signal emitted when all the futures have completed. doneAll = Signal() def __init__(self, futures=None, *args, **kwargs): # type: (List[Future], ...) -> None super().__init__(*args, **kwargs) self.__futures = None self.__semaphore = None self.__countdone = 0 if futures is not None: self.setFutures(futures) def setFutures(self, futures): # type: (List[Future]) -> None """ Set the future instances to watch. Raise a `RuntimeError` if futures are already set. Parameters ---------- futures : List[Future] """ if self.__futures is not None: raise RuntimeError("already set") self.__futures = [] selfweakref = weakref.ref(self) schedule_emit = methodinvoke(self, "__emitpending", (int, Future)) # Semaphore counting the number of future that have enqueued # done notifications. Used for the `wait` implementation. self.__semaphore = semaphore = QSemaphore(0) for i, future in enumerate(futures): self.__futures.append(future) def on_done(index, f): try: selfref = selfweakref() # not safe really if selfref is None: # pragma: no cover return try: schedule_emit(index, f) except RuntimeError: # pragma: no cover # Ignore RuntimeErrors (when C++ side of QObject is deleted) # (? Use QObject.destroyed and remove the done callback ?) pass finally: semaphore.release() future.add_done_callback(partial(on_done, i)) if not self.__futures: # `futures` was an empty sequence. methodinvoke(self, "doneAll", ())() @Slot(int, Future) def __emitpending(self, index, future): # type: (int, Future) -> None assert QThread.currentThread() is self.thread() assert self.__futures[index] is future assert future.done() assert self.__countdone < len(self.__futures) self.__futures[index] = None self.__countdone += 1 if future.cancelled(): self.cancelledAt.emit(index, future) self.doneAt.emit(index, future) elif future.done(): self.finishedAt.emit(index, future) self.doneAt.emit(index, future) if future.exception(): self.exceptionReadyAt.emit(index, future.exception()) else: self.resultReadyAt.emit(index, future.result()) else: assert False self.progressChanged.emit(self.__countdone, len(self.__futures)) if self.__countdone == len(self.__futures): self.doneAll.emit() def flush(self): """ Flush all pending signal emits currently enqueued. Must only ever be called from the thread this object lives in (:func:`QObject.thread()`). """ if QThread.currentThread() is not self.thread(): raise RuntimeError("`flush()` called from a wrong thread.") # NOTE: QEvent.MetaCall is the event implementing the # `Qt.QueuedConnection` method invocation. QCoreApplication.sendPostedEvents(self, QEvent.MetaCall) def wait(self): """ Wait for for all the futures to complete and *enqueue* notifications to this object, but do not emit any signals. Use `flush()` to emit all signals after a `wait()` """ if self.__futures is None: raise RuntimeError("Futures were not set.") self.__semaphore.acquire(len(self.__futures)) self.__semaphore.release(len(self.__futures))
class NodeItem(QGraphicsObject): """ An widget node item in the canvas. """ #: Signal emitted when the scene position of the node has changed. positionChanged = Signal() #: Signal emitted when the geometry of the channel anchors changes. anchorGeometryChanged = Signal() #: Signal emitted when the item has been activated (by a mouse double #: click or a keyboard) activated = Signal() #: The item is under the mouse. hovered = Signal() #: Span of the anchor in degrees ANCHOR_SPAN_ANGLE = 90 #: Z value of the item Z_VALUE = 100 def __init__(self, widget_description=None, parent=None, **kwargs): self.__boundingRect = None QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QGraphicsItem.ItemHasNoContents, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsFocusable, True) # central body shape item self.shapeItem = None # in/output anchor items self.inputAnchorItem = None self.outputAnchorItem = None # title text item self.captionTextItem = None # error, warning, info items self.errorItem = None self.warningItem = None self.infoItem = None self.__title = "" self.__processingState = 0 self.__progress = -1 self.__statusMessage = "" self.__error = None self.__warning = None self.__info = None self.__anchorLayout = None self.__animationEnabled = False self.setZValue(self.Z_VALUE) self.setupGraphics() self.setWidgetDescription(widget_description) @classmethod def from_node(cls, node): """ Create an :class:`NodeItem` instance and initialize it from a :class:`SchemeNode` instance. """ self = cls() self.setWidgetDescription(node.description) # self.setCategoryDescription(node.category) return self @classmethod def from_node_meta(cls, meta_description): """ Create an `NodeItem` instance from a node meta description. """ self = cls() self.setWidgetDescription(meta_description) return self def setupGraphics(self): """ Set up the graphics. """ shape_rect = QRectF(-24, -24, 48, 48) self.shapeItem = NodeBodyItem(self) self.shapeItem.setShapeRect(shape_rect) self.shapeItem.setAnimationEnabled(self.__animationEnabled) # Rect for widget's 'ears'. anchor_rect = QRectF(-31, -31, 62, 62) self.inputAnchorItem = SinkAnchorItem(self) input_path = QPainterPath() start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2 input_path.arcMoveTo(anchor_rect, start_angle) input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE) self.inputAnchorItem.setAnchorPath(input_path) self.outputAnchorItem = SourceAnchorItem(self) output_path = QPainterPath() start_angle = self.ANCHOR_SPAN_ANGLE / 2 output_path.arcMoveTo(anchor_rect, start_angle) output_path.arcTo(anchor_rect, start_angle, -self.ANCHOR_SPAN_ANGLE) self.outputAnchorItem.setAnchorPath(output_path) self.inputAnchorItem.hide() self.outputAnchorItem.hide() # Title caption item self.captionTextItem = NameTextItem(self) self.captionTextItem.setPlainText("") self.captionTextItem.setPos(0, 33) def iconItem(standard_pixmap): item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap), iconSize=QSize(16, 16)) item.hide() return item self.errorItem = iconItem(QStyle.SP_MessageBoxCritical) self.warningItem = iconItem(QStyle.SP_MessageBoxWarning) self.infoItem = iconItem(QStyle.SP_MessageBoxInformation) self.prepareGeometryChange() self.__boundingRect = None # TODO: Remove the set[Widget|Category]Description. The user should # handle setting of icons, title, ... def setWidgetDescription(self, desc): """ Set widget description. """ self.widget_description = desc if desc is None: return icon = icon_loader.from_description(desc).get(desc.icon) if icon: self.setIcon(icon) if not self.title(): self.setTitle(desc.name) if desc.inputs: self.inputAnchorItem.show() if desc.outputs: self.outputAnchorItem.show() tooltip = NodeItem_toolTipHelper(self) self.setToolTip(tooltip) def setWidgetCategory(self, desc): """ Set the widget category. """ self.category_description = desc if desc and desc.background: background = NAMED_COLORS.get(desc.background, desc.background) color = QColor(background) if color.isValid(): self.setColor(color) def setIcon(self, icon): """ Set the node item's icon (:class:`QIcon`). """ if isinstance(icon, QIcon): self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon, iconSize=QSize(36, 36)) self.icon_item.setPos(-18, -18) else: raise TypeError def setColor(self, color, selectedColor=None): """ Set the widget color. """ if selectedColor is None: selectedColor = saturated(color, 150) palette = create_palette(color, selectedColor) self.shapeItem.setPalette(palette) def setPalette(self, palette): # TODO: The palette should override the `setColor` raise NotImplementedError def setTitle(self, title): """ Set the node title. The title text is displayed at the bottom of the node. """ self.__title = title self.__updateTitleText() def title(self): """ Return the node title. """ return self.__title title_ = Property(str, fget=title, fset=setTitle, doc="Node title text.") def setFont(self, font): """ Set the title text font (:class:`QFont`). """ if font != self.font(): self.prepareGeometryChange() self.captionTextItem.setFont(font) self.__updateTitleText() def font(self): """ Return the title text font. """ return self.captionTextItem.font() def setAnimationEnabled(self, enabled): """ Set the node animation enabled state. """ if self.__animationEnabled != enabled: self.__animationEnabled = enabled self.shapeItem.setAnimationEnabled(enabled) def animationEnabled(self): """ Are node animations enabled. """ return self.__animationEnabled def setProcessingState(self, state): """ Set the node processing state i.e. the node is processing (is busy) or is idle. """ if self.__processingState != state: self.__processingState = state self.shapeItem.setProcessingState(state) if not state: # Clear the progress meter. self.setProgress(-1) if self.__animationEnabled: self.shapeItem.ping() def processingState(self): """ The node processing state. """ return self.__processingState processingState_ = Property(int, fget=processingState, fset=setProcessingState) def setProgress(self, progress): """ Set the node work progress state (number between 0 and 100). """ if progress is None or progress < 0 or not self.__processingState: progress = -1 progress = max(min(progress, 100), -1) if self.__progress != progress: self.__progress = progress self.shapeItem.setProgress(progress) self.__updateTitleText() def progress(self): """ Return the node work progress state. """ return self.__progress progress_ = Property(float, fget=progress, fset=setProgress, doc="Node progress state.") def setStatusMessage(self, message): """ Set the node status message text. This text is displayed below the node's title. """ if self.__statusMessage != message: self.__statusMessage = message self.__updateTitleText() def statusMessage(self): return self.__statusMessage def setStateMessage(self, message): """ Set a state message to display over the item. Parameters ---------- message : UserMessage Message to display. `message.severity` is used to determine the icon and `message.contents` is used as a tool tip. """ # TODO: Group messages by message_id not by severity # and deprecate set[Error|Warning|Error]Message if message.severity == UserMessage.Info: self.setInfoMessage(message.contents) elif message.severity == UserMessage.Warning: self.setWarningMessage(message.contents) elif message.severity == UserMessage.Error: self.setErrorMessage(message.contents) def setErrorMessage(self, message): if self.__error != message: self.__error = message self.__updateMessages() def setWarningMessage(self, message): if self.__warning != message: self.__warning = message self.__updateMessages() def setInfoMessage(self, message): if self.__info != message: self.__info = message self.__updateMessages() def newInputAnchor(self): """ Create and return a new input :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.inputs): raise ValueError("Widget has no inputs.") anchor = AnchorPoint() self.inputAnchorItem.addAnchor(anchor, position=1.0) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) return anchor def removeInputAnchor(self, anchor): """ Remove input anchor. """ self.inputAnchorItem.removeAnchor(anchor) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) def newOutputAnchor(self): """ Create and return a new output :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.outputs): raise ValueError("Widget has no outputs.") anchor = AnchorPoint(self) self.outputAnchorItem.addAnchor(anchor, position=1.0) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) return anchor def removeOutputAnchor(self, anchor): """ Remove output anchor. """ self.outputAnchorItem.removeAnchor(anchor) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) def inputAnchors(self): """ Return a list of all input anchor points. """ return self.inputAnchorItem.anchorPoints() def outputAnchors(self): """ Return a list of all output anchor points. """ return self.outputAnchorItem.anchorPoints() def setAnchorRotation(self, angle): """ Set the anchor rotation. """ self.inputAnchorItem.setRotation(angle) self.outputAnchorItem.setRotation(angle) self.anchorGeometryChanged.emit() def anchorRotation(self): """ Return the anchor rotation. """ return self.inputAnchorItem.rotation() def boundingRect(self): # TODO: Important because of this any time the child # items change geometry the self.prepareGeometryChange() # needs to be called. if self.__boundingRect is None: self.__boundingRect = self.childrenBoundingRect() return self.__boundingRect def shape(self): # Shape for mouse hit detection. # TODO: Should this return the union of all child items? return self.shapeItem.shape() def __updateTitleText(self): """ Update the title text item. """ text = ['<div align="center">%s' % escape(self.title())] status_text = [] progress_included = False if self.__statusMessage: msg = escape(self.__statusMessage) format_fields = dict(parse_format_fields(msg)) if "progress" in format_fields and len(format_fields) == 1: # Insert progress into the status text format string. spec, _ = format_fields["progress"] if spec != None: progress_included = True progress_str = "{0:.0f}%".format(self.progress()) status_text.append(msg.format(progress=progress_str)) else: status_text.append(msg) if self.progress() >= 0 and not progress_included: status_text.append("%i%%" % int(self.progress())) if status_text: text += [ "<br/>", '<span style="font-style: italic">', "<br/>".join(status_text), "</span>" ] text += ["</div>"] text = "".join(text) # The NodeItems boundingRect could change. self.prepareGeometryChange() self.__boundingRect = None self.captionTextItem.setHtml(text) self.captionTextItem.document().adjustSize() width = self.captionTextItem.textWidth() self.captionTextItem.setPos(-width / 2.0, 33) def __updateMessages(self): """ Update message items (position, visibility and tool tips). """ items = [self.errorItem, self.warningItem, self.infoItem] messages = [self.__error, self.__warning, self.__info] for message, item in zip(messages, items): item.setVisible(bool(message)) item.setToolTip(message or "") shown = [item for item in items if item.isVisible()] count = len(shown) if count: spacing = 3 rects = [item.boundingRect() for item in shown] width = sum(rect.width() for rect in rects) width += spacing * max(0, count - 1) height = max(rect.height() for rect in rects) origin = self.shapeItem.boundingRect().top() - spacing - height origin = QPointF(-width / 2, origin) for item, rect in zip(shown, rects): item.setPos(origin) origin = origin + QPointF(rect.width() + spacing, 0) def mousePressEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.mousePressEvent(self, event) else: event.ignore() def mouseDoubleClickEvent(self, event): if self.shapeItem.path().contains(event.pos()): QGraphicsObject.mouseDoubleClickEvent(self, event) QTimer.singleShot(0, self.activated.emit) else: event.ignore() def contextMenuEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.contextMenuEvent(self, event) else: event.ignore() def focusInEvent(self, event): self.shapeItem.setHasFocus(True) return QGraphicsObject.focusInEvent(self, event) def focusOutEvent(self, event): self.shapeItem.setHasFocus(False) return QGraphicsObject.focusOutEvent(self, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: self.shapeItem.setSelected(value) self.captionTextItem.setSelectionState(value) elif change == QGraphicsItem.ItemPositionHasChanged: self.positionChanged.emit() return QGraphicsObject.itemChange(self, change, value)
class ScoreTable(OWComponent, QObject): shown_scores = \ Setting(set(chain(*BUILTIN_SCORERS_ORDER.values()))) shownScoresChanged = Signal() class ItemDelegate(QStyledItemDelegate): def sizeHint(self, *args): size = super().sizeHint(*args) return QSize(size.width(), size.height() + 6) def __init__(self, master): QObject.__init__(self) OWComponent.__init__(self, master) self.view = gui.TableView(wordWrap=True, editTriggers=gui.TableView.NoEditTriggers) header = self.view.horizontalHeader() header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setDefaultAlignment(Qt.AlignCenter) header.setStretchLastSection(False) header.setContextMenuPolicy(Qt.CustomContextMenu) header.customContextMenuRequested.connect(self.show_column_chooser) self.model = QStandardItemModel(master) self.model.setHorizontalHeaderLabels(["Method"]) self.view.setModel(self.model) self.view.setItemDelegate(self.ItemDelegate()) def _column_names(self): return (self.model.horizontalHeaderItem(section).data(Qt.DisplayRole) for section in range(1, self.model.columnCount())) def show_column_chooser(self, pos): # pylint doesn't know that self.shown_scores is a set, not a Setting # pylint: disable=unsupported-membership-test def update(col_name, checked): if checked: self.shown_scores.add(col_name) else: self.shown_scores.remove(col_name) self._update_shown_columns() menu = QMenu() header = self.view.horizontalHeader() for col_name in self._column_names(): action = menu.addAction(col_name) action.setCheckable(True) action.setChecked(col_name in self.shown_scores) action.triggered.connect(partial(update, col_name)) menu.exec(header.mapToGlobal(pos)) def _update_shown_columns(self): # pylint doesn't know that self.shown_scores is a set, not a Setting # pylint: disable=unsupported-membership-test header = self.view.horizontalHeader() for section, col_name in enumerate(self._column_names(), start=1): header.setSectionHidden(section, col_name not in self.shown_scores) self.view.resizeColumnsToContents() self.shownScoresChanged.emit() def update_header(self, scorers): # Set the correct horizontal header labels on the results_model. self.model.setColumnCount(1 + len(scorers)) self.model.setHorizontalHeaderItem(0, QStandardItem("Model")) for col, score in enumerate(scorers, start=1): item = QStandardItem(score.name) item.setToolTip(score.long_name) self.model.setHorizontalHeaderItem(col, item) self._update_shown_columns()
class MovableVline(pg.UIGraphicsItem): sigMoveFinished = Signal(object) sigMoved = Signal(object) def __init__(self, position=0., label="", color=(225, 0, 0), report=None): pg.UIGraphicsItem.__init__(self) self.moving = False self.mouseHovering = False self.report = report self.color = color self.isnone = False hp = pg.mkPen(color=color, width=3) np = pg.mkPen(color=color, width=2) self.line = pg.InfiniteLine(angle=90, movable=True, pen=np, hoverPen=hp) self.line.setParentItem(self) self.line.setCursor(Qt.SizeHorCursor) self.label = pg.TextItem("", anchor=(0, 1), angle=-90) self.label.setParentItem(self) self.setValue(position) self.setLabel(label) self.line.sigPositionChangeFinished.connect(self._moveFinished) self.line.sigPositionChanged.connect( lambda: (self._moved(), self.sigMoved.emit(self.value()))) self._lastTransform = None def setLabel(self, l): # add space on top not to overlap with cursor coordinates # I can not think of a better way: the text # size is decided at rendering time. As it is always the same, # these spaces will scale with the cursor coordinates. top_space = " " * 7 self.label.setText(top_space + l, color=self.color) def value(self): if self.isnone: return None return self.line.value() def rounded_value(self): """ Round the value according to current view on the graph. Return a decimal.Decimal object """ v = self.value() dx, dy = pixel_decimals(self.getViewBox()) if v is not None: v = Decimal(float_to_str_decimals(v, dx)) return v def setValue(self, val): oldval = self.value() if oldval == val: return # prevents recursion with None on input self.isnone = val is None if not self.isnone: rep = self.report # temporarily disable report self.report = None with blocked(self.line): # block sigPositionChanged by setValue self.line.setValue(val) self.report = rep self.line.show() self.label.show() self._move_label() else: self.line.hide() self.label.hide() self._moved() def boundingRect(self): br = pg.UIGraphicsItem.boundingRect(self) val = self.line.value() br.setLeft(val) br.setRight(val) return br.normalized() def _move_label(self): if self.value() is not None and self.getViewBox(): self.label.setPos(self.value(), self.getViewBox().viewRect().bottom()) def _moved(self): self._move_label() if self.report: if self.value() is not None: self.report.report(self, [("x", self.value())]) else: self.report.report_finished(self) def _moveFinished(self): if self.report: self.report.report_finished(self) self.sigMoveFinished.emit(self.value()) def paint(self, p, *args): tr = p.transform() if self._lastTransform != tr: self._move_label() self._lastTransform = tr super().paint(p, *args)
class Scheme(QObject): """ An :class:`QObject` subclass representing the scheme widget workflow with annotations. Parameters ---------- parent : :class:`QObject` A parent QObject item (default `None`). title : str The scheme title. description : str A longer description of the scheme. env: Mapping[str, Any] Extra workflow environment definition (application defined). Attributes ---------- nodes : list of :class:`.SchemeNode` A list of all the nodes in the scheme. links : list of :class:`.SchemeLink` A list of all links in the scheme. annotations : list of :class:`BaseSchemeAnnotation` A list of all the annotations in the scheme. """ # Flags indicating if loops are allowed in the workflow. NoLoops, AllowLoops, AllowSelfLoops = 0, 1, 2 # Signal emitted when a `node` is added to the scheme. node_added = Signal(SchemeNode) # Signal emitted when a `node` is removed from the scheme. node_removed = Signal(SchemeNode) # Signal emitted when a `link` is added to the scheme. link_added = Signal(SchemeLink) # Signal emitted when a `link` is removed from the scheme. link_removed = Signal(SchemeLink) # Signal emitted when a `annotation` is added to the scheme. annotation_added = Signal(BaseSchemeAnnotation) # Signal emitted when a `annotation` is removed from the scheme. annotation_removed = Signal(BaseSchemeAnnotation) # Signal emitted when the title of scheme changes. title_changed = Signal(str) # Signal emitted when the description of scheme changes. description_changed = Signal(str) #: Signal emitted when the associated runtime environment changes runtime_env_changed = Signal(str, object, object) def __init__(self, parent=None, title="", description="", env={}, **kwargs): # type: (Optional[QObject], str, str, Mapping[str, Any], Any) -> None super().__init__(parent, **kwargs) #: Workflow title (empty string by default). self.__title = title or "" #: Workflow description (empty string by default). self.__description = description or "" self.__annotations = [] # type: List[BaseSchemeAnnotation] self.__nodes = [] # type: List[SchemeNode] self.__links = [] # type: List[SchemeLink] self.__loop_flags = Scheme.NoLoops self.__env = dict(env) # type: Dict[str, Any] @property def nodes(self): # type: () -> List[SchemeNode] """ A list of all nodes (:class:`.SchemeNode`) currently in the scheme. """ return list(self.__nodes) @property def links(self): # type: () -> List[SchemeLink] """ A list of all links (:class:`.SchemeLink`) currently in the scheme. """ return list(self.__links) @property def annotations(self): # type: () -> List[BaseSchemeAnnotation] """ A list of all annotations (:class:`.BaseSchemeAnnotation`) in the scheme. """ return list(self.__annotations) def set_loop_flags(self, flags): self.__loop_flags = flags def loop_flags(self): return self.__loop_flags def set_title(self, title): # type: (str) -> None """ Set the scheme title text. """ if self.__title != title: self.__title = title self.title_changed.emit(title) def title(self): """ The title (human readable string) of the scheme. """ return self.__title title = Property(str, fget=title, fset=set_title) # type: ignore def set_description(self, description): # type: (str) -> None """ Set the scheme description text. """ if self.__description != description: self.__description = description self.description_changed.emit(description) def description(self): """ Scheme description text. """ return self.__description description = Property( # type: ignore str, fget=description, fset=set_description) def add_node(self, node): # type: (SchemeNode) -> None """ Add a node to the scheme. An error is raised if the node is already in the scheme. Parameters ---------- node : :class:`.SchemeNode` Node instance to add to the scheme. """ assert isinstance(node, SchemeNode) check_arg(node not in self.__nodes, "Node already in scheme.") self.__nodes.append(node) ev = events.NodeEvent(events.NodeEvent.NodeAdded, node) QCoreApplication.sendEvent(self, ev) log.info("Added node %r to scheme %r." % (node.title, self.title)) self.node_added.emit(node) def new_node(self, description, title=None, position=None, properties=None): # type: (WidgetDescription, str, Tuple[float, float], dict) -> SchemeNode """ Create a new :class:`.SchemeNode` and add it to the scheme. Same as:: scheme.add_node(SchemeNode(description, title, position, properties)) Parameters ---------- description : :class:`WidgetDescription` The new node's description. title : str, optional Optional new nodes title. By default `description.name` is used. position : Tuple[float, float] Optional position in a 2D space. properties : dict, optional A dictionary of optional extra properties. See also -------- .SchemeNode, Scheme.add_node """ if isinstance(description, WidgetDescription): node = SchemeNode(description, title=title, position=position, properties=properties) else: raise TypeError("Expected %r, got %r." % \ (WidgetDescription, type(description))) self.add_node(node) return node def remove_node(self, node): # type: (SchemeNode) -> SchemeNode """ Remove a `node` from the scheme. All links into and out of the `node` are also removed. If the node in not in the scheme an error is raised. Parameters ---------- node : :class:`.SchemeNode` Node instance to remove. """ check_arg(node in self.__nodes, "Node is not in the scheme.") self.__remove_node_links(node) self.__nodes.remove(node) ev = events.NodeEvent(events.NodeEvent.NodeRemoved, node) QCoreApplication.sendEvent(self, ev) log.info("Removed node %r from scheme %r." % (node.title, self.title)) self.node_removed.emit(node) return node def __remove_node_links(self, node): # type: (SchemeNode) -> None """ Remove all links for node. """ links_in, links_out = [], [] for link in self.__links: if link.source_node is node: links_out.append(link) elif link.sink_node is node: links_in.append(link) for link in links_out + links_in: self.remove_link(link) def add_link(self, link): # type: (SchemeLink) -> None """ Add a `link` to the scheme. Parameters ---------- link : :class:`.SchemeLink` An initialized link instance to add to the scheme. """ assert isinstance(link, SchemeLink) self.check_connect(link) self.__links.append(link) ev = events.LinkEvent(events.LinkEvent.LinkAdded, link) QCoreApplication.sendEvent(self, ev) log.info("Added link %r (%r) -> %r (%r) to scheme %r." % \ (link.source_node.title, link.source_channel.name, link.sink_node.title, link.sink_channel.name, self.title) ) self.link_added.emit(link) def new_link(self, source_node, source_channel, sink_node, sink_channel): # type: (SchemeNode, OutputSignal, SchemeNode, InputSignal) -> SchemeLink """ Create a new :class:`.SchemeLink` from arguments and add it to the scheme. The new link is returned. Parameters ---------- source_node : :class:`.SchemeNode` Source node of the new link. source_channel : :class:`.OutputSignal` Source channel of the new node. The instance must be from ``source_node.output_channels()`` sink_node : :class:`.SchemeNode` Sink node of the new link. sink_channel : :class:`.InputSignal` Sink channel of the new node. The instance must be from ``sink_node.input_channels()`` See also -------- .SchemeLink, Scheme.add_link """ link = SchemeLink(source_node, source_channel, sink_node, sink_channel) self.add_link(link) return link def remove_link(self, link): # type: (SchemeLink) -> None """ Remove a link from the scheme. Parameters ---------- link : :class:`.SchemeLink` Link instance to remove. """ check_arg(link in self.__links, "Link is not in the scheme.") self.__links.remove(link) ev = events.LinkEvent(events.LinkEvent.LinkRemoved, link) QCoreApplication.sendEvent(self, ev) log.info("Removed link %r (%r) -> %r (%r) from scheme %r." % \ (link.source_node.title, link.source_channel.name, link.sink_node.title, link.sink_channel.name, self.title) ) self.link_removed.emit(link) def check_connect(self, link): # type: (SchemeLink) -> None """ Check if the `link` can be added to the scheme and raise an appropriate exception. Can raise: - :class:`.SchemeCycleError` if the `link` would introduce a loop in the graph which does not allow loops. - :class:`.IncompatibleChannelTypeError` if the channel types are not compatible - :class:`.SinkChannelError` if a sink channel has a `Single` flag specification and the channel is already connected. - :class:`.DuplicatedLinkError` if a `link` duplicates an already present link. """ if not self.loop_flags() & Scheme.AllowSelfLoops and \ link.source_node is link.sink_node: raise SchemeCycleError("Cannot create self cycle in the scheme") elif not self.loop_flags() & Scheme.AllowLoops and \ self.creates_cycle(link): raise SchemeCycleError("Cannot create cycles in the scheme") if not self.compatible_channels(link): raise IncompatibleChannelTypeError( "Cannot connect %r to %r." \ % (link.source_channel.type, link.sink_channel.type) ) links = self.find_links(source_node=link.source_node, source_channel=link.source_channel, sink_node=link.sink_node, sink_channel=link.sink_channel) if links: raise DuplicatedLinkError( "A link from %r (%r) -> %r (%r) already exists" \ % (link.source_node.title, link.source_channel.name, link.sink_node.title, link.sink_channel.name) ) if link.sink_channel.single: links = self.find_links(sink_node=link.sink_node, sink_channel=link.sink_channel) if links: raise SinkChannelError( "%r is already connected." % link.sink_channel.name ) def creates_cycle(self, link): # type: (SchemeLink) -> bool """ Return `True` if `link` would introduce a cycle in the scheme. Parameters ---------- link : :class:`.SchemeLink` """ assert isinstance(link, SchemeLink) source_node, sink_node = link.source_node, link.sink_node upstream = self.upstream_nodes(source_node) upstream.add(source_node) return sink_node in upstream def compatible_channels(self, link): # type: (SchemeLink) -> bool """ Return `True` if the channels in `link` have compatible types. Parameters ---------- link : :class:`.SchemeLink` """ assert isinstance(link, SchemeLink) return compatible_channels(link.source_channel, link.sink_channel) def can_connect(self, link): # type: (SchemeLink) -> bool """ Return `True` if `link` can be added to the scheme. See also -------- Scheme.check_connect """ assert isinstance(link, SchemeLink) try: self.check_connect(link) return True except (SchemeCycleError, IncompatibleChannelTypeError, SinkChannelError, DuplicatedLinkError): return False def upstream_nodes(self, start_node): # type: (SchemeNode) -> Set[SchemeNode] """ Return a set of all nodes upstream from `start_node` (i.e. all ancestor nodes). Parameters ---------- start_node : :class:`.SchemeNode` """ visited = set() # type: Set[SchemeNode] queue = deque([start_node]) while queue: node = queue.popleft() snodes = [link.source_node for link in self.input_links(node)] for source_node in snodes: if source_node not in visited: queue.append(source_node) visited.add(node) visited.remove(start_node) return visited def downstream_nodes(self, start_node): # type: (SchemeNode) -> Set[SchemeNode] """ Return a set of all nodes downstream from `start_node`. Parameters ---------- start_node : :class:`.SchemeNode` """ visited = set() # type: Set[SchemeNode] queue = deque([start_node]) while queue: node = queue.popleft() snodes = [link.sink_node for link in self.output_links(node)] for source_node in snodes: if source_node not in visited: queue.append(source_node) visited.add(node) visited.remove(start_node) return visited def is_ancestor(self, node, child): # type: (SchemeNode, SchemeNode) -> bool """ Return True if `node` is an ancestor node of `child` (is upstream of the child in the workflow). Both nodes must be in the scheme. Parameters ---------- node : :class:`.SchemeNode` child : :class:`.SchemeNode` """ return child in self.downstream_nodes(node) def children(self, node): # type: (SchemeNode) -> Set[SchemeNode] """ Return a set of all children of `node`. """ return set(link.sink_node for link in self.output_links(node)) def parents(self, node): # type: (SchemeNode) -> Set[SchemeNode] """ Return a set of all parents of `node`. """ return set(link.source_node for link in self.input_links(node)) def input_links(self, node): # type: (SchemeNode) -> List[SchemeLink] """ Return a list of all input links (:class:`.SchemeLink`) connected to the `node` instance. """ return self.find_links(sink_node=node) def output_links(self, node): # type: (SchemeNode) -> List[SchemeLink] """ Return a list of all output links (:class:`.SchemeLink`) connected to the `node` instance. """ return self.find_links(source_node=node) def find_links(self, source_node=None, source_channel=None, sink_node=None, sink_channel=None): # type: (Optional[SchemeNode], Optional[OutputSignal], Optional[SchemeNode], Optional[InputSignal]) -> List[SchemeLink] # TODO: Speedup - keep index of links by nodes and channels result = [] def match(query, value): # type: (Optional[T], T) -> bool return query is None or value == query for link in self.__links: if match(source_node, link.source_node) and \ match(sink_node, link.sink_node) and \ match(source_channel, link.source_channel) and \ match(sink_channel, link.sink_channel): result.append(link) return result def propose_links(self, source_node, sink_node): # type: (SchemeNode, SchemeNode) -> List[Tuple[OutputSignal, InputSignal, int]] """ Return a list of ordered (:class:`OutputSignal`, :class:`InputSignal`, weight) tuples that could be added to the scheme between `source_node` and `sink_node`. .. note:: This can depend on the links already in the scheme. """ if source_node is sink_node and \ not self.loop_flags() & Scheme.AllowSelfLoops: # Self loops are not enabled return [] elif not self.loop_flags() & Scheme.AllowLoops and \ self.is_ancestor(sink_node, source_node): # Loops are not enabled. return [] outputs = source_node.output_channels() inputs = sink_node.input_channels() # Get existing links to sink channels that are Single. links = self.find_links(None, None, sink_node) already_connected_sinks = [link.sink_channel for link in links \ if link.sink_channel.single] def weight(out_c, in_c): # type: (OutputSignal, InputSignal) -> int if out_c.explicit or in_c.explicit: # Zero weight for explicit links weight = 0 else: # Does the connection type check (can only ever be False for # dynamic signals) type_checks, _ = _classify_connection(out_c, in_c) # Dynamic signals that require runtime instance type check # are considered last. check = [type_checks, in_c not in already_connected_sinks, bool(in_c.default), bool(out_c.default) ] weights = [2 ** i for i in range(len(check), 0, -1)] weight = sum([w for w, c in zip(weights, check) if c]) return weight proposed_links = [] for out_c in outputs: for in_c in inputs: if compatible_channels(out_c, in_c): proposed_links.append((out_c, in_c, weight(out_c, in_c))) return sorted(proposed_links, key=itemgetter(-1), reverse=True) def add_annotation(self, annotation): # type: (BaseSchemeAnnotation) -> None """ Add an annotation (:class:`BaseSchemeAnnotation` subclass) instance to the scheme. """ assert isinstance(annotation, BaseSchemeAnnotation) if annotation in self.__annotations: raise ValueError("Cannot add the same annotation multiple times") self.__annotations.append(annotation) ev = events.AnnotationEvent(events.AnnotationEvent.AnnotationAdded, annotation) QCoreApplication.sendEvent(self, ev) self.annotation_added.emit(annotation) def remove_annotation(self, annotation): # type: (BaseSchemeAnnotation) -> None """ Remove the `annotation` instance from the scheme. """ self.__annotations.remove(annotation) ev = events.AnnotationEvent(events.AnnotationEvent.AnnotationRemoved, annotation) QCoreApplication.sendEvent(self, ev) self.annotation_removed.emit(annotation) def clear(self): # type: () -> None """ Remove all nodes, links, and annotation items from the scheme. """ def is_terminal(node): # type: (SchemeNode) -> bool return not bool(self.find_links(source_node=node)) while self.nodes: terminal_nodes = filter(is_terminal, self.nodes) for node in terminal_nodes: self.remove_node(node) for annotation in self.annotations: self.remove_annotation(annotation) assert not (self.nodes or self.links or self.annotations) def sync_node_properties(self): # type: () -> None """ Called before saving, allowing a subclass to update/sync. The default implementation does nothing. """ pass def save_to(self, stream, pretty=True, **kwargs): """ Save the scheme as an xml formatted file to `stream` See also -------- readwrite.scheme_to_ows_stream """ with ExitStack() as exitstack: if isinstance(stream, str): stream = exitstack.enter_context(open(stream, "wb")) self.sync_node_properties() readwrite.scheme_to_ows_stream(self, stream, pretty, **kwargs) def load_from(self, stream, *args, **kwargs): """ Load the scheme from xml formatted `stream`. Any extra arguments are passed to `readwrite.scheme_load` See Also -------- readwrite.scheme_load """ if self.__nodes or self.__links or self.__annotations: raise ValueError("Scheme is not empty.") with ExitStack() as exitstack: if isinstance(stream, str): stream = exitstack.enter_context(open(stream, "rb")) readwrite.scheme_load(self, stream, *args, **kwargs) def set_runtime_env(self, key, value): # type: (str, Any) -> None """ Set a runtime environment variable `key` to `value` """ oldvalue = self.__env.get(key, None) if value != oldvalue: self.__env[key] = value self.runtime_env_changed.emit(key, value, oldvalue) def get_runtime_env(self, key, default=None): # type: (str, Any) -> Any """ Return a runtime environment variable for `key`. """ return self.__env.get(key, default) def runtime_env(self): # type: () -> Mapping[str, Any] """ Return (a view to) the full runtime environment. The return value is a types.MappingProxyType of the underlying environment dictionary. Changes to the env. will be reflected in it. """ return types.MappingProxyType(self.__env) class WindowGroup(types.SimpleNamespace): name = None # type: str default = None # type: bool state = None # type: List[Tuple[SchemeNode, bytes]] def __init__(self, name="", default=False, state=[]): super().__init__(name=name, default=default, state=state) def window_group_presets(self): # type: () -> List[Scheme.WindowGroup] """ Return a collection of preset window groups and their encoded states. The base implementation returns an empty list. """ return self.property("_presets") or [] def set_window_group_presets(self, groups): # type: (List[Scheme.WindowGroup]) -> None self.setProperty("_presets", groups)