示例#1
0
class progress(QObject):
    advance = Signal()
示例#2
0
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,
        )
示例#3
0
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)
示例#4
0
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
示例#5
0
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)))
示例#6
0
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
示例#7
0
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)
示例#8
0
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()
示例#9
0
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)
示例#10
0
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()
示例#11
0
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()
示例#12
0
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)
示例#13
0
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/>
                   &nbsp;&nbsp;&nbsp;expected {exp} ({p_exp:.0f} %)<br/>
                   &nbsp;&nbsp;&nbsp;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()
示例#14
0
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()
示例#15
0
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
示例#16
0
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("")
示例#17
0
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
示例#18
0
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(),
                    )
                )
示例#19
0
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}
示例#20
0
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))
示例#21
0
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
示例#22
0
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)
示例#23
0
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))
示例#27
0
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)
示例#28
0
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()
示例#29
0
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)
示例#30
0
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)