Exemplo n.º 1
0
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.suggestions = document.suggestions()
        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
Exemplo n.º 2
0
class VizRankDialog(QDialog, ProgressBarMixin, WidgetMessagesMixin):
    """
    Base class for VizRank dialogs, providing a GUI with a table and a button,
    and the skeleton for managing the evaluation of visualizations.

    Derived classes must provide methods

    - `iterate_states` for generating combinations (e.g. pairs of attritutes),
    - `compute_score(state)` for computing the score of a combination,
    - `row_for_state(state)` that returns a list of items inserted into the
       table for the given state.

    and, optionally,

    - `state_count` that returns the number of combinations (used for progress
       bar)
    - `on_selection_changed` that handles event triggered when the user selects
      a table row. The method should emit signal
      `VizRankDialog.selectionChanged(object)`.
    - `bar_length` returns the length of the bar corresponding to the score.

    The class provides a table and a button. A widget constructs a single
    instance of this dialog in its `__init__`, like (in Sieve) by using a
    convenience method :obj:`add_vizrank`::

        self.vizrank, self.vizrank_button = SieveRank.add_vizrank(
            box, self, "Score Combinations", self.set_attr)

    When the widget receives new data, it must call the VizRankDialog's
    method :obj:`VizRankDialog.initialize()` to clear the GUI and reset the
    state.

    Clicking the Start button calls method `run` (and renames the button to
    Pause). Run sets up a progress bar by getting the number of combinations
    from :obj:`VizRankDialog.state_count()`. It restores the paused state
    (if any) and calls generator :obj:`VizRankDialog.iterate_states()`. For
    each generated state, it calls :obj:`VizRankDialog.score(state)`, which
    must return the score (lower is better) for this state. If the returned
    state is not `None`, the data returned by `row_for_state` is inserted at
    the appropriate place in the table.

    Args:
        master (Orange.widget.OWWidget): widget to which the dialog belongs

    Attributes:
        master (Orange.widget.OWWidget): widget to which the dialog belongs
        captionTitle (str): the caption for the dialog. This can be a class
          attribute. `captionTitle` is used by the `ProgressBarMixin`.
    """

    captionTitle = ""

    processingStateChanged = Signal(int)
    progressBarValueChanged = Signal(float)
    messageActivated = Signal(Msg)
    messageDeactivated = Signal(Msg)
    selectionChanged = Signal(object)

    class Information(WidgetMessagesMixin.Information):
        nothing_to_rank = Msg("There is nothing to rank.")

    def __init__(self, master):
        """Initialize the attributes and set up the interface"""
        QDialog.__init__(self, master, windowTitle=self.captionTitle)
        WidgetMessagesMixin.__init__(self)
        self.setLayout(QVBoxLayout())

        self.insert_message_bar()
        self.layout().insertWidget(0, self.message_bar)
        self.master = master

        self.keep_running = False
        self.scheduled_call = None
        self.saved_state = None
        self.saved_progress = 0
        self.scores = []
        self.add_to_model = queue.Queue()

        self.update_timer = QTimer(self)
        self.update_timer.timeout.connect(self._update)
        self.update_timer.setInterval(200)

        self._thread = None
        self._worker = None

        self.filter = QLineEdit()
        self.filter.setPlaceholderText("过滤...")
        self.filter.textChanged.connect(self.filter_changed)
        self.layout().addWidget(self.filter)
        # Remove focus from line edit
        self.setFocus(Qt.ActiveWindowFocusReason)

        self.rank_model = QStandardItemModel(self)
        self.model_proxy = QSortFilterProxyModel(self,
                                                 filterCaseSensitivity=False)
        self.model_proxy.setSourceModel(self.rank_model)
        self.rank_table = view = QTableView(
            selectionBehavior=QTableView.SelectRows,
            selectionMode=QTableView.SingleSelection,
            showGrid=False,
            editTriggers=gui.TableView.NoEditTriggers)
        if self._has_bars:
            view.setItemDelegate(TableBarItem())
        else:
            view.setItemDelegate(HorizontalGridDelegate())
        view.setModel(self.model_proxy)
        view.selectionModel().selectionChanged.connect(
            self.on_selection_changed)
        view.horizontalHeader().setStretchLastSection(True)
        view.horizontalHeader().hide()
        self.layout().addWidget(view)

        self.button = gui.button(self,
                                 self,
                                 "Start",
                                 callback=self.toggle,
                                 default=True)

    @property
    def _has_bars(self):
        return type(self).bar_length is not VizRankDialog.bar_length

    @classmethod
    def add_vizrank(cls, widget, master, button_label, set_attr_callback):
        """
        Equip the widget with VizRank button and dialog, and monkey patch the
        widget's `closeEvent` and `hideEvent` to close/hide the vizrank, too.

        Args:
            widget (QWidget): the widget into whose layout to insert the button
            master (Orange.widgets.widget.OWWidget): the master widget
            button_label: the label for the button
            set_attr_callback: the callback for setting the projection chosen
                in the vizrank

        Returns:
            tuple with Vizrank dialog instance and push button
        """
        # Monkey patching could be avoided by mixing-in the class (not
        # necessarily a good idea since we can make a mess of multiple
        # defined/derived closeEvent and hideEvent methods). Furthermore,
        # per-class patching would be better than per-instance, but we don't
        # want to mess with meta-classes either.

        vizrank = cls(master)
        button = gui.button(widget,
                            master,
                            button_label,
                            callback=vizrank.reshow,
                            enabled=False)
        vizrank.selectionChanged.connect(lambda args: set_attr_callback(*args))

        master_close_event = master.closeEvent
        master_hide_event = master.hideEvent
        master_delete_event = master.onDeleteWidget

        def closeEvent(event):
            vizrank.close()
            master_close_event(event)

        def hideEvent(event):
            vizrank.hide()
            master_hide_event(event)

        def deleteEvent():
            vizrank.keep_running = False
            if vizrank._thread is not None and vizrank._thread.isRunning():
                vizrank._thread.quit()
                vizrank._thread.wait()

            master_delete_event()

        master.closeEvent = closeEvent
        master.hideEvent = hideEvent
        master.onDeleteWidget = deleteEvent
        return vizrank, button

    def reshow(self):
        """Put the widget on top of all windows
        """
        self.show()
        self.raise_()
        self.activateWindow()

    def initialize(self):
        """
        Clear and initialize the dialog.

        This method must be called by the widget when the data is reset,
        e.g. from `set_data` handler.
        """
        if self._thread is not None and self._thread.isRunning():
            self.keep_running = False
            self._thread.quit()
            self._thread.wait()
        self.keep_running = False
        self.scheduled_call = None
        self.saved_state = None
        self.saved_progress = 0
        self.update_timer.stop()
        self.progressBarFinished()
        self.scores = []
        self._update_model()  # empty queue
        self.rank_model.clear()
        self.button.setText("Start")
        self.button.setEnabled(self.check_preconditions())
        self._thread = QThread(self)
        self._worker = Worker(self)
        self._worker.moveToThread(self._thread)
        self._worker.stopped.connect(self._thread.quit)
        self._worker.stopped.connect(self._select_first_if_none)
        self._worker.stopped.connect(self._stopped)
        self._worker.done.connect(self._done)
        self._thread.started.connect(self._worker.do_work)

    def filter_changed(self, text):
        self.model_proxy.setFilterFixedString(text)

    def stop_and_reset(self, reset_method=None):
        if self.keep_running:
            self.scheduled_call = reset_method or self.initialize
            self.keep_running = False
        else:
            self.initialize()

    def check_preconditions(self):
        """Check whether there is sufficient data for ranking."""
        return True

    def on_selection_changed(self, selected, deselected):
        """
        Set the new visualization in the widget when the user select a
        row in the table.

        If derived class does not reimplement this, the table gives the
        information but the user can't click it to select the visualization.

        Args:
            selected: the index of the selected item
            deselected: the index of the previously selected item
        """
        pass

    def iterate_states(self, initial_state):
        """
        Generate all possible states (e.g. attribute combinations) for the
        given data. The content of the generated states is specific to the
        visualization.

        This method must be defined in the derived classes.

        Args:
            initial_state: initial state; None if this is the first call
        """
        raise NotImplementedError

    def state_count(self):
        """
        Return the number of states for the progress bar.

        Derived classes should implement this to ensure the proper behaviour of
        the progress bar"""
        return 0

    def compute_score(self, state):
        """
        Abstract method for computing the score for the given state. Smaller
        scores are better.

        Args:
            state: the state, e.g. the combination of attributes as generated
                by :obj:`state_count`.
        """
        raise NotImplementedError

    def bar_length(self, score):
        """Compute the bar length (between 0 and 1) corresponding to the score.
        Return `None` if the score cannot be normalized.
        """
        return None

    def row_for_state(self, score, state):
        """
        Abstract method that return the items that are inserted into the table.

        Args:
            score: score, computed by :obj:`compute_score`
            state: the state, e.g. combination of attributes
            """
        raise NotImplementedError

    def _select_first_if_none(self):
        if not self.rank_table.selectedIndexes():
            self.rank_table.selectRow(0)

    def _done(self):
        self.button.setText("Finished")
        self.button.setEnabled(False)
        self.keep_running = False
        self.saved_state = None

    def _stopped(self):
        self.update_timer.stop()
        self.progressBarFinished()
        self._update_model()
        self.stopped()
        if self.scheduled_call:
            self.scheduled_call()

    def _update(self):
        self._update_model()
        self._update_progress()

    def _update_progress(self):
        self.progressBarSet(
            int(self.saved_progress * 100 / max(1, self.state_count())))

    def _update_model(self):
        try:
            while True:
                pos, row_items = self.add_to_model.get_nowait()
                self.rank_model.insertRow(pos, row_items)
        except queue.Empty:
            pass

    def toggle(self):
        """Start or pause the computation."""
        self.keep_running = not self.keep_running
        if self.keep_running:
            self.button.setText("Pause")
            self.progressBarInit()
            self.update_timer.start()
            self.before_running()
            self._thread.start()
        else:
            self.button.setText("Continue")
            self._thread.quit()
            # Need to sync state (the worker must read the keep_running
            # state and stop) for reliable restart.
            self._thread.wait()

    def before_running(self):
        """Code that is run before running vizrank in its own thread"""
        pass

    def stopped(self):
        """Code that is run after stopping the vizrank thread"""
        pass
Exemplo n.º 3
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]:
            self.graph_writers.append(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):
        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):
        super().check_data()
        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()
        if self._domain_invalidated:
            self.graph.update_axes()
            self._domain_invalidated = False
        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()
Exemplo n.º 4
0
class OWChoropleth(OWWidget):
    """
    This is to `OWDataProjectionWidget` what
    `OWChoroplethPlotGraph` is to `OWScatterPlotBase`.
    """

    name = 'Choropleth Map'
    description = 'A thematic map in which areas are shaded in proportion ' \
                  'to the measurement of the statistical variable being displayed.'
    icon = "icons/Choropleth.svg"
    priority = 120

    class Inputs:
        data = Input("Data", Table, default=True)

    class Outputs:
        selected_data = Output("Selected Data", Table, default=True)
        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)

    settings_version = 2
    settingsHandler = DomainContextHandler()
    selection = Setting(None, schema_only=True)
    auto_commit = Setting(True)

    attr_lat = ContextSetting(None)
    attr_lon = ContextSetting(None)

    agg_attr = ContextSetting(None)
    agg_func = ContextSetting(DEFAULT_AGG_FUNC)
    admin_level = Setting(0)
    binning_index = Setting(0)

    GRAPH_CLASS = OWChoroplethPlotMapGraph
    graph = SettingProvider(OWChoroplethPlotMapGraph)
    graph_name = "graph.plot_widget.plotItem"

    input_changed = Signal(object)
    output_changed = Signal(object)

    class Warning(OWWidget.Warning):
        no_lat_lon_vars = Msg("Data has no latitude and longitude variables.")
        no_region = Msg("{} points are not in any region.")

    def __init__(self):
        super().__init__()
        self.data = None
        self.data_ids = None  # type: Optional[np.ndarray]

        self.agg_data = None  # type: Optional[np.ndarray]
        self.region_ids = None  # type: Optional[np.ndarray]

        self.choropleth_regions = []
        self.binnings = []

        self.input_changed.connect(self.set_input_summary)
        self.output_changed.connect(self.set_output_summary)
        self.setup_gui()

    def setup_gui(self):
        self._add_graph()
        self._add_controls()
        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)

    def _add_controls(self):
        options = dict(
            labelWidth=75, orientation=Qt.Horizontal, sendSelectedValue=True,
            contentsLength=14
        )

        lat_lon_box = gui.vBox(self.controlArea, True)
        self.lat_lon_model = DomainModel(DomainModel.MIXED,
                                         valid_types=(ContinuousVariable,))
        gui.comboBox(lat_lon_box, self, 'attr_lon', label='Longitude:',
                     callback=self.setup_plot, model=self.lat_lon_model,
                     **options)

        gui.comboBox(lat_lon_box, self, 'attr_lat', label='Latitude:',
                     callback=self.setup_plot, model=self.lat_lon_model,
                     **options)

        agg_box = gui.vBox(self.controlArea, True)
        self.agg_attr_model = DomainModel(valid_types=(ContinuousVariable,
                                                       DiscreteVariable))
        gui.comboBox(agg_box, self, 'agg_attr', label='Attribute:',
                     callback=self.update_agg, model=self.agg_attr_model,
                     **options)

        self.agg_func_combo = gui.comboBox(agg_box, self, 'agg_func',
                                           label='Agg.:',
                                           items=[DEFAULT_AGG_FUNC],
                                           callback=self.graph.update_colors,
                                           **options)

        a_slider = gui.hSlider(agg_box, self, 'admin_level', minValue=0,
                               maxValue=2, step=1, label='Detail:',
                               createLabel=False, callback=self.setup_plot)
        a_slider.setFixedWidth(176)

        visualization_box = gui.vBox(self.controlArea, True)
        b_slider = gui.hSlider(visualization_box, self, "binning_index",
                               label="Bin width:", minValue=0,
                               maxValue=max(1, len(self.binnings) - 1),
                               createLabel=False,
                               callback=self.graph.update_colors)
        b_slider.setFixedWidth(176)

        av_slider = gui.hSlider(visualization_box, self, "graph.alpha_value",
                                minValue=0, maxValue=255, step=10,
                                label="Opacity:", createLabel=False,
                                callback=self.graph.update_colors)
        av_slider.setFixedWidth(176)

        gui.checkBox(visualization_box, self, "graph.show_legend",
                     "Show legend",
                     callback=self.graph.update_legend_visibility)

        self.controlArea.layout().addStretch(100)

        plot_gui = OWPlotGUI(self)
        plot_gui.box_zoom_select(self.controlArea)
        gui.auto_send(self.controlArea, self, "auto_commit")

    @property
    def effective_variables(self):
        return [self.attr_lat, self.attr_lon] \
            if self.attr_lat and self.attr_lon else []

    @property
    def effective_data(self):
        return self.data.transform(Domain(self.effective_variables))

    # 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.agg_func = DEFAULT_AGG_FUNC
        self.Warning.no_region.clear()
        self.Warning.no_lat_lon_vars.clear()
        self.init_attr_values()
        self.openContext(self.data)

        if not (data_existed and self.data is not None and
                array_equal(effective_data.X, self.effective_data.X)):
            self.clear(cache=True)
            self.input_changed.emit(data)
            self.setup_plot()
        self.update_agg()
        self.apply_selection()
        self.unconditional_commit()

    def init_attr_values(self):
        domain = self.data.domain if self.data else None
        self.lat_lon_model.set_domain(domain)
        self.agg_attr_model.set_domain(domain)

        self.agg_attr = None
        self.attr_lat, self.attr_lon = None, None

        if self.data:
            self.agg_attr = self.data.domain.class_var
            attr_lat, attr_lon = find_lat_lon(self.data, filter_hidden=True)
            if attr_lat is None or attr_lon is None:
                # we either find both or none
                self.Warning.no_lat_lon_vars()
            else:
                self.attr_lat, self.attr_lon = attr_lat, attr_lon

    def set_input_summary(self, data):
        summary = str(len(data)) if data else self.info.NoInput
        self.info.set_input_summary(summary)

    def set_output_summary(self, data):
        summary = str(len(data)) if data else self.info.NoOutput
        self.info.set_output_summary(summary)

    def update_agg(self):
        current_agg = self.agg_func
        self.agg_func_combo.clear()
        new_aggs = list(AGG_FUNCS)

        if self.agg_attr is not None:
            if self.agg_attr.is_discrete:
                new_aggs = [agg for agg in AGG_FUNCS if AGG_FUNCS[agg].disc]
            elif self.agg_attr.is_time:
                new_aggs = [agg for agg in AGG_FUNCS if AGG_FUNCS[agg].time]
        self.agg_func_combo.addItems(new_aggs)

        if current_agg in new_aggs:
            self.agg_func = current_agg
        else:
            self.agg_func = DEFAULT_AGG_FUNC

        self.graph.update_colors()

    def setup_plot(self):
        self.controls.binning_index.setEnabled(not self.is_mode())
        self.clear()
        self.graph.reset_graph()

    def is_valid(self):
        return self.attr_lat is not None and \
               (self.agg_attr is not None or self.agg_func == "Count")

    def apply_selection(self):
        if self.data is not None and self.selection is not None:
            index_group = np.array(self.selection).T
            selection = np.zeros(self.graph.n_ids, dtype=np.uint8)
            selection[index_group[0]] = index_group[1]
            self.graph.selection = selection
            self.graph.update_selection_colors()

    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()

    def commit(self):
        self.send_data()

    def send_data(self):
        data, graph_sel = self.data, self.graph.get_selection()
        group_sel, selected_data, ann_data = None, None, None
        if data is not None and len(data) and self.region_ids is not None:
            # we get selection by region ids so we have to map it to points
            group_sel = np.zeros(len(data), dtype=int)
            for id, s in zip(self.region_ids, graph_sel):
                if s == 0:
                    continue
                id_indices = np.where(self.data_ids == id)[0]
                group_sel[id_indices] = s

            if np.sum(graph_sel) > 0:
                selected_data = create_groups_table(data, group_sel, False, "Group")

            if data is not None:
                if np.max(graph_sel) > 1:
                    ann_data = create_groups_table(data, group_sel)
                else:
                    ann_data = create_annotated_table(data, group_sel.astype(bool))

        self.output_changed.emit(selected_data)
        self.Outputs.selected_data.send(selected_data)
        self.Outputs.annotated_data.send(ann_data)

    def recompute_binnings(self):
        if self.is_mode():
            return

        if self.is_time():
            self.binnings = time_binnings(self.agg_data, min_unique=3,
                                          min_bins=3, max_bins=15)
        else:
            self.binnings = decimal_binnings(self.agg_data, min_unique=3,
                                             min_bins=3, max_bins=15)

        max_bins = len(self.binnings) - 1
        self.controls.binning_index.setMaximum(max_bins)
        self.binning_index = min(max_bins, self.binning_index)

    def get_binning(self) -> BinDefinition:
        return self.binnings[self.binning_index]

    def get_palette(self):
        if self.agg_func in ('Count', 'Count defined'):
            return DefaultContinuousPalette
        elif self.is_mode():
            return LimitedDiscretePalette(MAX_COLORS)
        else:
            return self.agg_attr.palette

    def get_color_data(self):
        return self.get_reduced_agg_data()

    def get_color_labels(self):
        if self.is_mode():
            return self.get_reduced_agg_data(return_labels=True)
        elif self.is_time():
            return self.agg_attr.str_val

    def get_reduced_agg_data(self, return_labels=False):
        """
        This returns agg data or its labels. It also merges infrequent data.
        """
        needs_merging = self.is_mode() \
                        and len(self.agg_attr.values) >= MAX_COLORS
        if return_labels and not needs_merging:
            return self.agg_attr.values

        if not needs_merging:
            return self.agg_data

        dist = bincount(self.agg_data, max_val=len(self.agg_attr.values) - 1)[0]
        infrequent = np.zeros(len(self.agg_attr.values), dtype=bool)
        infrequent[np.argsort(dist)[:-(MAX_COLORS - 1)]] = True
        if return_labels:
            return [value for value, infreq in zip(self.agg_attr.values, infrequent)
                    if not infreq] + ["Other"]
        else:
            result = self.agg_data.copy()
            freq_vals = [i for i, f in enumerate(infrequent) if not f]
            for i, infreq in enumerate(infrequent):
                if infreq:
                    result[self.agg_data == i] = MAX_COLORS - 1
                else:
                    result[self.agg_data == i] = freq_vals.index(i)
            return result

    def is_mode(self):
        return self.agg_attr is not None and \
               self.agg_attr.is_discrete and \
               self.agg_func == 'Mode'

    def is_time(self):
        return self.agg_attr is not None and \
               self.agg_attr.is_time and \
               self.agg_func not in ('Count', 'Count defined')

    @memoize_method(3)
    def get_regions(self, lat_attr, lon_attr, admin):
        """
        Map points to regions and get regions information.
        Returns:
            ndarray of ids corresponding to points,
            dict of region ids matched to their additional info,
            dict of region ids matched to their polygon
        """
        latlon = np.c_[self.data.get_column_view(lat_attr)[0],
                       self.data.get_column_view(lon_attr)[0]]
        region_info = latlon2region(latlon, admin)
        ids = np.array([region.get('_id') for region in region_info])
        region_info = {info.get('_id'): info for info in region_info}

        self.data_ids = np.array(ids)
        no_region = np.sum(self.data_ids == None)
        if no_region:
            self.Warning.no_region(no_region)

        unique_ids = list(set(ids) - {None})
        polygons = {_id: poly
                    for _id, poly in zip(unique_ids, get_shape(unique_ids))}
        return ids, region_info, polygons

    def get_grouped(self, lat_attr, lon_attr, admin, attr, agg_func):
        """
        Get aggregation value for points grouped by regions.
        Returns:
            Series of aggregated values
        """
        if attr is not None:
            data = self.data.get_column_view(attr)[0]
        else:
            data = np.ones(len(self.data))

        ids, _, _ = self.get_regions(lat_attr, lon_attr, admin)
        result = pd.Series(data, dtype=float)\
            .groupby(ids)\
            .agg(AGG_FUNCS[agg_func].transform)

        return result

    def get_agg_data(self) -> np.ndarray:
        result = self.get_grouped(self.attr_lat, self.attr_lon,
                                  self.admin_level, self.agg_attr,
                                  self.agg_func)

        self.agg_data = np.array(result.values)
        self.region_ids = np.array(result.index)

        arg_region_sort = np.argsort(self.region_ids)
        self.region_ids = self.region_ids[arg_region_sort]
        self.agg_data = self.agg_data[arg_region_sort]

        self.recompute_binnings()

        return self.agg_data

    def _repr_val(self, value):
        if self.agg_func in ('Count', 'Count defined'):
            return f"{value:d}"
        else:
            return self.agg_attr.repr_val(value)

    def get_choropleth_regions(self) -> List[_ChoroplethRegion]:
        """Recalculate regions"""
        if not self.is_valid():
            return []

        _, region_info, polygons = self.get_regions(self.attr_lat,
                                                    self.attr_lon,
                                                    self.admin_level)

        regions = []
        for _id in polygons:
            if isinstance(polygons[_id], MultiPolygon):
                # some regions consist of multiple polygons
                polys = list(polygons[_id].geoms)
            else:
                polys = [polygons[_id]]

            qpolys = [self.poly2qpoly(transform(self.deg2canvas, poly))
                      for poly in polys]
            regions.append(_ChoroplethRegion(id=_id, info=region_info[_id],
                                             qpolys=qpolys))

        self.choropleth_regions = sorted(regions, key=lambda cr: cr.id)
        self.get_agg_data()
        return self.choropleth_regions

    @staticmethod
    def poly2qpoly(poly: Polygon) -> QPolygonF:
        return QPolygonF([QPointF(x, y)
                          for x, y in poly.exterior.coords])

    @staticmethod
    def deg2canvas(x, y):
        x, y = deg2norm(x, y)
        y = 1 - y
        return x, y

    def clear(self, cache=False):
        self.choropleth_regions = []
        if cache:
            self.get_regions.cache_clear()

    def send_report(self):
        if self.data is None:
            return
        self.report_plot()

    def sizeHint(self):
        return QSize(1132, 708)

    def onDeleteWidget(self):
        super().onDeleteWidget()
        self.graph.plot_widget.getViewBox().deleteLater()
        self.graph.plot_widget.clear()
        self.graph.clear()

    def keyPressEvent(self, event):
        """Update the tip about using the modifier keys when selecting"""
        super().keyPressEvent(event)
        self.graph.update_tooltip(event.modifiers())

    def keyReleaseEvent(self, event):
        """Update the tip about using the modifier keys when selecting"""
        super().keyReleaseEvent(event)
        self.graph.update_tooltip(event.modifiers())

    def showEvent(self, ev):
        super().showEvent(ev)
        # reset the map on show event since before that we didn't know the
        # right resolution
        self.graph.update_view_range()

    def resizeEvent(self, ev):
        super().resizeEvent(ev)
        # when resizing we need to constantly reset the map so that new
        # portions are drawn
        self.graph.update_view_range(match_data=False)

    @classmethod
    def migrate_settings(cls, settings, version):
        if version < 2:
            settings["graph"] = {}
            rename_setting(settings, "admin", "admin_level")
            rename_setting(settings, "autocommit", "auto_commit")
            settings["graph"]["alpha_value"] = \
                round(settings["opacity"] * 2.55)
            settings["graph"]["show_legend"] = settings["show_legend"]

    @classmethod
    def migrate_context(cls, context, version):
        if version < 2:
            migrate_str_to_variable(context, names="lat_attr",
                                    none_placeholder="")
            migrate_str_to_variable(context, names="lon_attr",
                                    none_placeholder="")
            migrate_str_to_variable(context, names="attr",
                                    none_placeholder="")

            rename_setting(context, "lat_attr", "attr_lat")
            rename_setting(context, "lon_attr", "attr_lon")
            rename_setting(context, "attr", "agg_attr")
            # old selection will not be ported
            rename_setting(context, "selection", "old_selection")

            if context.values["agg_func"][0] == "Max":
                context.values["agg_func"] = ("Maximal",
                                              context.values["agg_func"][1])
            elif context.values["agg_func"][0] == "Min":
                context.values["agg_func"] = ("Minimal",
                                              context.values["agg_func"][1])
            elif context.values["agg_func"][0] == "Std":
                context.values["agg_func"] = ("Std.",
                                              context.values["agg_func"][1])
Exemplo n.º 5
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
Exemplo n.º 6
0
class Installer(QObject):
    installStatusChanged = Signal(str)
    started = Signal()
    finished = Signal()
    error = Signal(str, object, int, list)

    def __init__(self, parent=None, steps=[]):
        QObject.__init__(self, parent)
        self.__interupt = False
        self.__queue = deque(steps)
        self.__statusMessage = ""

    def start(self):
        QTimer.singleShot(0, self._next)

    def interupt(self):
        self.__interupt = True

    def setStatusMessage(self, message):
        if self.__statusMessage != message:
            self.__statusMessage = message
            self.installStatusChanged.emit(message)

    @Slot()
    def _next(self):
        def fmt_cmd(cmd):
            return "python " + (" ".join(map(shlex.quote, cmd)))

        command, pkg = self.__queue.popleft()
        if command == Install:
            inst = pkg.installable
            self.setStatusMessage("Installing {}".format(inst.name))
            links = []

            cmd = ["-m", "pip", "install"] + links + [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
            self.setStatusMessage("Upgrading {}".format(inst.name))

            cmd = ["-m", "pip", "install", "--upgrade", "--no-deps", 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

            cmd = ["-m", "pip", "install", 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 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
Exemplo n.º 7
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 displayText(self, value, locale):
            if isinstance(value, float):
                return f"{value:.3f}"
            else:
                return super().displayText(value, locale)

    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.sorted_model = ScoreModel()
        self.sorted_model.setSourceModel(self.model)
        self.view.setModel(self.sorted_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()
Exemplo n.º 8
0
class EditableTreeView(QWidget):
    dataChanged = Signal()
    selectionChanged = Signal()

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.__stack: List = []
        self.__stack_index: int = -1

        def push_on_data_changed(_, __, roles):
            if Qt.EditRole in roles:
                self._push_data()

        self.__model = QStandardItemModel()
        self.__model.dataChanged.connect(self.dataChanged)
        self.__model.dataChanged.connect(push_on_data_changed)
        self.__root: QStandardItem = self.__model.invisibleRootItem()

        self.__tree = TreeView(self.dataChanged)
        self.__tree.drop_finished.connect(self.dataChanged)
        self.__tree.drop_finished.connect(self._push_data)
        self.__tree.setModel(self.__model)
        self.__tree.selectionModel().selectionChanged.connect(
            self.selectionChanged)

        actions_widget = ModelActionsWidget()
        actions_widget.layout().setSpacing(1)

        action = QAction("+", self, toolTip="Add a new word")
        action.triggered.connect(self.__on_add)
        actions_widget.addAction(action)

        action = QAction("\N{MINUS SIGN}", self, toolTip="Remove word")
        action.triggered.connect(self.__on_remove)
        actions_widget.addAction(action)

        action = QAction("\N{MINUS SIGN}R", self,
                         toolTip="Remove word recursively (incl. children)")
        action.triggered.connect(self.__on_remove_recursive)
        actions_widget.addAction(action)

        gui.rubber(actions_widget)

        self.__undo_action = action = QAction("Undo", self, toolTip="Undo")
        action.triggered.connect(self.__on_undo)
        actions_widget.addAction(action)

        self.__redo_action = action = QAction("Redo", self, toolTip="Redo")
        action.triggered.connect(self.__on_redo)
        actions_widget.addAction(action)

        self._enable_undo_redo()

        layout = QVBoxLayout()
        layout.setSpacing(1)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.__tree)
        layout.addWidget(actions_widget)
        self.setLayout(layout)

    def __on_add(self):
        parent: QStandardItem = self.__root
        selection: List = self.__tree.selectionModel().selectedIndexes()
        if selection:
            sel_index: QModelIndex = selection[0]
            parent: QStandardItem = self.__model.itemFromIndex(sel_index)

        item = QStandardItem("")
        parent.appendRow(item)
        index: QModelIndex = item.index()
        with disconnected(self.__model.dataChanged, self.dataChanged):
            self.__model.setItemData(index, {Qt.EditRole: ""})
        self.__tree.setCurrentIndex(index)
        self.__tree.edit(index)

    def __on_remove_recursive(self):
        sel_model: QItemSelectionModel = self.__tree.selectionModel()
        if len(sel_model.selectedIndexes()):
            while sel_model.selectedIndexes():
                index: QModelIndex = sel_model.selectedIndexes()[0]
                self.__model.removeRow(index.row(), index.parent())
            self._push_data()
            self.dataChanged.emit()

    def __on_remove(self):
        sel_model: QItemSelectionModel = self.__tree.selectionModel()
        if len(sel_model.selectedIndexes()):

            while sel_model.selectedIndexes():
                index: QModelIndex = sel_model.selectedIndexes()[0]

                # move children to item's parent
                item: QStandardItem = self.__model.itemFromIndex(index)
                children = [item.takeChild(i) for i in range(item.rowCount())]
                parent = item.parent() or self.__root

                self.__model.removeRow(index.row(), index.parent())

                for child in children[::-1]:
                    parent.insertRow(index.row(), child)

            self.__tree.expandAll()
            self._push_data()
            self.dataChanged.emit()

    def __on_undo(self):
        self.__stack_index -= 1
        self._set_from_stack()

    def __on_redo(self):
        self.__stack_index += 1
        self._set_from_stack()

    def get_words(self) -> List:
        return _model_to_words(self.__root)

    def get_selected_words(self) -> Set:
        return set(self.__model.itemFromIndex(index).text() for index in
                   self.__tree.selectionModel().selectedIndexes())

    def get_selected_words_with_children(self) -> Set:
        words = set()
        for index in self.__tree.selectionModel().selectedIndexes():
            item: QStandardItem = self.__model.itemFromIndex(index)
            words.update(_model_to_words(item))
        return words

    def get_data(self, with_selection=False) -> Union[Dict, OntoType]:
        selection = self.__tree.selectionModel().selectedIndexes()
        return _model_to_tree(self.__root, selection, with_selection)

    def set_data(self, data: Dict, keep_history: bool = False):
        if not keep_history:
            self.__stack = []
            self.__stack_index = -1
        self._set_data(data)
        self._push_data()

    def _set_data(self, data: Dict):
        self.clear()
        _tree_to_model(data, self.__root, self.__tree.selectionModel())
        self.__tree.expandAll()

    def clear(self):
        if self.__model.hasChildren():
            self.__model.removeRows(0, self.__model.rowCount())

    def _enable_undo_redo(self):
        index = self.__stack_index
        self.__undo_action.setEnabled(index >= 1)
        self.__redo_action.setEnabled(index < len(self.__stack) - 1)

    def _push_data(self):
        self.__stack_index += 1
        self.__stack = self.__stack[:self.__stack_index]
        self.__stack.append(self.get_data())
        self._enable_undo_redo()

    def _set_from_stack(self):
        assert self.__stack_index < len(self.__stack)
        assert self.__stack_index >= 0
        self._set_data(self.__stack[self.__stack_index])
        self._enable_undo_redo()
        self.dataChanged.emit()
Exemplo n.º 9
0
class DendrogramWidget(QGraphicsWidget):
    """A Graphics Widget displaying a dendrogram."""
    class ClusterGraphicsItem(QGraphicsPathItem):
        #: The untransformed source path in 'dendrogram' logical coordinate
        #: system
        sourcePath = QPainterPath()  # type: QPainterPath
        sourceAreaShape = QPainterPath()  # type: QPainterPath

        __shape = None  # type: Optional[QPainterPath]
        __boundingRect = None  # type: Optional[QRectF]
        #: An extended path describing the full mouse hit area
        #: (extends all the way to the base of the dendrogram)
        __mouseAreaShape = QPainterPath()  # type: QPainterPath

        def setGeometryData(self, path, hitArea):
            # type: (QPainterPath, QPainterPath) -> None
            """
            Set the geometry (path) and the mouse hit area (hitArea) for this
            item.
            """
            super().setPath(path)
            self.prepareGeometryChange()
            self.__boundingRect = self.__shape = None
            self.__mouseAreaShape = hitArea

        def shape(self):
            # type: () -> QPainterPath
            if self.__shape is None:
                path = super().shape()  # type: QPainterPath
                self.__shape = path.united(self.__mouseAreaShape)
            return self.__shape

        def boundingRect(self):
            # type: () -> QRectF
            if self.__boundingRect is None:
                sh = self.shape()
                pw = self.pen().widthF() / 2.0
                self.__boundingRect = sh.boundingRect().adjusted(
                    -pw, -pw, pw, pw)
            return self.__boundingRect

    class _SelectionItem(QGraphicsItemGroup):
        def __init__(self, parent, path, unscaled_path, label=""):
            super().__init__(parent)
            self.path = QGraphicsPathItem(path, self)
            self.path.setPen(make_pen(width=1, cosmetic=True))
            self.addToGroup(self.path)

            self.label = QGraphicsSimpleTextItem(label)
            self._update_label_pos()
            self.addToGroup(self.label)

            self.unscaled_path = unscaled_path

        def set_path(self, path):
            self.path.setPath(path)
            self._update_label_pos()

        def set_label(self, label):
            self.label.setText(label)
            self.label.setBrush(Qt.blue)
            self._update_label_pos()

        def set_color(self, color):
            self.path.setBrush(QColor(color))

        def _update_label_pos(self):
            path = self.path.path()
            elements = (path.elementAt(i) for i in range(path.elementCount()))
            points = ((p.x, p.y) for p in elements)
            p1, p2, *rest = sorted(points)
            x, y = p1[0], (p1[1] + p2[1]) / 2
            brect = self.label.boundingRect()
            # leaf nodes' paths are 4 pixels higher; leafs are `len(rest) == 3`
            self.label.setPos(x - brect.width() - 4,
                              y - brect.height() + 4 * (len(rest) == 3))

    #: Orientation
    Left, Top, Right, Bottom = 1, 2, 3, 4

    #: Selection flags
    NoSelection, SingleSelection, ExtendedSelection = 0, 1, 2

    #: Emitted when a user clicks on the cluster item.
    itemClicked = Signal(ClusterGraphicsItem)
    #: Signal emitted when the selection changes.
    selectionChanged = Signal()
    #: Signal emitted when the selection was changed by the user.
    selectionEdited = Signal()

    def __init__(self,
                 parent=None,
                 root=None,
                 orientation=Left,
                 hoverHighlightEnabled=True,
                 selectionMode=ExtendedSelection,
                 **kwargs):
        super().__init__(None, **kwargs)
        # Filter all events from children (`ClusterGraphicsItem`s)
        self.setFiltersChildEvents(True)
        self.orientation = orientation
        self._root = None
        #: A tree with dendrogram geometry
        self._layout = None
        self._highlighted_item = None
        #: a list of selected items
        self._selection = OrderedDict()
        #: a {node: item} mapping
        self._items = {
        }  # type: Dict[Tree, DendrogramWidget.ClusterGraphicsItem]
        #: container for all cluster items.
        self._itemgroup = QGraphicsWidget(self)
        self._itemgroup.setGeometry(self.contentsRect())
        #: Transform mapping from 'dendrogram' to widget local coordinate
        #: system
        self._transform = QTransform()
        self._cluster_parent = {}
        self.__hoverHighlightEnabled = hoverHighlightEnabled
        self.__selectionMode = selectionMode
        self.setContentsMargins(0, 0, 0, 0)
        self.setRoot(root)
        if parent is not None:
            self.setParentItem(parent)

    def setSelectionMode(self, mode):
        """
        Set the selection mode.
        """
        assert mode in [
            DendrogramWidget.NoSelection, DendrogramWidget.SingleSelection,
            DendrogramWidget.ExtendedSelection
        ]

        if self.__selectionMode != mode:
            self.__selectionMode = mode
            if self.__selectionMode == DendrogramWidget.NoSelection and \
                    self._selection:
                self.setSelectedClusters([])
            elif self.__selectionMode == DendrogramWidget.SingleSelection and \
                    len(self._selection) > 1:
                self.setSelectedClusters([self.selected_nodes()[-1]])

    def selectionMode(self):
        """
        Return the current selection mode.
        """
        return self.__selectionMode

    def setHoverHighlightEnabled(self, enabled):
        if self.__hoverHighlightEnabled != bool(enabled):
            self.__hoverHighlightEnabled = bool(enabled)
            if self._highlighted_item is not None:
                self._set_hover_item(None)

    def isHoverHighlightEnabled(self):
        return self.__hoverHighlightEnabled

    def clear(self):
        """
        Clear the widget.
        """
        scene = self.scene()
        if scene is not None:
            scene.removeItem(self._itemgroup)
        else:
            self._itemgroup.setParentItem(None)
        self._itemgroup = QGraphicsWidget(self)
        self._itemgroup.setGeometry(self.contentsRect())
        self._items.clear()

        for item in self._selection.values():
            if scene is not None:
                scene.removeItem(item)
            else:
                item.setParentItem(None)

        self._root = None
        self._items = {}
        self._selection = OrderedDict()
        self._highlighted_item = None
        self._cluster_parent = {}
        self.updateGeometry()

    def setRoot(self, root):
        # type: (Tree) -> None
        """
        Set the root cluster tree node for display.

        Parameters
        ----------
        root : Tree
            The tree root node.
        """
        self.clear()
        self._root = root
        if root is not None:
            pen = make_pen(Qt.blue,
                           width=1,
                           cosmetic=True,
                           join_style=Qt.MiterJoin)
            for node in postorder(root):
                item = DendrogramWidget.ClusterGraphicsItem(self._itemgroup)
                item.setAcceptHoverEvents(True)
                item.setPen(pen)
                item.node = node
                for branch in node.branches:
                    assert branch in self._items
                    self._cluster_parent[branch] = node
                self._items[node] = item

            self._relayout()
            self._rescale()
        self.updateGeometry()

    set_root = setRoot

    def root(self):
        # type: () -> Tree
        """
        Return the cluster tree root node.

        Returns
        -------
        root : Tree
        """
        return self._root

    def item(self, node):
        # type: (Tree) -> DendrogramWidget.ClusterGraphicsItem
        """
        Return the ClusterGraphicsItem instance representing the cluster `node`.
        """
        return self._items.get(node)

    def heightAt(self, point):
        # type: (QPointF) -> float
        """
        Return the cluster height at the point in widget local coordinates.
        """
        if not self._root:
            return 0
        tinv, ok = self._transform.inverted()
        if not ok:
            return 0
        tpoint = tinv.map(point)
        if self.orientation in [self.Left, self.Right]:
            height = tpoint.x()
        else:
            height = tpoint.y()
        # Undo geometry prescaling
        base = self._root.value.height
        scale = self._height_scale_factor()
        # Use better better precision then double provides.
        Fr = fractions.Fraction
        if scale > 0:
            height = Fr(height) / Fr(scale)
        else:
            height = 0
        if self.orientation in [self.Left, self.Bottom]:
            height = Fr(base) - Fr(height)
        return float(height)

    height_at = heightAt

    def posAtHeight(self, height):
        # type: (float) -> float
        """
        Return a point in local coordinates for `height` (in cluster
        """
        if not self._root:
            return QPointF()
        scale = self._height_scale_factor()
        base = self._root.value.height
        height = scale * height
        if self.orientation in [self.Left, self.Bottom]:
            height = scale * base - height

        if self.orientation in [self.Left, self.Right]:
            p = QPointF(height, 0)
        else:
            p = QPointF(0, height)
        return self._transform.map(p)

    pos_at_height = posAtHeight

    def _set_hover_item(self, item):
        """Set the currently highlighted item."""
        if self._highlighted_item is item:
            return

        def branches(item):
            return [self._items[ch] for ch in item.node.branches]

        if self._highlighted_item:
            pen = make_pen(Qt.blue, width=1, cosmetic=True)
            for it in postorder(self._highlighted_item, branches):
                it.setPen(pen)

        self._highlighted_item = item
        if item:
            hpen = make_pen(Qt.blue, width=2, cosmetic=True)
            for it in postorder(item, branches):
                it.setPen(hpen)

    def leafItems(self):
        """Iterate over the dendrogram leaf items (:class:`QGraphicsItem`).
        """
        if self._root:
            return (self._items[leaf] for leaf in leaves(self._root))
        else:
            return iter(())

    leaf_items = leafItems

    def leafAnchors(self):
        """Iterate over the dendrogram leaf anchor points (:class:`QPointF`).

        The points are in the widget local coordinates.
        """
        for item in self.leafItems():
            anchor = QPointF(item.element.anchor)
            yield self.mapFromItem(item, anchor)

    leaf_anchors = leafAnchors

    def selectedNodes(self):
        """
        Return the selected cluster nodes.
        """
        return [item.node for item in self._selection]

    selected_nodes = selectedNodes

    def setSelectedItems(self, items: List[ClusterGraphicsItem]):
        """Set the item selection."""
        to_remove = set(self._selection) - set(items)
        to_add = set(items) - set(self._selection)

        for sel in to_remove:
            self._remove_selection(sel)
        for sel in to_add:
            self._add_selection(sel)

        if to_add or to_remove:
            self._re_enumerate_selections()
            self.selectionChanged.emit()

    set_selected_items = setSelectedItems

    def setSelectedClusters(self, clusters: List[Tree]) -> None:
        """Set the selected clusters.
        """
        self.setSelectedItems(list(map(self.item, clusters)))

    set_selected_clusters = setSelectedClusters

    def isItemSelected(self, item: ClusterGraphicsItem) -> bool:
        """Is `item` selected (is a root of a selection)."""
        return item in self._selection

    def isItemIncludedInSelection(self, item: ClusterGraphicsItem) -> bool:
        """Is item included in any selection."""
        return self._selected_super_item(item) is not None

    is_included = isItemIncludedInSelection

    def setItemSelected(self, item, state):
        # type: (ClusterGraphicsItem, bool) -> None
        """Set the `item`s selection state to `state`."""
        if state is False and item not in self._selection or \
                state is True and item in self._selection:
            return  # State unchanged

        if item in self._selection:
            if state is False:
                self._remove_selection(item)
                self._re_enumerate_selections()
                self.selectionChanged.emit()
        else:
            # If item is already inside another selected item,
            # remove that selection
            super_selection = self._selected_super_item(item)

            if super_selection:
                self._remove_selection(super_selection)
            # Remove selections this selection will override.
            sub_selections = self._selected_sub_items(item)

            for sub in sub_selections:
                self._remove_selection(sub)

            if state:
                self._add_selection(item)

            elif item in self._selection:
                self._remove_selection(item)

            self._re_enumerate_selections()
            self.selectionChanged.emit()

    select_item = setItemSelected

    @staticmethod
    def _create_path(item, path):
        ppath = QPainterPath()
        if item.node.is_leaf:
            ppath.addRect(path.boundingRect().adjusted(-8, -4, 0, 4))
        else:
            ppath.addPolygon(path)
            ppath = path_outline(ppath, width=-8)
        return ppath

    @staticmethod
    def _create_label(i):
        return f"C{i + 1}"

    def _add_selection(self, item):
        """Add selection rooted at item
        """
        outline = self._selection_poly(item)
        path = self._transform.map(outline)
        ppath = self._create_path(item, path)
        label = self._create_label(len(self._selection))
        selection_item = self._SelectionItem(self, ppath, outline, label)
        selection_item.setPos(self.contentsRect().topLeft())
        self._selection[item] = selection_item

    def _remove_selection(self, item):
        """Remove selection rooted at item."""

        selection_item = self._selection[item]

        selection_item.hide()
        selection_item.setParentItem(None)
        if self.scene():
            self.scene().removeItem(selection_item)

        del self._selection[item]

    def _selected_sub_items(self, item):
        """Return all selected subclusters under item."""
        def branches(item):
            return [self._items[ch] for ch in item.node.branches]

        res = []
        for item in list(preorder(item, branches))[1:]:
            if item in self._selection:
                res.append(item)
        return res

    def _selected_super_item(self, item):
        """Return the selected super item if it exists."""
        def branches(item):
            return [self._items[ch] for ch in item.node.branches]

        for selected_item in self._selection:
            if item in set(preorder(selected_item, branches)):
                return selected_item
        return None

    def _re_enumerate_selections(self):
        """Re enumerate the selection items and update the colors."""
        # Order the clusters
        items = sorted(self._selection.items(),
                       key=lambda item: item[0].node.value.first)

        palette = colorpalettes.LimitedDiscretePalette(len(items))
        for i, (item, selection_item) in enumerate(items):
            # delete and then reinsert to update the ordering
            del self._selection[item]
            self._selection[item] = selection_item
            selection_item.set_label(self._create_label(i))
            color = palette[i]
            color.setAlpha(150)
            selection_item.set_color(color)

    def _selection_poly(self, item):
        # type: (Tree) -> QPolygonF
        """
        Return an selection geometry covering item and all its children.
        """
        def left(item):
            return [self._items[ch] for ch in item.node.branches[:1]]

        def right(item):
            return [self._items[ch] for ch in item.node.branches[-1:]]

        itemsleft = list(preorder(item, left))[::-1]
        itemsright = list(preorder(item, right))
        # itemsleft + itemsright walks from the leftmost leaf up to the root
        # and down to the rightmost leaf
        assert itemsleft[0].node.is_leaf
        assert itemsright[-1].node.is_leaf

        if item.node.is_leaf:
            # a single anchor point
            vert = [itemsleft[0].element.anchor]
        else:
            vert = []
            for it in itemsleft[1:]:
                vert.extend([
                    it.element.path[0], it.element.path[1], it.element.anchor
                ])
            for it in itemsright[:-1]:
                vert.extend([
                    it.element.anchor, it.element.path[-2], it.element.path[-1]
                ])
            # close the polygon
            vert.append(vert[0])

            def isclose(a, b, rel_tol=1e-6):
                return abs(a - b) < rel_tol * max(abs(a), abs(b))

            def isclose_p(p1, p2, rel_tol=1e-6):
                return isclose(p1.x, p2.x, rel_tol) and \
                       isclose(p1.y, p2.y, rel_tol)

            # merge consecutive vertices that are (too) close
            acc = [vert[0]]
            for v in vert[1:]:
                if not isclose_p(v, acc[-1]):
                    acc.append(v)
            vert = acc

        return QPolygonF([QPointF(*p) for p in vert])

    def _update_selection_items(self):
        """Update the shapes of selection items after a scale change.
        """
        transform = self._transform
        for item, selection in self._selection.items():
            path = transform.map(selection.unscaled_path)
            ppath = self._create_path(item, path)
            selection.set_path(ppath)

    def _height_scale_factor(self):
        # Internal dendrogram height scale factor. The dendrogram geometry is
        # scaled by this factor to better condition the geometry
        if self._root is None:
            return 1
        base = self._root.value.height
        # implicitly scale the geometry to 0..1 scale or flush to 0 for fuzz
        if base >= np.finfo(base).eps:
            return 1 / base
        else:
            return 0

    def _relayout(self):
        if self._root is None:
            return

        scale = self._height_scale_factor()
        base = scale * self._root.value.height
        self._layout = dendrogram_path(self._root,
                                       self.orientation,
                                       scaleh=scale)
        for node_geom in postorder(self._layout):
            node, geom = node_geom.value
            item = self._items[node]
            item.element = geom
            # the untransformed source path
            item.sourcePath = path_toQtPath(geom)
            r = item.sourcePath.boundingRect()

            if self.orientation == Left:
                r.setRight(base)
            elif self.orientation == Right:
                r.setLeft(0)
            elif self.orientation == Top:
                r.setBottom(base)
            else:
                r.setTop(0)

            hitarea = QPainterPath()
            hitarea.addRect(r)
            item.sourceAreaShape = hitarea
            item.setGeometryData(item.sourcePath, item.sourceAreaShape)
            item.setZValue(-node.value.height)

    def _rescale(self):
        if self._root is None:
            return

        scale = self._height_scale_factor()
        base = scale * self._root.value.height
        crect = self.contentsRect()
        leaf_count = len(list(leaves(self._root)))
        if self.orientation in [Left, Right]:
            drect = QSizeF(base, leaf_count)
        else:
            drect = QSizeF(leaf_count, base)

        eps = np.finfo(np.float64).eps

        if abs(drect.width()) < eps:
            sx = 1.0
        else:
            sx = crect.width() / drect.width()

        if abs(drect.height()) < eps:
            sy = 1.0
        else:
            sy = crect.height() / drect.height()

        transform = QTransform().scale(sx, sy)
        self._transform = transform
        self._itemgroup.setPos(crect.topLeft())
        self._itemgroup.setGeometry(crect)
        for node_geom in postorder(self._layout):
            node, _ = node_geom.value
            item = self._items[node]
            item.setGeometryData(transform.map(item.sourcePath),
                                 transform.map(item.sourceAreaShape))
        self._selection_items = None
        self._update_selection_items()

    def sizeHint(self, which: Qt.SizeHint, constraint=QSizeF()) -> QRectF:
        # reimplemented
        fm = QFontMetrics(self.font())
        spacing = fm.lineSpacing()
        mleft, mtop, mright, mbottom = self.getContentsMargins()

        if self._root and which == Qt.PreferredSize:
            nleaves = len(
                [node for node in self._items.keys() if not node.branches])
            base = max(10, min(spacing * 16, 250))
            if self.orientation in [self.Left, self.Right]:
                return QSizeF(base, spacing * nleaves + mleft + mright)
            else:
                return QSizeF(spacing * nleaves + mtop + mbottom, base)

        elif which == Qt.MinimumSize:
            return QSizeF(mleft + mright + 10, mtop + mbottom + 10)
        else:
            return QSizeF()

    def sceneEventFilter(self, obj, event):
        if isinstance(obj, DendrogramWidget.ClusterGraphicsItem):
            if event.type() == QEvent.GraphicsSceneHoverEnter and \
                    self.__hoverHighlightEnabled:
                self._set_hover_item(obj)
                event.accept()
                return True
            elif event.type() == QEvent.GraphicsSceneMousePress and \
                    event.button() == Qt.LeftButton:

                is_selected = self.isItemSelected(obj)
                is_included = self.is_included(obj)
                current_selection = list(self._selection)

                if self.__selectionMode == DendrogramWidget.SingleSelection:
                    if event.modifiers() & Qt.ControlModifier:
                        self.setSelectedItems([obj] if not is_selected else [])
                    elif event.modifiers() & Qt.AltModifier:
                        self.setSelectedItems([])
                    elif event.modifiers() & Qt.ShiftModifier:
                        if not is_included:
                            self.setSelectedItems([obj])
                    elif current_selection != [obj]:
                        self.setSelectedItems([obj])
                elif self.__selectionMode == DendrogramWidget.ExtendedSelection:
                    if event.modifiers() & Qt.ControlModifier:
                        self.setItemSelected(obj, not is_selected)
                    elif event.modifiers() & Qt.AltModifier:
                        self.setItemSelected(self._selected_super_item(obj),
                                             False)
                    elif event.modifiers() & Qt.ShiftModifier:
                        if not is_included:
                            self.setItemSelected(obj, True)
                    elif current_selection != [obj]:
                        self.setSelectedItems([obj])

                if current_selection != self._selection:
                    self.selectionEdited.emit()
                self.itemClicked.emit(obj)
                event.accept()
                return True

        if event.type() == QEvent.GraphicsSceneHoverLeave:
            self._set_hover_item(None)

        return super().sceneEventFilter(obj, event)

    def changeEvent(self, event):
        # reimplemented
        super().changeEvent(event)

        if event.type() == QEvent.FontChange:
            self.updateGeometry()

        # QEvent.ContentsRectChange is missing in PyQt4 <= 4.11.3
        if event.type() == 178:  # QEvent.ContentsRectChange:
            self._rescale()

    def resizeEvent(self, event):
        # reimplemented
        super().resizeEvent(event)
        self._rescale()

    def mousePressEvent(self, event):
        # reimplemented
        super().mousePressEvent(event)
        # A mouse press on an empty widget part
        if event.modifiers() == Qt.NoModifier and self._selection:
            self.set_selected_clusters([])
Exemplo n.º 10
0
class AsyncUpdateLoop(QObject):
    """
    Run/drive an coroutine from the event loop.

    This is a utility class which can be used for implementing
    asynchronous update loops. I.e. coroutines which periodically yield
    control back to the Qt event loop.

    """
    Next = QEvent.registerEventType()

    #: State flags
    Idle, Running, Cancelled, Finished = 0, 1, 2, 3
    #: The coroutine has yielded control to the caller (with `object`)
    yielded = Signal(object)
    #: The coroutine has finished/exited (either with an exception
    #: or with a return statement)
    finished = Signal()

    #: The coroutine has returned (normal return statement / StopIteration)
    returned = Signal(object)
    #: The coroutine has exited with with an exception.
    raised = Signal(object)
    #: The coroutine was cancelled/closed.
    cancelled = Signal()

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.__coroutine = None
        self.__next_pending = False  # Flag for compressing scheduled events
        self.__in_next = False
        self.__state = AsyncUpdateLoop.Idle

    @Slot(object)
    def setCoroutine(self, loop):
        """
        Set the coroutine.

        The coroutine will be resumed (repeatedly) from the event queue.
        If there is an existing coroutine set it is first closed/cancelled.

        Raises an RuntimeError if the current coroutine is running.
        """
        if self.__coroutine is not None:
            self.__coroutine.close()
            self.__coroutine = None
            self.__state = AsyncUpdateLoop.Cancelled

            self.cancelled.emit()
            self.finished.emit()

        if loop is not None:
            self.__coroutine = loop
            self.__state = AsyncUpdateLoop.Running
            self.__schedule_next()

    @Slot()
    def cancel(self):
        """
        Cancel/close the current coroutine.

        Raises an RuntimeError if the current coroutine is running.
        """
        self.setCoroutine(None)

    def state(self):
        """
        Return the current state.
        """
        return self.__state

    def isRunning(self):
        return self.__state == AsyncUpdateLoop.Running

    def __schedule_next(self):
        if not self.__next_pending:
            self.__next_pending = True
            QTimer.singleShot(10, self.__on_timeout)

    def __next(self):
        if self.__coroutine is not None:
            try:
                rval = next(self.__coroutine)
            except StopIteration as stop:
                self.__state = AsyncUpdateLoop.Finished
                self.returned.emit(stop.value)
                self.finished.emit()
                self.__coroutine = None
            except BaseException as er:
                self.__state = AsyncUpdateLoop.Finished
                self.raised.emit(er)
                self.finished.emit()
                self.__coroutine = None
            else:
                self.yielded.emit(rval)
                self.__schedule_next()

    @Slot()
    def __on_timeout(self):
        assert self.__next_pending
        self.__next_pending = False
        if not self.__in_next:
            self.__in_next = True
            try:
                self.__next()
            finally:
                self.__in_next = False
        else:
            # warn
            self.__schedule_next()

    def customEvent(self, event):
        if event.type() == AsyncUpdateLoop.Next:
            self.__on_timeout()
        else:
            super().customEvent(event)
Exemplo n.º 11
0
class TreeView(QTreeView):
    Style = f"""
    QTreeView::branch {{
        background: palette(base);
    }}
    QTreeView::branch:has-siblings:!adjoins-item {{
        border-image: url({resources_path}/vline.png) 0;
    }}

    QTreeView::branch:has-siblings:adjoins-item {{
        border-image: url({resources_path}/branch-more.png) 0;
    }}

    QTreeView::branch:!has-children:!has-siblings:adjoins-item {{
        border-image: url({resources_path}/branch-end.png) 0;
    }}

    QTreeView::branch:has-children:!has-siblings:closed,
    QTreeView::branch:closed:has-children:has-siblings {{
            border-image: none;
            image: url({resources_path}/branch-closed.png);
    }}

    QTreeView::branch:open:has-children:!has-siblings,
    QTreeView::branch:open:has-children:has-siblings  {{
            border-image: none;
            image: url({resources_path}/branch-open.png);
    }}
    """
    drop_finished = Signal()

    def __init__(self, data_changed_cb: Callable):
        self.__data_changed_cb = data_changed_cb

        edit_triggers = QTreeView.DoubleClicked | QTreeView.EditKeyPressed
        super().__init__(
            editTriggers=int(edit_triggers),
            selectionMode=QTreeView.ExtendedSelection,
            dragEnabled=True,
            acceptDrops=True,
            defaultDropAction=Qt.MoveAction
        )
        self.setHeaderHidden(True)
        self.setDropIndicatorShown(True)
        self.setStyleSheet(self.Style)

        self.__disconnected = False

    def startDrag(self, actions: Qt.DropActions):
        with disconnected(self.model().dataChanged, self.__data_changed_cb):
            super().startDrag(actions)
        self.drop_finished.emit()

    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.source() != self:
            self.__disconnected = True
            self.model().dataChanged.disconnect(self.__data_changed_cb)
        super().dragEnterEvent(event)

    def dragLeaveEvent(self, event: QDragLeaveEvent):
        super().dragLeaveEvent(event)
        if self.__disconnected:
            self.__disconnected = False
            self.model().dataChanged.connect(self.__data_changed_cb)

    def dropEvent(self, event: QDropEvent):
        super().dropEvent(event)
        self.expandAll()
        if self.__disconnected:
            self.__disconnected = False
            self.model().dataChanged.connect(self.__data_changed_cb)
            self.drop_finished.emit()
Exemplo n.º 12
0
class ToolBox(QFrame):
    """
    A tool box widget.
    """
    # Signal emitted when a tab is toggled.
    tabToggled = Signal(int, bool)

    __exclusive = False  # type: bool

    def setExclusive(self, exclusive):  # type: (bool) -> None
        """
        Set exclusive tabs (only one tab can be open at a time).
        """
        if self.__exclusive != exclusive:
            self.__exclusive = exclusive
            self.__tabActionGroup.setExclusive(exclusive)
            checked = self.__tabActionGroup.checkedAction()
            if checked is None:
                # The action group can be out of sync with the actions state
                # when switching between exclusive states.
                actions_checked = [
                    page.action for page in self.__pages
                    if page.action.isChecked()
                ]
                if actions_checked:
                    checked = actions_checked[0]

            # Trigger/toggle remaining open pages
            if exclusive and checked is not None:
                for page in self.__pages:
                    if checked != page.action and page.action.isChecked():
                        page.action.trigger()

    def exclusive(self):  # type: () -> bool
        """
        Are the tabs in the toolbox exclusive.
        """
        return self.__exclusive

    exclusive_ = Property(bool,
                          fget=exclusive,
                          fset=setExclusive,
                          designable=True,
                          doc="Exclusive tabs")

    def __init__(self, parent=None, **kwargs):
        # type: (Optional[QWidget], Any)-> None
        super().__init__(parent, **kwargs)
        self.__pages = []  # type: List[_ToolBoxPage]
        self.__tabButtonHeight = -1
        self.__tabIconSize = QSize()
        self.__exclusive = False
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        # Scroll area for the contents.
        self.__scrollArea = QScrollArea(
            self,
            objectName="toolbox-scroll-area",
            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                   QSizePolicy.MinimumExpanding),
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            widgetResizable=True,
        )
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)

        # A widget with all of the contents.
        # The tabs/contents are placed in the layout inside this widget
        self.__contents = QWidget(self.__scrollArea,
                                  objectName="toolbox-contents")
        self.__contentsLayout = _ToolBoxLayout(
            sizeConstraint=_ToolBoxLayout.SetMinAndMaxSize, spacing=0)
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contents.setLayout(self.__contentsLayout)

        self.__scrollArea.setWidget(self.__contents)

        layout.addWidget(self.__scrollArea)

        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

        self.__tabActionGroup = QActionGroup(
            self,
            objectName="toolbox-tab-action-group",
            exclusive=self.__exclusive)
        self.__actionMapper = QSignalMapper(self)
        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToggled)

    def setTabButtonHeight(self, height):
        # type: (int) -> None
        """
        Set the tab button height.
        """
        if self.__tabButtonHeight != height:
            self.__tabButtonHeight = height
            for page in self.__pages:
                page.button.setFixedHeight(height)

    def tabButtonHeight(self):
        # type: () -> int
        """
        Return the tab button height.
        """
        return self.__tabButtonHeight

    def setTabIconSize(self, size):
        # type: (QSize) -> None
        """
        Set the tab button icon size.
        """
        if self.__tabIconSize != size:
            self.__tabIconSize = QSize(size)
            for page in self.__pages:
                page.button.setIconSize(size)

    def tabIconSize(self):
        # type: () -> QSize
        """
        Return the tab icon size.
        """
        return QSize(self.__tabIconSize)

    def tabButton(self, index):
        # type: (int) -> QAbstractButton
        """
        Return the tab button at `index`
        """
        return self.__pages[index].button

    def tabAction(self, index):
        # type: (int) -> QAction
        """
        Return open/close action for the tab at `index`.
        """
        return self.__pages[index].action

    def addItem(self, widget, text, icon=QIcon(), toolTip=""):
        # type: (QWidget, str, QIcon, str) -> int
        """
        Append the `widget` in a new tab and return its index.

        Parameters
        ----------
        widget : QWidget
            A widget to be inserted. The toolbox takes ownership
            of the widget.
        text : str
            Name/title of the new tab.
        icon : QIcon
            An icon for the tab button.
        toolTip : str
            Tool tip for the tab button.

        Returns
        -------
        index : int
            Index of the inserted tab
        """
        return self.insertItem(self.count(), widget, text, icon, toolTip)

    def insertItem(self, index, widget, text, icon=QIcon(), toolTip=""):
        # type: (int, QWidget, str, QIcon, str) -> int
        """
        Insert the `widget` in a new tab at position `index`.

        See also
        --------
        ToolBox.addItem
        """
        button = self.createTabButton(widget, text, icon, toolTip)

        self.__contentsLayout.insertWidget(index * 2, button)
        self.__contentsLayout.insertWidget(index * 2 + 1, widget)

        widget.hide()

        page = _ToolBoxPage(index, widget, button.defaultAction(), button)
        self.__pages.insert(index, page)

        # update the indices __pages list
        for i in range(index + 1, self.count()):
            self.__pages[i] = self.__pages[i]._replace(index=i)

        self.__updatePositions()

        # Show (open) the first tab.
        if self.count() == 1 and index == 0:
            page.action.trigger()

        self.__updateSelected()

        self.updateGeometry()
        return index

    def removeItem(self, index):
        # type: (int) -> None
        """
        Remove the widget at `index`.

        Note
        ----
        The widget is hidden but is is not deleted. It is up to the caller to
        delete it.
        """
        self.__contentsLayout.takeAt(2 * index + 1)
        self.__contentsLayout.takeAt(2 * index)
        page = self.__pages.pop(index)

        # Update the page indexes
        for i in range(index, self.count()):
            self.__pages[i] = self.__pages[i]._replace(index=i)

        page.button.deleteLater()

        # Hide the widget and reparent to self
        # This follows QToolBox.removeItem
        page.widget.hide()
        page.widget.setParent(self)

        self.__updatePositions()
        self.__updateSelected()

        self.updateGeometry()

    def count(self):
        # type: () -> int
        """
        Return the number of widgets inserted in the toolbox.
        """
        return len(self.__pages)

    def widget(self, index):
        # type: (int) -> QWidget
        """
        Return the widget at `index`.
        """
        return self.__pages[index].widget

    def createTabButton(self, widget, text, icon=QIcon(), toolTip=""):
        # type: (QWidget, str, QIcon, str) -> QAbstractButton
        """
        Create the tab button for `widget`.
        """
        action = QAction(text, self)
        action.setCheckable(True)

        if icon:
            action.setIcon(icon)

        if toolTip:
            action.setToolTip(toolTip)
        self.__tabActionGroup.addAction(action)
        self.__actionMapper.setMapping(action, action)
        action.toggled.connect(self.__actionMapper.map)

        button = ToolBoxTabButton(self, objectName="toolbox-tab-button")
        button.setDefaultAction(action)
        button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        button.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)

        if self.__tabIconSize.isValid():
            button.setIconSize(self.__tabIconSize)

        if self.__tabButtonHeight > 0:
            button.setFixedHeight(self.__tabButtonHeight)

        return button

    def ensureWidgetVisible(self, child, xmargin=50, ymargin=50):
        # type: (QWidget, int, int) -> None
        """
        Scroll the contents so child widget instance is visible inside
        the viewport.
        """
        self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin)

    def sizeHint(self):
        # type: () -> QSize
        """
        Reimplemented.
        """
        hint = self.__contentsLayout.sizeHint()

        if self.count():
            # Compute max width of hidden widgets also.
            scroll = self.__scrollArea
            scroll_w = scroll.verticalScrollBar().sizeHint().width()
            frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2
            max_w = max([p.widget.sizeHint().width() for p in self.__pages])
            hint = QSize(
                max(max_w, hint.width()) + scroll_w + frame_w, hint.height())

        return QSize(200, 200).expandedTo(hint)

    def __onTabActionToggled(self, action):
        # type: (QAction) -> None
        page = find(self.__pages, action, key=attrgetter("action"))
        on = action.isChecked()
        page.widget.setVisible(on)
        index = page.index

        if index > 0:
            # Update the `previous` tab buttons style hints
            previous = self.__pages[index - 1].button
            previous.selected = set_flag(previous.selected,
                                         QStyleOptionToolBox.NextIsSelected,
                                         on)
            previous.update()
        if index < self.count() - 1:
            next = self.__pages[index + 1].button
            next.selected = set_flag(next.selected,
                                     QStyleOptionToolBox.PreviousIsSelected,
                                     on)
            next.update()

        self.tabToggled.emit(index, on)

        self.__contentsLayout.invalidate()

    def __updateSelected(self):
        # type: () -> None
        """Update the tab buttons selected style flags.
        """
        if self.count() == 0:
            return

        def update(button, next_sel, prev_sel):
            # type: (ToolBoxTabButton, bool, bool) -> None
            button.selected = set_flag(button.selected,
                                       QStyleOptionToolBox.NextIsSelected,
                                       next_sel)
            button.selected = set_flag(button.selected,
                                       QStyleOptionToolBox.PreviousIsSelected,
                                       prev_sel)
            button.update()

        if self.count() == 1:
            update(self.__pages[0].button, False, False)
        elif self.count() >= 2:
            pages = self.__pages
            for i in range(1, self.count() - 1):
                update(pages[i].button, pages[i + 1].action.isChecked(),
                       pages[i - 1].action.isChecked())

    def __updatePositions(self):
        # type: () -> None
        """Update the tab buttons position style flags.
        """
        if self.count() == 0:
            return
        elif self.count() == 1:
            self.__pages[0].button.position = QStyleOptionToolBox.OnlyOneTab
        else:
            self.__pages[0].button.position = QStyleOptionToolBox.Beginning
            self.__pages[-1].button.position = QStyleOptionToolBox.End
            for p in self.__pages[1:-1]:
                p.button.position = QStyleOptionToolBox.Middle

        for p in self.__pages:
            p.button.update()
Exemplo n.º 13
0
class CanvasApplication(QApplication):
    fileOpenRequest = Signal(QUrl)

    __args = None

    def __init__(self, argv):
        fix_qt_plugins_path()
        if hasattr(Qt, "AA_EnableHighDpiScaling"):
            # Turn on HighDPI support when available
            QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)

        CanvasApplication.__args, argv_ = self.parse_style_arguments(argv)
        if self.__args.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)
        self.configureStyle()

    def event(self, event):
        if event.type() == QEvent.FileOpen:
            self.fileOpenRequest.emit(event.url())
        elif event.type() == QEvent.PolishRequest:
            self.configureStyle()
        return super().event(event)

    @staticmethod
    def parse_style_arguments(argv):
        parser = argparse.ArgumentParser()
        parser.add_argument("-style", type=str, default=None)
        parser.add_argument("-colortheme", type=str, default=None)
        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)
Exemplo n.º 14
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()

    # pylint: disable=unused-argument,no-self-use,unnecessary-pass
    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
Exemplo n.º 15
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("")
Exemplo n.º 16
0
class VolcanoGraph(pg.PlotWidget):
    selectionChanged = Signal()

    #: Selection Modes
    NoSelection, SymetricSelection, RectSelection = 0, 1, 2

    def __init__(self, parent=None, symbolSize=5, **kwargs):
        pg.PlotWidget.__init__(self, parent, **kwargs)

        self.getAxis("bottom").setLabel("log<sub>2</sub> (ratio)")
        self.getAxis("left").setLabel("-log<sub>10</sub> (P_value)")

        # Absolute cutoff values for symmetric selection mode.
        self.cutoffX = 2.0
        self.cutoffY = 2.0
        # maximum absolute x, y values.
        self.maxX, self.maxY = 10, 10
        self.symbolSize = symbolSize

        self.__selectionMode = VolcanoGraph.NoSelection
        self.__selectiondelegate = None
        self._item = None
        self._selitem = None
        self.plotData = numpy.empty((0, 2))
        self._stylebrush = numpy.array([
            pg.mkBrush((0, 0, 255, 100)),  # normal points
            pg.mkBrush((255, 0, 0, 100))  # selected points
        ])

    def setSelectionMode(self, mode):
        if mode != self.__selectionMode:
            viewbox = self.getViewBox()
            viewbox.setAcceptHoverEvents(True)
            if self.__selectiondelegate is not None:
                viewbox.removeEventFilter(self.__selectiondelegate)
                self.__selectiondelegate.deleteLater()
                self.__selectiondelegate = None

            self.__selectionMode = mode
            if mode == VolcanoGraph.SymetricSelection:
                self.__selectiondelegate = SymetricSelections(
                    viewbox,
                    x=min(self.maxX, self.cutoffX),
                    y=min(self.maxY, self.cutoffY))
                viewbox.installEventFilter(self.__selectiondelegate)
            elif mode == VolcanoGraph.RectSelection:
                self.__selectiondelegate = GraphSelections(viewbox)
                viewbox.installEventFilter(self.__selectiondelegate)
            else:
                pass

            if self.__selectiondelegate is not None:
                self.__selectiondelegate.selectionGeometryChanged.connect(
                    self.__on_selectionChanged)

            if self.plotData.size:
                self.updateSelectionArea()

    def selectionMode(self):
        return self.__selectionMode

    def __on_selectionChanged(self):
        if self.__selectionMode == VolcanoGraph.SymetricSelection:
            self.cutoffX, self.cutoffY = self.__selectiondelegate.cuttoff()

        self.updateSelectionArea()

        self.selectionChanged.emit()

    def selectionMask(self):
        if self.__selectiondelegate is not None:
            return self.__selectiondelegate.testSelection(self.plotData)
        else:
            return numpy.zeros((len(self.plotData), ), dtype=bool)

    def setPlotData(self, data):
        self.plotData = numpy.asarray(data)
        if self.plotData.size:
            self.maxX = numpy.max(numpy.abs(self.plotData[:, 0]))
            self.maxY = numpy.max(self.plotData[:, 1])
        else:
            self.maxX, self.maxY = 10, 10

        self.replot_()
        self.selectionChanged.emit()

    def setSymbolSize(self, size):
        if size != self.symbolSize:
            self.symbolSize = size
            if self._item is not None:
                self._item.setSize(size)

    def updateSelectionArea(self):
        mask = self.selectionMask()
        brush = self._stylebrush[mask.astype(int)]
        self._item.setBrush(brush)

        if self._selitem is not None:
            self.removeItem(self._selitem)
            self._selitem = None

        if self.__selectiondelegate is not None:
            self._selitem = QGraphicsItemGroup()
            self._selitem.dataBounds = \
                lambda axis, frac=1.0, orthoRange=None: None

            for p1, p2 in self.__selectiondelegate.selection:
                r = QRectF(p1, p2).normalized()
                ritem = QGraphicsRectItem(r, self._selitem)
                ritem.setBrush(QBrush(Qt.NoBrush))
                ritem.setPen(QPen(Qt.red, 0))

            self.addItem(self._selitem)

    def replot_(self):
        if self.plotData.size:
            if self._item is not None:
                self.removeItem(self._item)

            mask = self.selectionMask()
            brush = self._stylebrush[mask.astype(int)]
            self._item = ScatterPlotItem(x=self.plotData[:, 0],
                                         y=self.plotData[:, 1],
                                         brush=brush,
                                         size=self.symbolSize,
                                         antialias=True,
                                         data=numpy.arange(len(self.plotData)))
            self.addItem(self._item)

            self.updateSelectionArea()
        else:
            self.removeItem(self._item)
            self._item = None

    def clear(self):
        self._item = None
        self._selitem = None
        self.plotData = numpy.empty((0, 2))
        super().clear()
        self.selectionChanged.emit()

    def sizeHint(self):
        return QSize(500, 500)
Exemplo n.º 17
0
class RadvizVizRank(VizRankDialog, OWComponent):
    captionTitle = "Score Plots"
    n_attrs = Setting(3)
    minK = 10

    attrsSelected = Signal([])
    _AttrRole = next(gui.OrangeUserRole)

    percent_data_used = Setting(100)

    def __init__(self, master):
        """Add the spin box for maximal number of attributes"""
        VizRankDialog.__init__(self, master)
        OWComponent.__init__(self, master)

        self.master = master
        self.n_neighbors = 10

        box = gui.hBox(self)
        max_n_attrs = min(MAX_DISPLAYED_VARS, len(master.model_selected))
        self.n_attrs_spin = gui.spin(box,
                                     self,
                                     "n_attrs",
                                     3,
                                     max_n_attrs,
                                     label="Maximum number of variables: ",
                                     controlWidth=50,
                                     alignment=Qt.AlignRight,
                                     callback=self._n_attrs_changed)
        gui.rubber(box)
        self.last_run_n_attrs = None
        self.attr_color = master.attr_color
        self.attr_ordering = None
        self.data = None
        self.valid_data = None

        self.rank_table.clicked.connect(self.on_row_clicked)
        self.rank_table.verticalHeader().sectionClicked.connect(
            self.on_header_clicked)

    def initialize(self):
        super().initialize()
        self.attr_color = self.master.attr_color

    def _compute_attr_order(self):
        """
        used by VizRank to evaluate attributes
        """
        master = self.master
        attrs = [
            v for v in master.primitive_variables if v is not self.attr_color
        ]
        data = self.master.data.transform(
            Domain(attributes=attrs, class_vars=self.attr_color))
        self.data = data
        self.valid_data = np.hstack(
            (~np.isnan(data.X), ~np.isnan(data.Y.reshape(len(data.Y), 1))))
        relief = ReliefF if self.attr_color.is_discrete else RReliefF
        weights = relief(n_iterations=100, k_nearest=self.minK)(data)
        attrs = sorted(zip(weights, attrs), key=lambda x: (-x[0], x[1].name))
        self.attr_ordering = attr_ordering = [a for _, a in attrs]
        return attr_ordering

    def _evaluate_projection(self, x, y):
        """
        kNNEvaluate - evaluate class separation in the given projection using a k-NN method
        Parameters
        ----------
        x - variables to evaluate
        y - class

        Returns
        -------
        scores
        """
        if self.percent_data_used != 100:
            rand = np.random.choice(len(x),
                                    int(len(x) * self.percent_data_used / 100),
                                    replace=False)
            x = x[rand]
            y = y[rand]
        neigh = KNeighborsClassifier(n_neighbors=3) if self.attr_color.is_discrete else \
            KNeighborsRegressor(n_neighbors=3)
        assert ~(np.isnan(x).any(axis=None) | np.isnan(x).any(axis=None))
        neigh.fit(x, y)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", category=UserWarning)
            scores = cross_val_score(neigh, x, y, cv=3)
        return scores.mean()

    def _n_attrs_changed(self):
        """
        Change the button label when the number of attributes changes. The method does not reset
        anything so the user can still see the results until actually restarting the search.
        """
        if self.n_attrs != self.last_run_n_attrs or self.saved_state is None:
            self.button.setText("Start")
        else:
            self.button.setText("Continue")
        self.button.setEnabled(self.check_preconditions())

    def progressBarSet(self, value):
        self.setWindowTitle(self.captionTitle +
                            " Evaluated {} permutations".format(value))

    def check_preconditions(self):
        master = self.master
        if not super().check_preconditions():
            return False
        elif not master.btn_vizrank.isEnabled():
            return False
        self.n_attrs_spin.setMaximum(
            min(MAX_DISPLAYED_VARS, len(master.model_selected)))
        return True

    def on_selection_changed(self, selected, _):
        self.on_row_clicked(selected.indexes()[0])

    def on_row_clicked(self, index):
        self.selectionChanged.emit(index.data(self._AttrRole))

    def on_header_clicked(self, section):
        self.on_row_clicked(self.rank_model.index(section, 0))

    def iterate_states(self, state):
        if state is None:  # on the first call, compute order
            self.attrs = self._compute_attr_order()
            state = list(range(3))
        else:
            state = list(state)

        def combinations(n, s):
            while True:
                yield s
                for up, _ in enumerate(s):
                    s[up] += 1
                    if up + 1 == len(s) or s[up] < s[up + 1]:
                        break
                    s[up] = up
                if s[-1] == n:
                    if len(s) < self.n_attrs:
                        s = list(range(len(s) + 1))
                    else:
                        break

        for c in combinations(len(self.attrs), state):
            for p in islice(permutations(c[1:]), factorial(len(c) - 1) // 2):
                yield (c[0], ) + p

    def compute_score(self, state):
        attrs = [self.attrs[i] for i in state]
        domain = Domain(attributes=attrs, class_vars=[self.attr_color])
        data = self.data.transform(domain)
        projector = RadViz()
        projection = projector(data)
        radviz_xy = projection(data).X
        y = projector.preprocess(data).Y
        return -self._evaluate_projection(radviz_xy, y)

    def bar_length(self, score):
        return -score

    def row_for_state(self, score, state):
        attrs = [self.attrs[s] for s in state]
        item = QStandardItem("[{:0.6f}] ".format(-score) +
                             ", ".join(a.name for a in attrs))
        item.setData(attrs, self._AttrRole)
        return [item]

    def _update_progress(self):
        self.progressBarSet(int(self.saved_progress))

    def before_running(self):
        """
        Disable the spin for number of attributes before running and
        enable afterwards. Also, if the number of attributes is different than
        in the last run, reset the saved state (if it was paused).
        """
        if self.n_attrs != self.last_run_n_attrs:
            self.saved_state = None
            self.saved_progress = 0
        if self.saved_state is None:
            self.scores = []
            self.rank_model.clear()
        self.last_run_n_attrs = self.n_attrs
        self.n_attrs_spin.setDisabled(True)

    def stopped(self):
        self.n_attrs_spin.setDisabled(False)
Exemplo n.º 18
0
class GraphSelections(QObject):
    """
    Selection manager using a union of rectangle areas.
    """
    selectionStarted = Signal()
    selectionGeometryChanged = Signal()
    selectionFinished = Signal()

    def __init__(self, parent):
        QObject.__init__(self, parent)
        self.selection = []

    def mapToView(self, pos):
        """
        Map the `pos` in viewbox local into the view (plot) coordinates.
        """
        viewbox = self.parent()
        return viewbox.mapToView(pos)

    def start(self, event):
        pos = self.mapToView(event.pos())
        if event.modifiers() & Qt.ControlModifier:
            self.selection.append((pos, pos))
        else:
            self.selection = [(pos, pos)]
        self.selectionStarted.emit()
        self.selectionGeometryChanged.emit()

    def update(self, event):
        pos = self.mapToView(event.pos())
        self.selection[-1] = self.selection[-1][:-1] + (pos, )
        self.selectionGeometryChanged.emit()

    def end(self, event):
        self.update(event)
        self.selectionFinished.emit()

    def testSelection(self, data):
        """
        Given a

        Parameters
        ----------
        data : (N, 2) array
            Point coordinates

        """
        if len(data) == 0:
            return numpy.zeros(0, dtype=bool)

        def contained(a, left, top, right, bottom):
            assert left <= right and bottom <= top
            x, y = a.T
            return (x >= left) & (x <= right) & (y <= top) & (y >= bottom)

        data = numpy.asarray(data)
        selected = numpy.zeros(len(data), dtype=bool)
        for p1, p2 in self.selection:
            r = QRectF(p1, p2).normalized()
            # Note the inverted top/bottom (Qt coordinate system)
            selected |= contained(data, r.left(), r.bottom(), r.right(),
                                  r.top())
        return selected

    def mousePressEvent(self, event):
        """
        Handle the intercepted mouse event.

        Return True if the event has been handled, False otherwise
        (let the viewbox handle the event).
        """
        if event.button() == Qt.LeftButton:
            self.start(event)
            event.accept()
            return True
        else:
            return False

    def mouseMoveEvent(self, event):
        """
        Handle the intercepted mouse event.

        Return True if the event has been handled, False otherwise
        (let the viewbox handle the event).
        """
        if event.buttons() & Qt.LeftButton:
            self.update(event)
            event.accept()
            return True
        else:
            return False

    def mouseReleaseEvent(self, event):
        """
        Handle the intercepted mouse event.

        Return True if the event has been handled, False otherwise
        (let the viewbox handle the event).
        """
        if event.button() == Qt.LeftButton:
            self.update(event)
            self.end(event)
            event.accept()
            return True
        else:
            return False

    def eventFilter(self, obj, event):
        if obj is self.parent():
            if event.type() == QEvent.GraphicsSceneMousePress:
                return self.mousePressEvent(event)
            elif event.type() == QEvent.GraphicsSceneMouseMove:
                return self.mouseMoveEvent(event)
            elif event.type() == QEvent.GraphicsSceneMouseRelease:
                return self.mouseReleaseEvent(event)

        return QObject.eventFilter(self, obj, event)
Exemplo n.º 19
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
            n_attrs = len(self.attrs)
            use_heuristic = n_attrs > KMeansCorrelationHeuristic.n_clusters
            self.use_heuristic = use_heuristic and \
                len(data) * n_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):
        if self.sel_feature_index is not None:
            return len(self.attrs) - 1
        elif self.use_heuristic:
            n_clusters = KMeansCorrelationHeuristic.n_clusters
            n_avg_attrs = len(self.attrs) / n_clusters
            return n_clusters * n_avg_attrs * (n_avg_attrs - 1) / 2
        else:
            n_attrs = len(self.attrs)
            return n_attrs * (n_attrs - 1) / 2

    @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)
Exemplo n.º 20
0
class TaskState(QObject):

    status_changed = Signal(str)
    _p_status_changed = Signal(str)

    progress_changed = Signal(float)
    _p_progress_changed = Signal(float)

    partial_result_ready = Signal(object)
    _p_partial_result_ready = Signal(object)

    def __init__(self, *args):
        super().__init__(*args)
        self.__future = None
        self.watcher = FutureWatcher()
        self.__interuption_requested = False
        self.__progress = 0
        # Helpers to route the signal emits via a this object's queue.
        # This ensures 'atomic' disconnect from signals for targets/slots
        # in the same thread. Requires that the event loop is running in this
        # object's thread.
        self._p_status_changed.connect(self.status_changed,
                                       Qt.QueuedConnection)
        self._p_progress_changed.connect(self.progress_changed,
                                         Qt.QueuedConnection)
        self._p_partial_result_ready.connect(self.partial_result_ready,
                                             Qt.QueuedConnection)

    @property
    def future(self):
        # type: () -> Future
        return self.__future

    def set_status(self, text):
        self._p_status_changed.emit(text)

    def set_progress_value(self, value):
        if round(value, 1) > round(self.__progress, 1):
            # Only emit progress when it has changed sufficiently
            self._p_progress_changed.emit(value)
            self.__progress = value

    def set_partial_results(self, value):
        self._p_partial_result_ready.emit(value)

    def is_interuption_requested(self):
        return self.__interuption_requested

    def start(self, executor, func=None):
        # type: (futures.Executor, Callable[[], Any]) -> Future
        assert self.future is None
        assert not self.__interuption_requested
        self.__future = executor.submit(func)
        self.watcher.setFuture(self.future)
        return self.future

    def cancel(self):
        assert not self.__interuption_requested
        self.__interuption_requested = True
        if self.future is not None:
            rval = self.future.cancel()
        else:
            # not even scheduled yet
            rval = True
        return rval
Exemplo n.º 21
0
class ChoroplethItem(pg.GraphicsObject):
    """
    GraphicsObject that represents regions.
    Regions can consist of multiple disjoint polygons.
    """

    itemClicked = Signal(str)  # send region id

    def __init__(self, region: _ChoroplethRegion, pen: QPen, brush: QBrush):
        pg.GraphicsObject.__init__(self)
        self.region = region
        self.agg_value = None
        self.pen = pen
        self.brush = brush

        self._region_info = self._get_region_info(self.region)
        self._bounding_rect = reduce(
            lambda br1, br2: br1.united(br2),
            (qpoly.boundingRect() for qpoly in self.region.qpolys)
        )

    @staticmethod
    def _get_region_info(region: _ChoroplethRegion):
        region_text = "<br/>".join(escape('{} = {}'.format(k, v))
                                   for k, v in region.info.items())
        return "<b>Region info:</b><br/>" + region_text

    def tooltip(self):
        return f"<b>Agg. value = {self.agg_value}</b><hr/>{self._region_info}"

    def setPen(self, pen):
        self.pen = pen
        self.update()

    def setBrush(self, brush):
        self.brush = brush
        self.update()

    def paint(self, p: QPainter, *args):
        p.setBrush(self.brush)
        p.setPen(self.pen)
        for qpoly in self.region.qpolys:
            p.drawPolygon(qpoly)

    def boundingRect(self) -> QRectF:
        return self._bounding_rect

    def contains(self, point: QPointF) -> bool:
        return any(qpoly.containsPoint(point, Qt.OddEvenFill)
                   for qpoly in self.region.qpolys)

    def intersects(self, poly: QPolygonF) -> bool:
        return any(not qpoly.intersected(poly).isEmpty()
                   for qpoly in self.region.qpolys)

    def mouseClickEvent(self, ev):
        if ev.button() == Qt.LeftButton and self.contains(ev.pos()):
            self.itemClicked.emit(self.region.id)
            ev.accept()
        else:
            ev.ignore()
Exemplo n.º 22
0
class ViolinPlot(pg.PlotItem):
    """
    A violin plot item with interactive data boundary selection.
    """
    #: Emitted when the selection boundary has changed
    selectionChanged = Signal()
    #: Emitted when the selection boundary has been edited by the user
    #: (by dragging the boundary lines)
    selectionEdited = Signal()

    #: Selection Flags
    NoSelection, Low, High = 0, 1, 2

    def __init__(self, *args, enableMenu=False, axisItems=None, **kwargs):
        if axisItems is None:
            axisItems = {}
        for position in ("left", 'right', 'top', 'bottom'):
            axisItems.setdefault(position, AxisItem(position))

        super().__init__(*args, enableMenu=enableMenu, axisItems=axisItems,
                         **kwargs)
        self.__data = None
        #: min/max cutoff line positions
        self.__min = 0
        self.__max = 0
        self.__dataPointsVisible = True
        self.__selectionEnabled = True
        self.__selectionMode = ViolinPlot.High | ViolinPlot.Low
        self._plotitems = None

    def setData(self, data, nsamples, sample_range=None, color=Qt.magenta):
        assert np.all(np.isfinite(data))

        if data.size > 0:
            xmin, xmax = np.min(data), np.max(data)
        else:
            xmin = xmax = 0.0

        if sample_range is None:
            xrange = xmax - xmin
            sample_min = xmin - xrange * 0.025
            sample_max = xmax + xrange * 0.025
        else:
            sample_min, sample_max = sample_range

        sample = np.linspace(sample_min, sample_max, nsamples)
        if data.size < 2:
            est = np.full(sample.size, 1. / sample.size, )
        else:
            try:
                density = stats.gaussian_kde(data)
                est = density.evaluate(sample)
            except np.linalg.LinAlgError:
                est = np.zeros(sample.size)

        item = QGraphicsPathItem(violin_shape(sample, est))
        color = QColor(color)
        color.setAlphaF(0.5)
        item.setBrush(QBrush(color))
        pen = QPen(self.palette().color(QPalette.Shadow))
        pen.setCosmetic(True)
        item.setPen(pen)
        est_max = np.max(est)

        x = np.random.RandomState(0xD06F00D).uniform(
            -est_max, est_max, size=data.size
        )
        dots = ScatterPlotItem(
            x=x, y=data, size=3,
        )
        dots.setVisible(self.__dataPointsVisible)
        pen = QPen(self.palette().color(QPalette.Shadow), 1)
        hoverPen = QPen(self.palette().color(QPalette.Highlight), 1.5)
        cmax = SelectionLine(
            angle=0, pos=xmax, movable=True, bounds=(sample_min, sample_max),
            pen=pen, hoverPen=hoverPen
        )
        cmin = SelectionLine(
            angle=0, pos=xmin, movable=True, bounds=(sample_min, sample_max),
            pen=pen, hoverPen=hoverPen
        )
        cmax.setCursor(Qt.SizeVerCursor)
        cmin.setCursor(Qt.SizeVerCursor)

        selection_item = QGraphicsRectItem(
            QRectF(-est_max, xmin, est_max * 2, xmax - xmin)
        )
        selection_item.setPen(QPen(Qt.NoPen))
        selection_item.setBrush(QColor(0, 250, 0, 50))

        def update_selection_rect():
            mode = self.__selectionMode
            p = selection_item.parentItem()  # type: Optional[QGraphicsItem]
            while p is not None and not isinstance(p, pg.ViewBox):
                p = p.parentItem()
            if p is not None:
                viewbox = p  # type: pg.ViewBox
            else:
                viewbox = None
            rect = selection_item.rect()  # type: QRectF
            if mode & ViolinPlot.High:
                rect.setTop(cmax.value())
            elif viewbox is not None:
                rect.setTop(viewbox.viewRect().bottom())
            else:
                rect.setTop(cmax.maxRange[1])

            if mode & ViolinPlot.Low:
                rect.setBottom(cmin.value())
            elif viewbox is not None:
                rect.setBottom(viewbox.viewRect().top())
            else:
                rect.setBottom(cmin.maxRange[0])

            selection_item.setRect(rect.normalized())

        cmax.sigPositionChanged.connect(update_selection_rect)
        cmin.sigPositionChanged.connect(update_selection_rect)
        cmax.visibleChanged.connect(update_selection_rect)
        cmin.visibleChanged.connect(update_selection_rect)

        def setupper(line):
            ebound = self.__effectiveBoundary()
            elower, eupper = ebound
            mode = self.__selectionMode
            if not mode & ViolinPlot.High:
                return
            upper = line.value()
            lower = min(elower, upper)
            if lower != elower and mode & ViolinPlot.Low:
                self.__min = lower
                cmin.setValue(lower)

            if upper != eupper:
                self.__max = upper

            if ebound != self.__effectiveBoundary():
                self.selectionEdited.emit()
                self.selectionChanged.emit()

        def setlower(line):
            ebound = self.__effectiveBoundary()
            elower, eupper = ebound
            mode = self.__selectionMode
            if not mode & ViolinPlot.Low:
                return
            lower = line.value()
            upper = max(eupper, lower)
            if upper != eupper and mode & ViolinPlot.High:
                self.__max = upper
                cmax.setValue(upper)

            if lower != elower:
                self.__min = lower

            if ebound != self.__effectiveBoundary():
                self.selectionEdited.emit()
                self.selectionChanged.emit()

        cmax.sigPositionChanged.connect(setupper)
        cmin.sigPositionChanged.connect(setlower)
        selmode = self.__selectionMode
        cmax.setVisible(selmode & ViolinPlot.High)
        cmin.setVisible(selmode & ViolinPlot.Low)
        selection_item.setVisible(selmode)

        self.addItem(dots)
        self.addItem(item)
        self.addItem(cmax)
        self.addItem(cmin)
        self.addItem(selection_item)

        self.setRange(
            QRectF(-est_max, np.min(sample), est_max * 2, np.ptp(sample))
        )
        self._plotitems = SimpleNamespace(
            pointsitem=dots,
            densityitem=item,
            cmax=cmax,
            cmin=cmin,
            selection_item=selection_item
        )
        self.__min = xmin
        self.__max = xmax

    def setDataPointsVisible(self, visible):
        self.__dataPointsVisible = visible
        if self._plotitems is not None:
            self._plotitems.pointsitem.setVisible(visible)

    def setSelectionMode(self, mode):
        oldlower, oldupper = self.__effectiveBoundary()
        oldmode = self.__selectionMode
        mode = mode & 0b11
        if self.__selectionMode == mode:
            return

        self.__selectionMode = mode

        if self._plotitems is None:
            return

        cmin = self._plotitems.cmin
        cmax = self._plotitems.cmax
        selitem = self._plotitems.selection_item

        cmin.setVisible(mode & ViolinPlot.Low)
        cmax.setVisible(mode & ViolinPlot.High)
        selitem.setVisible(bool(mode))

        lower, upper = self.__effectiveBoundary()
        # The recorded values are not bounded by each other on gui interactions
        # when one is disabled. Rectify this now.
        if (oldmode ^ mode) & ViolinPlot.Low and mode & ViolinPlot.High:
            # Lower activated and High enabled
            lower = min(lower, upper)
        if (oldmode ^ mode) & ViolinPlot.High and mode & ViolinPlot.Low:
            # High activated and Low enabled
            upper = max(lower, upper)

        with block_signals(self):
            if lower != oldlower and mode & ViolinPlot.Low:
                cmin.setValue(lower)
            if upper != oldupper and mode & ViolinPlot.High:
                cmax.setValue(upper)

        self.selectionChanged.emit()

    def setBoundary(self, low, high):
        """
        Set the lower and upper selection boundary value.
        """
        changed = 0
        mode = self.__selectionMode
        if self.__min != low:
            self.__min = low
            changed |= mode & ViolinPlot.Low
        if self.__max != high:
            self.__max = high
            changed |= mode & ViolinPlot.High

        if changed:
            if self._plotitems:
                with block_signals(self):
                    if changed & ViolinPlot.Low:
                        self._plotitems.cmin.setValue(low)
                    if changed & ViolinPlot.High:
                        self._plotitems.cmax.setValue(high)

            self.selectionChanged.emit()

    def boundary(self):
        """
        Return the current lower and upper selection boundary values.
        """
        return self.__min, self.__max

    def __effectiveBoundary(self):
        # effective boundary, masked by selection mode
        low, high = -np.inf, np.inf
        if self.__selectionMode & ViolinPlot.Low:
            low = self.__min
        if self.__selectionMode & ViolinPlot.High:
            high = self.__max
        return low, high

    def clear(self):
        super().clear()
        self._plotitems = None

    def mouseDragEvent(self, event):
        mode = self.__selectionMode
        if mode != ViolinPlot.NoSelection and event.buttons() & Qt.LeftButton:
            start = event.buttonDownScenePos(Qt.LeftButton)  # type: QPointF
            pos = event.scenePos()  # type: QPointF
            cmin, cmax = self._plotitems.cmin, self._plotitems.cmax
            assert cmin.parentItem() is cmax.parentItem()
            pos = self.mapToItem(cmin.parentItem(), pos)
            start = self.mapToItem(cmin.parentItem(), start)
            if mode & ViolinPlot.Low and mode & ViolinPlot.High:
                lower, upper = min(pos.y(), start.y()), max(pos.y(), start.y())
                cmin.setValue(lower)
                cmax.setValue(upper)
            elif mode & ViolinPlot.Low:
                lower = pos.y()
                cmin.setValue(lower)
            elif mode & ViolinPlot.High:
                upper = pos.y()
                cmax.setValue(upper)
            event.accept()
Exemplo n.º 23
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(cleanup(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)
Exemplo n.º 24
0
class LinearProjectionVizRank(VizRankDialog, OWComponent):
    captionTitle = "Score Plots"
    n_attrs = Setting(3)
    minK = 10

    attrsSelected = Signal([])
    _AttrRole = next(gui.OrangeUserRole)

    def __init__(self, master):
        # Add the spin box for a number of attributes to take into account.
        VizRankDialog.__init__(self, master)
        OWComponent.__init__(self, master)

        box = gui.hBox(self)
        max_n_attrs = len(master.model_selected)
        self.n_attrs_spin = gui.spin(box,
                                     self,
                                     "n_attrs",
                                     3,
                                     max_n_attrs,
                                     label="Number of variables: ",
                                     controlWidth=50,
                                     alignment=Qt.AlignRight,
                                     callback=self._n_attrs_changed)
        gui.rubber(box)
        self.last_run_n_attrs = None
        self.attr_color = master.attr_color
        self.attrs = []

    def initialize(self):
        super().initialize()
        self.attr_color = self.master.attr_color

    def before_running(self):
        """
        Disable the spin for number of attributes before running and
        enable afterwards. Also, if the number of attributes is different than
        in the last run, reset the saved state (if it was paused).
        """
        if self.n_attrs != self.last_run_n_attrs:
            self.saved_state = None
            self.saved_progress = 0
        if self.saved_state is None:
            self.scores = []
            self.rank_model.clear()
        self.last_run_n_attrs = self.n_attrs
        self.n_attrs_spin.setDisabled(True)

    def stopped(self):
        self.n_attrs_spin.setDisabled(False)

    def check_preconditions(self):
        master = self.master
        if not super().check_preconditions():
            return False
        elif not master.btn_vizrank.isEnabled():
            return False
        n_cont_var = len([
            v for v in master.continuous_variables
            if v is not master.attr_color
        ])
        self.n_attrs_spin.setMaximum(n_cont_var)
        return True

    def state_count(self):
        n_all_attrs = len(self.attrs)
        if not n_all_attrs:
            return 0
        n_attrs = self.n_attrs
        return factorial(n_all_attrs) // (
            2 * factorial(n_all_attrs - n_attrs) * n_attrs)

    def iterate_states(self, state):
        if state is None:  # on the first call, compute order
            self.attrs = self._score_heuristic()
            state = list(range(self.n_attrs))
        else:
            state = list(state)

        def combinations(n, s):
            while True:
                yield s
                for up, _ in enumerate(s):
                    s[up] += 1
                    if up + 1 == len(s) or s[up] < s[up + 1]:
                        break
                    s[up] = up
                if s[-1] == n:
                    break

        for c in combinations(len(self.attrs), state):
            for p in islice(permutations(c[1:]), factorial(len(c) - 1) // 2):
                yield (c[0], ) + p

    def compute_score(self, state):
        master = self.master
        data = master.data
        domain = Domain([self.attrs[i] for i in state], data.domain.class_vars)
        projection = master.projector(data.transform(domain))
        ec = projection(data).X
        y = column_data(data, self.attr_color, dtype=float)
        if ec.shape[0] < self.minK:
            return None
        n_neighbors = min(self.minK, len(ec) - 1)
        knn = NearestNeighbors(n_neighbors=n_neighbors).fit(ec)
        ind = knn.kneighbors(return_distance=False)
        # pylint: disable=invalid-unary-operand-type
        if self.attr_color.is_discrete:
            return -np.sum(y[ind] == y.reshape(-1, 1)) / n_neighbors / len(y)
        return -r2_score(y, np.mean(y[ind], axis=1)) * (len(y) / len(data))

    def bar_length(self, score):
        return max(0, -score)

    def _score_heuristic(self):
        def normalized(a):
            span = np.max(a, axis=0) - np.min(a, axis=0)
            span[span == 0] = 1
            return (a - np.mean(a, axis=0)) / span

        domain = self.master.data.domain
        attr_color = self.master.attr_color
        domain = Domain(attributes=[
            v for v in chain(domain.variables, domain.metas)
            if v.is_continuous and v is not attr_color
        ],
                        class_vars=attr_color)
        data = self.master.data.transform(domain)
        data.X = normalized(data.X)
        relief = ReliefF if attr_color.is_discrete else RReliefF
        weights = relief(n_iterations=100, k_nearest=self.minK)(data)
        results = sorted(zip(weights, domain.attributes),
                         key=lambda x: (-x[0], x[1].name))
        return [attr for _, attr in results]

    def row_for_state(self, score, state):
        attrs = [self.attrs[i] for i in state]
        item = QStandardItem(", ".join(a.name for a in attrs))
        item.setData(attrs, self._AttrRole)
        return [item]

    def on_selection_changed(self, selected, deselected):
        if not selected.indexes():
            return
        attrs = selected.indexes()[0].data(self._AttrRole)
        self.selectionChanged.emit([attrs])

    def _n_attrs_changed(self):
        if self.n_attrs != self.last_run_n_attrs or self.saved_state is None:
            self.button.setText("Start")
        else:
            self.button.setText("Continue")
        self.button.setEnabled(self.check_preconditions())
Exemplo n.º 25
0
class progress(QObject):
    advance = Signal()
class LinksEditWidget(QGraphicsWidget):
    """
    A Graphics Widget for editing the links between two nodes.
    """
    def __init__(self, *args, **kwargs):
        QGraphicsWidget.__init__(self, *args, **kwargs)
        self.setAcceptedMouseButtons(Qt.LeftButton | Qt.RightButton)

        self.source = None
        self.sink = None

        # QGraphicsWidget/Items in the scene.
        self.sourceNodeWidget = None
        self.sourceNodeTitle = None
        self.sinkNodeWidget = None
        self.sinkNodeTitle = None

        self.__links = []

        self.__textItems = []
        self.__iconItems = []
        self.__tmpLine = None
        self.__dragStartItem = None

        self.setLayout(QGraphicsLinearLayout(Qt.Vertical))
        self.layout().setContentsMargins(0, 0, 0, 0)

    def removeItems(self, items):
        """
        Remove child items from the widget and scene.
        """
        scene = self.scene()
        for item in items:
            item.setParentItem(None)
            if scene is not None:
                scene.removeItem(item)

    def clear(self):
        """
        Clear the editor state (source and sink nodes, channels ...).
        """
        if self.layout().count():
            widget = self.layout().takeAt(0).graphicsItem()
            self.removeItems([widget])

        self.source = None
        self.sink = None

    def setNodes(self, source, sink):
        """
        Set the source/sink nodes (:class:`SchemeNode` instances) between
        which to edit the links.

        .. note:: Call this before :func:`setLinks`.

        """
        self.clear()

        self.source = source
        self.sink = sink

        self.__updateState()

    def setLinks(self, links):
        """
        Set a list of links to display between the source and sink
        nodes. `links` must be a list of (`OutputSignal`, `InputSignal`)
        tuples where the first element refers to the source node
        and the second to the sink node (as set by `setNodes`).

        """
        self.clearLinks()
        for output, input in links:
            self.addLink(output, input)

    def links(self):
        """
        Return the links between the source and sink node.
        """
        return [(link.output, link.input) for link in self.__links]

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            startItem = find_item_at(self.scene(),
                                     event.pos(),
                                     type=ChannelAnchor)
            if startItem is not None:
                # Start a connection line drag.
                self.__dragStartItem = startItem
                self.__tmpLine = None
                event.accept()
                return

            lineItem = find_item_at(self.scene(),
                                    event.scenePos(),
                                    type=QGraphicsLineItem)
            if lineItem is not None:
                # Remove a connection under the mouse
                for link in self.__links:
                    if link.lineItem == lineItem:
                        self.removeLink(link.output, link.input)
                event.accept()
                return

        QGraphicsWidget.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:

            downPos = event.buttonDownPos(Qt.LeftButton)
            if not self.__tmpLine and self.__dragStartItem and \
                    (downPos - event.pos()).manhattanLength() > \
                        QApplication.instance().startDragDistance():
                # Start a line drag
                line = QGraphicsLineItem(self)
                start = self.__dragStartItem.boundingRect().center()
                start = self.mapFromItem(self.__dragStartItem, start)
                line.setLine(start.x(), start.y(),
                             event.pos().x(),
                             event.pos().y())

                pen = QPen(self.palette().color(QPalette.Foreground), 4)
                pen.setCapStyle(Qt.RoundCap)
                line.setPen(pen)
                line.show()

                self.__tmpLine = line

            if self.__tmpLine:
                # Update the temp line
                line = self.__tmpLine.line()
                line.setP2(event.pos())
                self.__tmpLine.setLine(line)

        QGraphicsWidget.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton and self.__tmpLine:
            endItem = find_item_at(self.scene(),
                                   event.scenePos(),
                                   type=ChannelAnchor)

            if endItem is not None:
                startItem = self.__dragStartItem
                startChannel = startItem.channel()
                endChannel = endItem.channel()
                possible = False

                # Make sure the drag was from input to output (or reversed) and
                # not between input -> input or output -> output
                if type(startChannel) != type(endChannel):
                    if isinstance(startChannel, InputSignal):
                        startChannel, endChannel = endChannel, startChannel

                    possible = compatible_channels(startChannel, endChannel)

                if possible:
                    self.addLink(startChannel, endChannel)

            self.scene().removeItem(self.__tmpLine)
            self.__tmpLine = None
            self.__dragStartItem = None

        QGraphicsWidget.mouseReleaseEvent(self, event)

    def addLink(self, output, input):
        """
        Add a link between `output` (:class:`OutputSignal`) and `input`
        (:class:`InputSignal`).

        """
        if not compatible_channels(output, input):
            return

        if output not in self.source.output_channels():
            raise ValueError("%r is not an output channel of %r" % \
                             (output, self.source))

        if input not in self.sink.input_channels():
            raise ValueError("%r is not an input channel of %r" % \
                             (input, self.sink))

        if input.single:
            # Remove existing link if it exists.
            for s1, s2, _ in self.__links:
                if s2 == input:
                    self.removeLink(s1, s2)

        line = QGraphicsLineItem(self)

        source_anchor = self.sourceNodeWidget.anchor(output)
        sink_anchor = self.sinkNodeWidget.anchor(input)

        source_pos = source_anchor.boundingRect().center()
        source_pos = self.mapFromItem(source_anchor, source_pos)

        sink_pos = sink_anchor.boundingRect().center()
        sink_pos = self.mapFromItem(sink_anchor, sink_pos)
        line.setLine(source_pos.x(), source_pos.y(), sink_pos.x(),
                     sink_pos.y())
        pen = QPen(self.palette().color(QPalette.Foreground), 4)
        pen.setCapStyle(Qt.RoundCap)
        line.setPen(pen)

        self.__links.append(_Link(output, input, line))

    def removeLink(self, output, input):
        """
        Remove a link between the `output` and `input` channels.
        """
        for link in list(self.__links):
            if link.output == output and link.input == input:
                self.scene().removeItem(link.lineItem)
                self.__links.remove(link)
                break
        else:
            raise ValueError("No such link {0.name!r} -> {1.name!r}." \
                             .format(output, input))

    def clearLinks(self):
        """
        Clear (remove) all the links.
        """
        for output, input, _ in list(self.__links):
            self.removeLink(output, input)

    def __updateState(self):
        """
        Update the widget with the new source/sink node signal descriptions.
        """
        widget = QGraphicsWidget()
        widget.setLayout(QGraphicsGridLayout())

        # Space between left and right anchors
        widget.layout().setHorizontalSpacing(50)

        left_node = EditLinksNode(self,
                                  direction=Qt.LeftToRight,
                                  node=self.source)

        left_node.setSizePolicy(QSizePolicy.MinimumExpanding,
                                QSizePolicy.MinimumExpanding)

        right_node = EditLinksNode(self,
                                   direction=Qt.RightToLeft,
                                   node=self.sink)

        right_node.setSizePolicy(QSizePolicy.MinimumExpanding,
                                 QSizePolicy.MinimumExpanding)

        left_node.setMinimumWidth(150)
        right_node.setMinimumWidth(150)

        widget.layout().addItem(
            left_node,
            0,
            0,
        )
        widget.layout().addItem(
            right_node,
            0,
            1,
        )

        title_template = "<center><b>{0}<b></center>"

        left_title = GraphicsTextWidget(self)
        left_title.setHtml(title_template.format(escape(self.source.title)))
        left_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        right_title = GraphicsTextWidget(self)
        right_title.setHtml(title_template.format(escape(self.sink.title)))
        right_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        widget.layout().addItem(left_title,
                                1,
                                0,
                                alignment=Qt.AlignHCenter | Qt.AlignTop)
        widget.layout().addItem(right_title,
                                1,
                                1,
                                alignment=Qt.AlignHCenter | Qt.AlignTop)

        widget.setParentItem(self)

        max_w = max(
            left_node.sizeHint(Qt.PreferredSize).width(),
            right_node.sizeHint(Qt.PreferredSize).width())

        # fix same size
        left_node.setMinimumWidth(max_w)
        right_node.setMinimumWidth(max_w)
        left_title.setMinimumWidth(max_w)
        right_title.setMinimumWidth(max_w)

        self.layout().addItem(widget)
        self.layout().activate()

        self.sourceNodeWidget = left_node
        self.sinkNodeWidget = right_node
        self.sourceNodeTitle = left_title
        self.sinkNodeTitle = right_title

    if QT_VERSION < 0x40700:
        geometryChanged = Signal()

        def setGeometry(self, rect):
            QGraphicsWidget.setGeometry(self, rect)
            self.geometryChanged.emit()
Exemplo n.º 27
0
class VizRankDialogAttrPair(VizRankDialog):
    """
    VizRank dialog for pairs of attributes. The class provides most of the
    needed methods, except for `initialize` which is expected to store a
    list of `Variable` instances to `self.attrs`, and method
    `compute_score(state)` for scoring the combinations.

    The state is a pair of indices into `self.attrs`.

    When the user selects a pair, the dialog emits signal `selectionChanged`
    with a tuple of variables as parameter.
    """

    pairSelected = Signal(Variable, Variable)
    _AttrRole = next(gui.OrangeUserRole)

    def __init__(self, master):
        VizRankDialog.__init__(self, master)
        self.resize(320, 512)
        self.attrs = []
        manual_change_signal = getattr(master, "xy_changed_manually", None)
        if manual_change_signal:
            manual_change_signal.connect(self.on_manual_change)

    def sizeHint(self):
        """Assuming two columns in the table, return `QSize(320, 512)` as
        a reasonable default size."""
        return QSize(320, 512)

    def check_preconditions(self):
        """Refuse ranking if there are less than two feature or instances."""
        can_rank = self.master.data is not None and \
            len(self.master.data.domain.attributes) >= 2 and \
            len(self.master.data) >= 2
        self.Information.nothing_to_rank(shown=not can_rank)
        return can_rank

    def on_selection_changed(self, selected, deselected):
        selection = selected.indexes()
        if not selection:
            return
        attrs = selected.indexes()[0].data(self._AttrRole)
        self.selectionChanged.emit(attrs)

    def on_manual_change(self, attr1, attr2):
        model = self.rank_model
        self.rank_table.selectionModel().clear()
        for row in range(model.rowCount()):
            a1, a2 = model.data(model.index(row, 0), self._AttrRole)
            if a1 is attr1 and a2 is attr2:
                self.rank_table.selectRow(row)
                return

    def state_count(self):
        n_attrs = len(self.attrs)
        return n_attrs * (n_attrs - 1) / 2

    def iterate_states(self, initial_state):
        si, sj = initial_state or (0, 0)
        for i in range(si, len(self.attrs)):
            for j in range(sj, i):
                yield i, j
            sj = 0

    def row_for_state(self, score, state):
        attrs = sorted((self.attrs[x] for x in state), key=attrgetter("name"))
        item = QStandardItem(", ".join(a.name for a in attrs))
        item.setData(attrs, self._AttrRole)
        return [item]
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()

    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.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)
        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):
        def createHandle(self):
            """Create splitter handle"""
            return self._Handle(self.orientation(),
                                self,
                                cursor=Qt.PointingHandCursor)

        class _Handle(QSplitterHandle):
            def mouseReleaseEvent(self, event):
                """Resize on left button"""
                if event.button() == Qt.LeftButton:
                    splitter = self.splitter()
                    splitter.setSizes([int(splitter.sizes()[0] == 0), 100000])
                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)
        if self.graphButton is not None:
            self.buttonsArea.layout().addWidget(self.graphButton)
        if self.report_button is not None:
            self.buttonsArea.layout().addWidget(self.report_button)

    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)

            self.message_bar = MessagesWidget(self)
            self.message_bar.setSizePolicy(QSizePolicy.Preferred,
                                           QSizePolicy.Preferred)
            pb = QProgressBar(maximumWidth=120, minimum=0, maximum=100)
            pb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored)
            pb.setAttribute(Qt.WA_LayoutUsesWidgetRect)
            pb.setAttribute(Qt.WA_MacMiniSize)
            pb.hide()
            sb.addPermanentWidget(pb)
            sb.addPermanentWidget(self.message_bar)

            def statechanged():
                pb.setVisible(bool(self.processingState) or self.isBlocking())
                if self.isBlocking() and not self.processingState:
                    pb.setRange(0, 0)  # indeterminate pb
                elif self.processingState:
                    pb.setRange(0, 100)  # determinate pb

            self.processingStateChanged.connect(statechanged)
            self.blockingStateChanged.connect(statechanged)

            @self.progressBarValueChanged.connect
            def _(val):
                pb.setValue(int(val))

            # Reserve the bottom margins for the status bar
            margins = self.layout().contentsMargins()
            margins.setBottom(sb.sizeHint().height())
            self.setContentsMargins(margins)

    def save_graph(self):
        """Save the graph with the name given in class attribute `graph_name`.

        The method is called by the *Save graph* button, which is created
        automatically if the `graph_name` is defined.
        """
        graph_obj = getdeepattr(self, self.graph_name, None)
        if graph_obj is None:
            return
        saveplot.save_plot(graph_obj, self.graph_writers)

    def copy_to_clipboard(self):
        if self.graph_name:
            graph_obj = getdeepattr(self, self.graph_name, None)
            if graph_obj is None:
                return
            ClipboardFormat.write_image(None, graph_obj)

    def __restoreWidgetGeometry(self):
        def _fullscreen_to_maximized(geometry):
            """Don't restore windows into full screen mode because it loses
            decorations and can't be de-fullscreened at least on some platforms.
            Use Maximized state insted."""
            w = QWidget(visible=False)
            w.restoreGeometry(QByteArray(geometry))
            if w.isFullScreen():
                w.setWindowState(w.windowState() & ~Qt.WindowFullScreen
                                 | Qt.WindowMaximized)
            return w.saveGeometry()

        restored = False
        if self.save_position:
            geometry = self.savedWidgetGeometry
            if geometry is not None:
                geometry = _fullscreen_to_maximized(geometry)
                restored = self.restoreGeometry(geometry)

            if restored and not self.windowState() & \
                    (Qt.WindowMaximized | Qt.WindowFullScreen):
                space = QApplication.desktop().availableGeometry(self)
                frame, geometry = self.frameGeometry(), self.geometry()

                #Fix the widget size to fit inside the available space
                width = space.width() - (frame.width() - geometry.width())
                width = min(width, geometry.width())
                height = space.height() - (frame.height() - geometry.height())
                height = min(height, geometry.height())
                self.resize(width, height)

                # Move the widget to the center of available space if it is
                # currently outside it
                if not space.contains(self.frameGeometry()):
                    x = max(0, space.width() / 2 - width / 2)
                    y = max(0, space.height() / 2 - height / 2)

                    self.move(x, y)

        # Mark as explicitly moved/resized if not already. QDialog would
        # otherwise adjust position/size on subsequent hide/show
        # (move/resize events coming from the window manager do not set
        # these flags).
        if not self.testAttribute(Qt.WA_Moved):
            self.setAttribute(Qt.WA_Moved)
        if not self.testAttribute(Qt.WA_Resized):
            self.setAttribute(Qt.WA_Resized)

        return restored

    def __updateSavedGeometry(self):
        if self.__was_restored and self.isVisible():
            # Update the saved geometry only between explicit show/hide
            # events (i.e. changes initiated by the user not by Qt's default
            # window management).
            # Note: This should always be stored as bytes and not QByteArray.
            self.savedWidgetGeometry = bytes(self.saveGeometry())

    # when widget is resized, save the new width and height
    def resizeEvent(self, event):
        """Overloaded to save the geometry (width and height) when the widget
        is resized.
        """
        QDialog.resizeEvent(self, event)
        # Don't store geometry if the widget is not visible
        # (the widget receives a resizeEvent (with the default sizeHint)
        # before first showEvent and we must not overwrite the the
        # savedGeometry with it)
        if self.save_position and self.isVisible():
            self.__updateSavedGeometry()

    def moveEvent(self, event):
        """Overloaded to save the geometry when the widget is moved
        """
        QDialog.moveEvent(self, event)
        if self.save_position and self.isVisible():
            self.__updateSavedGeometry()

    def hideEvent(self, event):
        """Overloaded to save the geometry when the widget is hidden
        """
        if self.save_position:
            self.__updateSavedGeometry()
        QDialog.hideEvent(self, event)

    def closeEvent(self, event):
        """Overloaded to save the geometry when the widget is closed
        """
        if self.save_position and self.isVisible():
            self.__updateSavedGeometry()
        QDialog.closeEvent(self, event)

    def showEvent(self, event):
        """Overloaded to restore the geometry when the widget is shown
        """
        QDialog.showEvent(self, event)
        if self.save_position and not self.__was_restored:
            # Restore saved geometry on (first) show
            self.__restoreWidgetGeometry()
            self.__was_restored = True
        self.__quicktipOnce()

    def wheelEvent(self, event):
        """Silently accept the wheel event.

        This is to ensure combo boxes and other controls that have focus
        don't receive this event unless the cursor is over them.
        """
        event.accept()

    def setCaption(self, caption):
        # save caption title in case progressbar will change it
        self.captionTitle = str(caption)
        self.setWindowTitle(caption)

    def reshow(self):
        """Put the widget on top of all windows
        """
        self.show()
        self.raise_()
        self.activateWindow()

    def openContext(self, *a):
        """Open a new context corresponding to the given data.

        The settings handler first checks the stored context for a
        suitable match. If one is found, it becomes the current contexts and
        the widgets settings are initialized accordingly. If no suitable
        context exists, a new context is created and data is copied from
        the widget's settings into the new context.

        Widgets that have context settings must call this method after
        reinitializing the user interface (e.g. combo boxes) with the new
        data.

        The arguments given to this method are passed to the context handler.
        Their type depends upon the handler. For instance,
        `DomainContextHandler` expects `Orange.data.Table` or
        `Orange.data.Domain`.
        """
        self.contextAboutToBeOpened.emit(a)
        self.settingsHandler.open_context(self, *a)
        self.contextOpened.emit()

    def closeContext(self):
        """Save the current settings and close the current context.

        Widgets that have context settings must call this method before
        reinitializing the user interface (e.g. combo boxes) with the new
        data.
        """
        self.settingsHandler.close_context(self)
        self.contextClosed.emit()

    def retrieveSpecificSettings(self):
        """
        Retrieve data that is not registered as setting.

        This method is called by
        `Orange.widgets.settings.ContextHandler.settings_to_widget`.
        Widgets may define it to retrieve any data that is not stored in widget
        attributes. See :obj:`Orange.widgets.data.owcolor.OWColor` for an
        example.
        """
        pass

    def storeSpecificSettings(self):
        """
        Store data that is not registered as setting.

        This method is called by
        `Orange.widgets.settings.ContextHandler.settings_from_widget`.
        Widgets may define it to store any data that is not stored in widget
        attributes. See :obj:`Orange.widgets.data.owcolor.OWColor` for an
        example.
        """
        pass

    def saveSettings(self):
        """
        Writes widget instance's settings to class defaults. Usually called
        when the widget is deleted.
        """
        self.settingsHandler.update_defaults(self)

    def onDeleteWidget(self):
        """
        Invoked by the canvas to notify the widget it has been deleted
        from the workflow.

        If possible, subclasses should gracefully cancel any currently
        executing tasks.
        """
        pass

    def handleNewSignals(self):
        """
        Invoked by the workflow signal propagation manager after all
        signals handlers have been called.

        Reimplement this method in order to coalesce updates from
        multiple updated inputs.
        """
        pass

    #: Widget's status message has changed.
    statusMessageChanged = Signal(str)

    def setStatusMessage(self, text):
        """
        Set widget's status message.

        This is a short status string to be displayed inline next to
        the instantiated widget icon in the canvas.
        """
        if self.__statusMessage != text:
            self.__statusMessage = text
            self.statusMessageChanged.emit(text)

    def statusMessage(self):
        """
        Return the widget's status message.
        """
        return self.__statusMessage

    def keyPressEvent(self, e):
        """Handle default key actions or pass the event to the inherited method
        """
        if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions:
            OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self)
        else:
            QDialog.keyPressEvent(self, e)

    defaultKeyActions = {}

    if sys.platform == "darwin":
        defaultKeyActions = {
            (Qt.ControlModifier, Qt.Key_M):
            lambda self: self.showMaximized
            if self.isMinimized() else self.showMinimized(),
            (Qt.ControlModifier, Qt.Key_W):
            lambda self: self.setVisible(not self.isVisible())
        }

    def setBlocking(self, state=True):
        """
        Set blocking flag for this widget.

        While this flag is set this widget and all its descendants
        will not receive any new signals from the workflow signal manager.

        This is useful for instance if the widget does it's work in a
        separate thread or schedules processing from the event queue.
        In this case it can set the blocking flag in it's processNewSignals
        method schedule the task and return immediately. After the task
        has completed the widget can clear the flag and send the updated
        outputs.

        .. note::
            Failure to clear this flag will block dependent nodes forever.
        """
        if self.__blocking != state:
            self.__blocking = state
            self.blockingStateChanged.emit(state)

    def isBlocking(self):
        """Is this widget blocking signal processing."""
        return self.__blocking

    def resetSettings(self):
        """Reset the widget settings to default"""
        self.settingsHandler.reset_settings(self)

    def workflowEnv(self):
        """
        Return (a view to) the workflow runtime environment.

        Returns
        -------
        env : types.MappingProxyType
        """
        return self.__env

    def workflowEnvChanged(self, key, value, oldvalue):
        """
        A workflow environment variable `key` has changed to value.

        Called by the canvas framework to notify widget of a change
        in the workflow runtime environment.

        The default implementation does nothing.
        """
        pass

    def __showMessage(self, message):
        if self.__msgwidget is not None:
            self.__msgwidget.hide()
            self.__msgwidget.deleteLater()
            self.__msgwidget = None

        if message is None:
            return

        buttons = MessageOverlayWidget.Ok | MessageOverlayWidget.Close
        if message.moreurl is not None:
            buttons |= MessageOverlayWidget.Help

        if message.icon is not None:
            icon = message.icon
        else:
            icon = Message.Information

        self.__msgwidget = MessageOverlayWidget(parent=self,
                                                text=message.text,
                                                icon=icon,
                                                wordWrap=True,
                                                standardButtons=buttons)

        btn = self.__msgwidget.button(MessageOverlayWidget.Ok)
        btn.setText("Ok, got it")

        self.__msgwidget.setStyleSheet("""
            MessageOverlayWidget {
                background: qlineargradient(
                    x1: 0, y1: 0, x2: 0, y2: 1,
                    stop:0 #666, stop:0.3 #6D6D6D, stop:1 #666)
            }
            MessageOverlayWidget QLabel#text-label {
                color: white;
            }""")

        if message.moreurl is not None:
            helpbutton = self.__msgwidget.button(MessageOverlayWidget.Help)
            helpbutton.setText("Learn more\N{HORIZONTAL ELLIPSIS}")
            self.__msgwidget.helpRequested.connect(
                lambda: QDesktopServices.openUrl(QUrl(message.moreurl)))

        self.__msgwidget.setWidget(self)
        self.__msgwidget.show()

    def __quicktip(self):
        messages = list(self.UserAdviceMessages)
        if messages:
            message = messages[self.__msgchoice % len(messages)]
            self.__msgchoice += 1
            self.__showMessage(message)

    def __quicktipOnce(self):
        filename = os.path.join(settings.widget_settings_dir(),
                                "user-session-state.ini")
        namespace = (
            "user-message-history/{0.__module__}.{0.__qualname__}".format(
                type(self)))
        session_hist = QSettings(filename, QSettings.IniFormat)
        session_hist.beginGroup(namespace)
        messages = self.UserAdviceMessages

        def _ispending(msg):
            return not session_hist.value("{}/confirmed".format(
                msg.persistent_id),
                                          defaultValue=False,
                                          type=bool)

        messages = [msg for msg in messages if _ispending(msg)]

        if not messages:
            return

        message = messages[self.__msgchoice % len(messages)]
        self.__msgchoice += 1

        self.__showMessage(message)

        def _userconfirmed():
            session_hist = QSettings(filename, QSettings.IniFormat)
            session_hist.beginGroup(namespace)
            session_hist.setValue("{}/confirmed".format(message.persistent_id),
                                  True)
            session_hist.sync()

        self.__msgwidget.accepted.connect(_userconfirmed)

    @classmethod
    def migrate_settings(cls, settings, version):
        """Fix settings to work with the current version of widgets

        Parameters
        ----------
        settings : dict
            dict of name - value mappings
        version : Optional[int]
            version of the saved settings
            or None if settings were created before migrations
        """

    @classmethod
    def migrate_context(cls, context, version):
        """Fix contexts to work with the current version of widgets
class Histogram(pg.PlotWidget):
    """
    A histogram plot with interactive 'tail' selection
    """
    #: Emitted when the selection boundary has changed
    selectionChanged = Signal()
    #: Emitted when the selection boundary has been edited by the user
    #: (by dragging the boundary lines)
    selectionEdited = Signal()

    #: Selection mode
    NoSelection, Low, High, TwoSided, Middle = 0, 1, 2, 3, 4

    def __init__(self, parent=None, **kwargs):
        pg.PlotWidget.__init__(self, parent, **kwargs)

        self.getAxis("bottom").setLabel("Score")
        self.getAxis("left").setLabel("Counts")

        self.__data = None
        self.__histcurve = None

        self.__mode = Histogram.NoSelection
        self.__min = 0
        self.__max = 0

        def makeline(pos):
            pen = QPen(Qt.darkGray, 1)
            pen.setCosmetic(True)
            line = InfiniteLine(angle=90, pos=pos, pen=pen, movable=True)
            line.setCursor(Qt.SizeHorCursor)
            return line

        self.__cuthigh = makeline(self.__max)
        self.__cuthigh.sigPositionChanged.connect(self.__on_cuthigh_changed)
        self.__cuthigh.sigPositionChangeFinished.connect(self.selectionEdited)
        self.__cutlow = makeline(self.__min)
        self.__cutlow.sigPositionChanged.connect(self.__on_cutlow_changed)
        self.__cutlow.sigPositionChangeFinished.connect(self.selectionEdited)

        brush = pg.mkBrush((200, 200, 200, 180))
        self.__taillow = pg.PlotCurveItem(fillLevel=0,
                                          brush=brush,
                                          pen=QPen(Qt.NoPen))
        self.__taillow.setVisible(False)

        self.__tailhigh = pg.PlotCurveItem(fillLevel=0,
                                           brush=brush,
                                           pen=QPen(Qt.NoPen))
        self.__tailhigh.setVisible(False)

    def setData(self, hist, bins=None):
        """
        Set the histogram data
        """
        if bins is None:
            bins = np.arange(len(hist))

        self.__data = (hist, bins)
        if self.__histcurve is None:
            self.__histcurve = pg.PlotCurveItem(x=bins, y=hist, stepMode=True)
        else:
            self.__histcurve.setData(x=bins, y=hist, stepMode=True)

        self.__update()

    def setHistogramCurve(self, curveitem):
        """
        Set the histogram plot curve.
        """
        if self.__histcurve is curveitem:
            return

        if self.__histcurve is not None:
            self.removeItem(self.__histcurve)
            self.__histcurve = None
            self.__data = None

        if curveitem is not None:
            if not curveitem.opts["stepMode"]:
                raise ValueError("The curve must have `stepMode == True`")
            self.addItem(curveitem)
            self.__histcurve = curveitem
            self.__data = (curveitem.yData, curveitem.xData)

        self.__update()

    def histogramCurve(self):
        """
        Return the histogram plot curve.
        """
        return self.__histcurve

    def setSelectionMode(self, mode):
        """
        Set selection mode
        """
        if self.__mode != mode:
            self.__mode = mode
            self.__update_cutlines()
            self.__update_tails()

    def setLower(self, value):
        """
        Set the lower boundary value.
        """
        if self.__min != value:
            self.__min = value
            self.__update_cutlines()
            self.__update_tails()
            self.selectionChanged.emit()

    def setUpper(self, value):
        """
        Set the upper boundary value.
        """
        if self.__max != value:
            self.__max = value
            self.__update_cutlines()
            self.__update_tails()
            self.selectionChanged.emit()

    def setBoundary(self, lower, upper):
        """
        Set lower and upper boundary value.
        """
        changed = False
        if self.__min != lower:
            self.__min = lower
            changed = True

        if self.__max != upper:
            self.__max = upper
            changed = True

        if changed:
            self.__update_cutlines()
            self.__update_tails()
            self.selectionChanged.emit()

    def boundary(self):
        """
        Return the lower and upper boundary values.
        """
        return (self.__min, self.__max)

    def clear(self):
        """
        Clear the plot.
        """
        self.__data = None
        self.__histcurve = None
        super().clear()

    def __update(self):
        def additem(item):
            if item.scene() is not self.scene():
                self.addItem(item)

        def removeitem(item):
            if item.scene() is self.scene():
                self.removeItem(item)

        if self.__data is not None:
            additem(self.__cuthigh)
            additem(self.__cutlow)
            additem(self.__tailhigh)
            additem(self.__taillow)

            _, edges = self.__data
            # Update the allowable cutoff line bounds
            minx, maxx = np.min(edges), np.max(edges)
            span = maxx - minx
            bounds = minx - span * 0.005, maxx + span * 0.005

            self.__cuthigh.setBounds(bounds)
            self.__cutlow.setBounds(bounds)

            self.__update_cutlines()
            self.__update_tails()
        else:
            removeitem(self.__cuthigh)
            removeitem(self.__cutlow)
            removeitem(self.__tailhigh)
            removeitem(self.__taillow)

    def __update_cutlines(self):
        self.__cuthigh.setVisible(self.__mode & Histogram.High)
        self.__cuthigh.setValue(self.__max)
        self.__cutlow.setVisible(self.__mode & Histogram.Low)
        self.__cutlow.setValue(self.__min)

    def __update_tails(self):
        if self.__mode == Histogram.NoSelection:
            return
        if self.__data is None:
            return

        hist, edges = self.__data

        self.__taillow.setVisible(self.__mode & Histogram.Low)
        if self.__min > edges[0]:
            datalow = histogram_cut(hist, edges, edges[0], self.__min)
            self.__taillow.setData(*datalow, fillLevel=0, stepMode=True)
        else:
            self.__taillow.clear()

        self.__tailhigh.setVisible(self.__mode & Histogram.High)
        if self.__max < edges[-1]:
            datahigh = histogram_cut(hist, edges, self.__max, edges[-1])
            self.__tailhigh.setData(*datahigh, fillLevel=0, stepMode=True)
        else:
            self.__tailhigh.clear()

    def __on_cuthigh_changed(self):
        self.setUpper(self.__cuthigh.value())

    def __on_cutlow_changed(self):
        self.setLower(self.__cutlow.value())
Exemplo n.º 30
0
class CanvasScene(QGraphicsScene):
    """
    A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance.
    """

    #: Signal emitted when a :class:`NodeItem` has been added to the scene.
    node_item_added = Signal(object)

    #: Signal emitted when a :class:`NodeItem` has been removed from the
    #: scene.
    node_item_removed = Signal(object)

    #: Signal emitted when a new :class:`LinkItem` has been added to the
    #: scene.
    link_item_added = Signal(object)

    #: Signal emitted when a :class:`LinkItem` has been removed.
    link_item_removed = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been added.
    annotation_added = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been removed.
    annotation_removed = Signal(object)

    #: Signal emitted when the position of a :class:`NodeItem` has changed.
    node_item_position_changed = Signal(object, QPointF)

    #: Signal emitted when an :class:`NodeItem` has been double clicked.
    node_item_double_clicked = Signal(object)

    #: An node item has been activated (clicked)
    node_item_activated = Signal(object)

    #: An node item has been hovered
    node_item_hovered = Signal(object)

    #: Link item has been hovered
    link_item_hovered = Signal(object)

    def __init__(self, *args, **kwargs):
        QGraphicsScene.__init__(self, *args, **kwargs)

        self.scheme = None
        self.registry = None

        # All node items
        self.__node_items = []
        # Mapping from SchemeNodes to canvas items
        self.__item_for_node = {}
        # All link items
        self.__link_items = []
        # Mapping from SchemeLinks to canvas items.
        self.__item_for_link = {}

        # All annotation items
        self.__annotation_items = []
        # Mapping from SchemeAnnotations to canvas items.
        self.__item_for_annotation = {}

        # Is the scene editable
        self.editable = True

        # Anchor Layout
        self.__anchor_layout = AnchorLayout()
        self.addItem(self.__anchor_layout)

        self.__channel_names_visible = True
        self.__node_animation_enabled = True

        self.user_interaction_handler = None

        self.activated_mapper = QSignalMapper(self)
        self.activated_mapper.mapped[QObject].connect(
            lambda node: self.node_item_activated.emit(node))
        self.hovered_mapper = QSignalMapper(self)
        self.hovered_mapper.mapped[QObject].connect(
            lambda node: self.node_item_hovered.emit(node))
        self.position_change_mapper = QSignalMapper(self)
        self.position_change_mapper.mapped[QObject].connect(
            self._on_position_change)
        log.info("'%s' intitialized." % self)

    def clear_scene(self):
        """
        Clear (reset) the scene.
        """
        if self.scheme is not None:
            self.scheme.node_added.disconnect(self.add_node)
            self.scheme.node_removed.disconnect(self.remove_node)

            self.scheme.link_added.disconnect(self.add_link)
            self.scheme.link_removed.disconnect(self.remove_link)

            self.scheme.annotation_added.disconnect(self.add_annotation)
            self.scheme.annotation_removed.disconnect(self.remove_annotation)

            self.scheme.node_state_changed.disconnect(
                self.on_widget_state_change)
            self.scheme.channel_state_changed.disconnect(
                self.on_link_state_change)

            # Remove all items to make sure all signals from scheme items
            # to canvas items are disconnected.

            for annot in self.scheme.annotations:
                if annot in self.__item_for_annotation:
                    self.remove_annotation(annot)

            for link in self.scheme.links:
                if link in self.__item_for_link:
                    self.remove_link(link)

            for node in self.scheme.nodes:
                if node in self.__item_for_node:
                    self.remove_node(node)

        self.scheme = None
        self.__node_items = []
        self.__item_for_node = {}
        self.__link_items = []
        self.__item_for_link = {}
        self.__annotation_items = []
        self.__item_for_annotation = {}

        self.__anchor_layout.deleteLater()

        self.user_interaction_handler = None

        self.clear()
        log.info("'%s' cleared." % self)

    def set_scheme(self, scheme):
        """
        Set the scheme to display. Populates the scene with nodes and links
        already in the scheme. Any further change to the scheme will be
        reflected in the scene.

        Parameters
        ----------
        scheme : :class:`~.scheme.Scheme`

        """
        if self.scheme is not None:
            # Clear the old scheme
            self.clear_scene()

        log.info("Setting scheme '%s' on '%s'" % (scheme, self))

        self.scheme = scheme
        if self.scheme is not None:
            self.scheme.node_added.connect(self.add_node)
            self.scheme.node_removed.connect(self.remove_node)

            self.scheme.link_added.connect(self.add_link)
            self.scheme.link_removed.connect(self.remove_link)

            self.scheme.annotation_added.connect(self.add_annotation)
            self.scheme.annotation_removed.connect(self.remove_annotation)

            self.scheme.node_state_changed.connect(self.on_widget_state_change)
            self.scheme.channel_state_changed.connect(
                self.on_link_state_change)

            self.scheme.topology_changed.connect(self.on_scheme_change)

        for node in scheme.nodes:
            self.add_node(node)

        for link in scheme.links:
            self.add_link(link)

        for annot in scheme.annotations:
            self.add_annotation(annot)

    def set_registry(self, registry):
        """
        Set the widget registry.
        """
        # TODO: Remove/Deprecate. Is used only to get the category/background
        # color. That should be part of the SchemeNode/WidgetDescription.
        log.info("Setting registry '%s on '%s'." % (registry, self))
        self.registry = registry

    def set_anchor_layout(self, layout):
        """
        Set an :class:`~.layout.AnchorLayout`
        """
        if self.__anchor_layout != layout:
            if self.__anchor_layout:
                self.__anchor_layout.deleteLater()
                self.__anchor_layout = None

            self.__anchor_layout = layout

    def anchor_layout(self):
        """
        Return the anchor layout instance.
        """
        return self.__anchor_layout

    def set_channel_names_visible(self, visible):
        """
        Set the channel names visibility.
        """
        self.__channel_names_visible = visible
        for link in self.__link_items:
            link.setChannelNamesVisible(visible)

    def channel_names_visible(self):
        """
        Return the channel names visibility state.
        """
        return self.__channel_names_visible

    def set_node_animation_enabled(self, enabled):
        """
        Set node animation enabled state.
        """
        if self.__node_animation_enabled != enabled:
            self.__node_animation_enabled = enabled

            for node in self.__node_items:
                node.setAnimationEnabled(enabled)

    def add_node_item(self, item):
        """
        Add a :class:`.NodeItem` instance to the scene.
        """
        if item in self.__node_items:
            raise ValueError("%r is already in the scene." % item)

        if item.pos().isNull():
            if self.__node_items:
                pos = self.__node_items[-1].pos() + QPointF(150, 0)
            else:
                pos = QPointF(150, 150)

            item.setPos(pos)

        item.setFont(self.font())

        # Set signal mappings
        self.activated_mapper.setMapping(item, item)
        item.activated.connect(self.activated_mapper.map)

        self.hovered_mapper.setMapping(item, item)
        item.hovered.connect(self.hovered_mapper.map)

        self.position_change_mapper.setMapping(item, item)
        item.positionChanged.connect(self.position_change_mapper.map)

        self.addItem(item)

        self.__node_items.append(item)

        self.node_item_added.emit(item)

        log.info("Added item '%s' to '%s'" % (item, self))
        return item

    def add_node(self, node):
        """
        Add and return a default constructed :class:`.NodeItem` for a
        :class:`SchemeNode` instance `node`. If the `node` is already in
        the scene do nothing and just return its item.

        """
        if node in self.__item_for_node:
            # Already added
            return self.__item_for_node[node]

        item = self.new_node_item(node.description)

        if node.position:
            pos = QPointF(*node.position)
            item.setPos(pos)

        item.setTitle(node.title)
        item.setProcessingState(node.processing_state)
        item.setProgress(node.progress)

        for message in node.state_messages():
            item.setStateMessage(message)

        item.setStatusMessage(node.status_message())

        self.__item_for_node[node] = item

        node.position_changed.connect(self.__on_node_pos_changed)
        node.title_changed.connect(item.setTitle)
        node.progress_changed.connect(item.setProgress)
        node.processing_state_changed.connect(item.setProcessingState)
        node.state_message_changed.connect(item.setStateMessage)
        node.status_message_changed.connect(item.setStatusMessage)

        return self.add_node_item(item)

    def new_node_item(self, widget_desc, category_desc=None):
        """
        Construct an new :class:`.NodeItem` from a `WidgetDescription`.
        Optionally also set `CategoryDescription`.

        """
        item = items.NodeItem()
        item.setWidgetDescription(widget_desc)

        if category_desc is None and self.registry and widget_desc.category:
            category_desc = self.registry.category(widget_desc.category)

        if category_desc is None and self.registry is not None:
            try:
                category_desc = self.registry.category(widget_desc.category)
            except KeyError:
                pass

        if category_desc is not None:
            item.setWidgetCategory(category_desc)

        item.setAnimationEnabled(self.__node_animation_enabled)
        return item

    def remove_node_item(self, item):
        """
        Remove `item` (:class:`.NodeItem`) from the scene.
        """
        self.activated_mapper.removeMappings(item)
        self.hovered_mapper.removeMappings(item)
        self.position_change_mapper.removeMappings(item)

        item.hide()
        self.removeItem(item)
        self.__node_items.remove(item)

        self.node_item_removed.emit(item)

        log.info("Removed item '%s' from '%s'" % (item, self))

    def remove_node(self, node):
        """
        Remove the :class:`.NodeItem` instance that was previously
        constructed for a :class:`SchemeNode` `node` using the `add_node`
        method.

        """
        item = self.__item_for_node.pop(node)

        node.position_changed.disconnect(self.__on_node_pos_changed)
        node.title_changed.disconnect(item.setTitle)
        node.progress_changed.disconnect(item.setProgress)
        node.processing_state_changed.disconnect(item.setProcessingState)
        node.state_message_changed.disconnect(item.setStateMessage)

        self.remove_node_item(item)

    def node_items(self):
        """
        Return all :class:`.NodeItem` instances in the scene.
        """
        return list(self.__node_items)

    def add_link_item(self, item):
        """
        Add a link (:class:`.LinkItem`) to the scene.
        """
        if item.scene() is not self:
            self.addItem(item)

        item.setFont(self.font())
        self.__link_items.append(item)

        self.link_item_added.emit(item)

        log.info("Added link %r -> %r to '%s'" %
                 (item.sourceItem.title(), item.sinkItem.title(), self))

        self.__anchor_layout.invalidateLink(item)

        return item

    def add_link(self, scheme_link):
        """
        Create and add a :class:`.LinkItem` instance for a
        :class:`SchemeLink` instance. If the link is already in the scene
        do nothing and just return its :class:`.LinkItem`.

        """
        if scheme_link in self.__item_for_link:
            return self.__item_for_link[scheme_link]

        source = self.__item_for_node[scheme_link.source_node]
        sink = self.__item_for_node[scheme_link.sink_node]

        item = self.new_link_item(source, scheme_link.source_channel, sink,
                                  scheme_link.sink_channel)

        item.setEnabled(scheme_link.enabled)
        scheme_link.enabled_changed.connect(item.setEnabled)

        if scheme_link.is_dynamic():
            item.setDynamic(True)
            item.setDynamicEnabled(scheme_link.dynamic_enabled)
            scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled)

        item.setRuntimeState(scheme_link.runtime_state())
        scheme_link.state_changed.connect(item.setRuntimeState)

        self.add_link_item(item)
        self.__item_for_link[scheme_link] = item
        return item

    def new_link_item(self, source_item, source_channel, sink_item,
                      sink_channel):
        """
        Construct and return a new :class:`.LinkItem`
        """
        item = items.LinkItem()
        item.setSourceItem(source_item)
        item.setSinkItem(sink_item)

        def channel_name(channel):
            if isinstance(channel, str):
                return channel
            else:
                return channel.name

        source_name = channel_name(source_channel)
        sink_name = channel_name(sink_channel)

        fmt = "<b>{0}</b>&nbsp; \u2192 &nbsp;<b>{1}</b>"
        item.setToolTip(fmt.format(escape(source_name), escape(sink_name)))

        item.setSourceName(source_name)
        item.setSinkName(sink_name)
        item.setChannelNamesVisible(self.__channel_names_visible)

        return item

    def remove_link_item(self, item):
        """
        Remove a link (:class:`.LinkItem`) from the scene.
        """
        # Invalidate the anchor layout.
        self.__anchor_layout.invalidateAnchorItem(
            item.sourceItem.outputAnchorItem)
        self.__anchor_layout.invalidateAnchorItem(
            item.sinkItem.inputAnchorItem)

        self.__link_items.remove(item)

        # Remove the anchor points.
        item.removeLink()
        self.removeItem(item)

        self.link_item_removed.emit(item)

        log.info("Removed link '%s' from '%s'" % (item, self))

        return item

    def remove_link(self, scheme_link):
        """
        Remove a :class:`.LinkItem` instance that was previously constructed
        for a :class:`SchemeLink` instance `link` using the `add_link` method.

        """
        item = self.__item_for_link.pop(scheme_link)
        scheme_link.enabled_changed.disconnect(item.setEnabled)

        if scheme_link.is_dynamic():
            scheme_link.dynamic_enabled_changed.disconnect(
                item.setDynamicEnabled)
        scheme_link.state_changed.disconnect(item.setRuntimeState)
        self.remove_link_item(item)

    def link_items(self):
        """
        Return all :class:`.LinkItem`\s in the scene.
        """
        return list(self.__link_items)

    def add_annotation_item(self, annotation):
        """
        Add an :class:`.Annotation` item to the scene.
        """
        self.__annotation_items.append(annotation)
        self.addItem(annotation)
        self.annotation_added.emit(annotation)
        return annotation

    def add_annotation(self, scheme_annot):
        """
        Create a new item for :class:`SchemeAnnotation` and add it
        to the scene. If the `scheme_annot` is already in the scene do
        nothing and just return its item.

        """
        if scheme_annot in self.__item_for_annotation:
            # Already added
            return self.__item_for_annotation[scheme_annot]

        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
            item = items.TextAnnotation()
            x, y, w, h = scheme_annot.rect
            item.setPos(x, y)
            item.resize(w, h)
            item.setTextInteractionFlags(Qt.TextEditorInteraction)

            font = font_from_dict(scheme_annot.font, item.font())
            item.setFont(font)
            item.setContent(scheme_annot.content, scheme_annot.content_type)
            scheme_annot.content_changed.connect(item.setContent)
        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
            item = items.ArrowAnnotation()
            start, end = scheme_annot.start_pos, scheme_annot.end_pos
            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
            item.setColor(QColor(scheme_annot.color))

        scheme_annot.geometry_changed.connect(
            self.__on_scheme_annot_geometry_change)

        self.add_annotation_item(item)
        self.__item_for_annotation[scheme_annot] = item

        return item

    def remove_annotation_item(self, annotation):
        """
        Remove an :class:`.Annotation` instance from the scene.

        """
        self.__annotation_items.remove(annotation)
        self.removeItem(annotation)
        self.annotation_removed.emit(annotation)

    def remove_annotation(self, scheme_annotation):
        """
        Remove an :class:`.Annotation` instance that was previously added
        using :func:`add_anotation`.

        """
        item = self.__item_for_annotation.pop(scheme_annotation)

        scheme_annotation.geometry_changed.disconnect(
            self.__on_scheme_annot_geometry_change)

        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
            scheme_annotation.content_changed.disconnect(item.setContent)
        self.remove_annotation_item(item)

    def annotation_items(self):
        """
        Return all :class:`.Annotation` items in the scene.
        """
        return self.__annotation_items

    def item_for_annotation(self, scheme_annotation):
        return self.__item_for_annotation[scheme_annotation]

    def annotation_for_item(self, item):
        rev = dict(
            reversed(item) for item in self.__item_for_annotation.items())
        return rev[item]

    def commit_scheme_node(self, node):
        """
        Commit the `node` into the scheme.
        """
        if not self.editable:
            raise Exception("Scheme not editable.")

        if node not in self.__item_for_node:
            raise ValueError("No 'NodeItem' for node.")

        item = self.__item_for_node[node]

        try:
            self.scheme.add_node(node)
        except Exception:
            log.error("An error occurred while committing node '%s'",
                      node,
                      exc_info=True)
            # Cleanup (remove the node item)
            self.remove_node_item(item)
            raise

        log.info("Commited node '%s' from '%s' to '%s'" %
                 (node, self, self.scheme))

    def commit_scheme_link(self, link):
        """
        Commit a scheme link.
        """
        if not self.editable:
            raise Exception("Scheme not editable")

        if link not in self.__item_for_link:
            raise ValueError("No 'LinkItem' for link.")

        self.scheme.add_link(link)
        log.info("Commited link '%s' from '%s' to '%s'" %
                 (link, self, self.scheme))

    def node_for_item(self, item):
        """
        Return the `SchemeNode` for the `item`.
        """
        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
        return rev[item]

    def item_for_node(self, node):
        """
        Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
        """
        return self.__item_for_node[node]

    def link_for_item(self, item):
        """
        Return the `SchemeLink for `item` (:class:`LinkItem`).
        """
        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
        return rev[item]

    def item_for_link(self, link):
        """
        Return the :class:`LinkItem` for a :class:`SchemeLink`
        """
        return self.__item_for_link[link]

    def selected_node_items(self):
        """
        Return the selected :class:`NodeItem`'s.
        """
        return [item for item in self.__node_items if item.isSelected()]

    def selected_annotation_items(self):
        """
        Return the selected :class:`Annotation`'s
        """
        return [item for item in self.__annotation_items if item.isSelected()]

    def node_links(self, node_item):
        """
        Return all links from the `node_item` (:class:`NodeItem`).
        """
        return self.node_output_links(node_item) + self.node_input_links(
            node_item)

    def node_output_links(self, node_item):
        """
        Return a list of all output links from `node_item`.
        """
        return [
            link for link in self.__link_items if link.sourceItem == node_item
        ]

    def node_input_links(self, node_item):
        """
        Return a list of all input links for `node_item`.
        """
        return [
            link for link in self.__link_items if link.sinkItem == node_item
        ]

    def neighbor_nodes(self, node_item):
        """
        Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
        """
        neighbors = list(
            map(attrgetter("sourceItem"), self.node_input_links(node_item)))

        neighbors.extend(
            map(attrgetter("sinkItem"), self.node_output_links(node_item)))
        return neighbors

    def on_widget_state_change(self, widget, state):
        pass

    def on_link_state_change(self, link, state):
        pass

    def on_scheme_change(self, ):
        pass

    def _on_position_change(self, item):
        # Invalidate the anchor point layout and schedule a layout.
        self.__anchor_layout.invalidateNode(item)

        self.node_item_position_changed.emit(item, item.pos())

    def __on_node_pos_changed(self, pos):
        node = self.sender()
        item = self.__item_for_node[node]
        item.setPos(*pos)

    def __on_scheme_annot_geometry_change(self):
        annot = self.sender()
        item = self.__item_for_annotation[annot]
        if isinstance(annot, scheme.SchemeTextAnnotation):
            item.setGeometry(QRectF(*annot.rect))
        elif isinstance(annot, scheme.SchemeArrowAnnotation):
            p1 = item.mapFromScene(QPointF(*annot.start_pos))
            p2 = item.mapFromScene(QPointF(*annot.end_pos))
            item.setLine(QLineF(p1, p2))
        else:
            pass

    def item_at(self, pos, type_or_tuple=None, buttons=0):
        """Return the item at `pos` that is an instance of the specified
        type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given
        only return the item if it is the top level item that would
        accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`).

        """
        rect = QRectF(pos, QSizeF(1, 1))
        items = self.items(rect)

        if buttons:
            items = itertools.dropwhile(
                lambda item: not item.acceptedMouseButtons() & buttons, items)
            items = list(items)[:1]

        if type_or_tuple:
            items = [i for i in items if isinstance(i, type_or_tuple)]

        return items[0] if items else None

    if USE_PYQT and PYQT_VERSION < 0x40900:
        # For QGraphicsObject subclasses items, itemAt ... return a
        # QGraphicsItem wrapper instance and not the actual class instance.
        def itemAt(self, *args, **kwargs):
            item = QGraphicsScene.itemAt(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def items(self, *args, **kwargs):
            items = QGraphicsScene.items(self, *args, **kwargs)
            return list(map(toGraphicsObjectIfPossible, items))

        def selectedItems(self, *args, **kwargs):
            return list(
                map(
                    toGraphicsObjectIfPossible,
                    QGraphicsScene.selectedItems(self, *args, **kwargs),
                ))

        def collidingItems(self, *args, **kwargs):
            return list(
                map(
                    toGraphicsObjectIfPossible,
                    QGraphicsScene.collidingItems(self, *args, **kwargs),
                ))

        def focusItem(self, *args, **kwargs):
            item = QGraphicsScene.focusItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def mouseGrabberItem(self, *args, **kwargs):
            item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

    def mousePressEvent(self, event):
        if (self.user_interaction_handler
                and self.user_interaction_handler.mousePressEvent(event)):
            return

        # Right (context) click on the node item. If the widget is not
        # in the current selection then select the widget (only the widget).
        # Else simply return and let customContextMenuRequested signal
        # handle it
        shape_item = self.item_at(event.scenePos(), items.NodeItem)
        if (shape_item and event.button() == Qt.RightButton
                and shape_item.flags() & QGraphicsItem.ItemIsSelectable):
            if not shape_item.isSelected():
                self.clearSelection()
                shape_item.setSelected(True)

        return QGraphicsScene.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if (self.user_interaction_handler
                and self.user_interaction_handler.mouseMoveEvent(event)):
            return

        return QGraphicsScene.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if (self.user_interaction_handler
                and self.user_interaction_handler.mouseReleaseEvent(event)):
            return
        return QGraphicsScene.mouseReleaseEvent(self, event)

    def mouseDoubleClickEvent(self, event):
        if (self.user_interaction_handler and
                self.user_interaction_handler.mouseDoubleClickEvent(event)):
            return

        return QGraphicsScene.mouseDoubleClickEvent(self, event)

    def keyPressEvent(self, event):
        if (self.user_interaction_handler
                and self.user_interaction_handler.keyPressEvent(event)):
            return
        return QGraphicsScene.keyPressEvent(self, event)

    def keyReleaseEvent(self, event):
        if (self.user_interaction_handler
                and self.user_interaction_handler.keyReleaseEvent(event)):
            return
        return QGraphicsScene.keyReleaseEvent(self, event)

    def contextMenuEvent(self, event):
        if (self.user_interaction_handler
                and self.user_interaction_handler.contextMenuEvent(event)):
            return
        super().contextMenuEvent(event)

    def set_user_interaction_handler(self, handler):
        if (self.user_interaction_handler
                and not self.user_interaction_handler.isFinished()):
            self.user_interaction_handler.cancel()

        log.info("Setting interaction '%s' to '%s'" % (handler, self))

        self.user_interaction_handler = handler
        if handler:
            handler.start()

    def __str__(self):
        return "%s(objectName=%r, ...)" % (type(self).__name__,
                                           str(self.objectName()))