Пример #1
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)

        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        # scale factor accumulating partial increments from wheel events
        self.__zoomLevel = 100
        # effective scale level(rounded to whole integers)
        self.__effectiveZoomLevel = 100

        self.__zoomInAction = QAction(
            self.tr("Zoom in"),
            self,
            objectName="action-zoom-in",
            shortcut=QKeySequence.ZoomIn,
            triggered=self.zoomIn,
        )

        self.__zoomOutAction = QAction(self.tr("Zoom out"),
                                       self,
                                       objectName="action-zoom-out",
                                       shortcut=QKeySequence.ZoomOut,
                                       triggered=self.zoomOut)
        self.__zoomResetAction = QAction(
            self.tr("Reset Zoom"),
            self,
            objectName="action-zoom-reset",
            triggered=self.zoomReset,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0))

    def setScene(self, scene):
        super().setScene(scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()
        return super().mouseReleaseEvent(event)

    def wheelEvent(self, event: QWheelEvent):
        if event.modifiers() & Qt.ControlModifier \
                and event.buttons() == Qt.NoButton:
            delta = event.angleDelta().y()
            # use mouse position as anchor while zooming
            anchor = self.transformationAnchor()
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120)
            self.setTransformationAnchor(anchor)
            event.accept()
        else:
            super().wheelEvent(event)

    def zoomIn(self):
        self.__setZoomLevel(self.__zoomLevel + 10)

    def zoomOut(self):
        self.__setZoomLevel(self.__zoomLevel - 10)

    def zoomReset(self):
        """
        Reset the zoom level.
        """
        self.__setZoomLevel(100)

    def zoomLevel(self):
        # type: () -> float
        """
        Return the current zoom level.

        Level is expressed in percentages; 100 is unscaled, 50 is half size, ...
        """
        return self.__effectiveZoomLevel

    def setZoomLevel(self, level):
        self.__setZoomLevel(level)

    def __setZoomLevel(self, scale):
        # type: (float) -> None
        self.__zoomLevel = max(30, min(scale, 300))
        scale = round(self.__zoomLevel)
        self.__zoomOutAction.setEnabled(scale != 30)
        self.__zoomInAction.setEnabled(scale != 300)
        if self.__effectiveZoomLevel != scale:
            self.__effectiveZoomLevel = scale
            transform = QTransform()
            transform.scale(scale / 100, scale / 100)
            self.setTransform(transform)
            self.zoomLevelChanged.emit(scale)

    zoomLevelChanged = Signal(float)
    zoomLevel_ = Property(float,
                          zoomLevel,
                          setZoomLevel,
                          notify=zoomLevelChanged)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        super().drawBackground(painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo(
                QSize(200, 200)))
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
Пример #2
0
class VariableEditor(QWidget):
    """An editor widget for a variable.

    Can edit the variable name, and its attributes dictionary.

    """
    variable_changed = Signal()

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.var = None
        self.setup_gui()

    def setup_gui(self):
        layout = QVBoxLayout()
        self.setLayout(layout)

        self.main_form = QFormLayout()
        self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        layout.addLayout(self.main_form)

        self._setup_gui_name()
        self._setup_gui_labels()

    def _setup_gui_name(self):
        self.name_edit = QLineEdit()
        self.main_form.addRow("Name:", self.name_edit)
        self.name_edit.editingFinished.connect(self.on_name_changed)

    def _setup_gui_labels(self):
        vlayout = QVBoxLayout()
        vlayout.setContentsMargins(0, 0, 0, 0)
        vlayout.setSpacing(1)

        self.labels_edit = QTreeView()
        self.labels_edit.setEditTriggers(QTreeView.CurrentChanged)
        self.labels_edit.setRootIsDecorated(False)

        self.labels_model = DictItemsModel()
        self.labels_edit.setModel(self.labels_model)

        self.labels_edit.selectionModel().selectionChanged.connect(
            self.on_label_selection_changed)

        # Necessary signals to know when the labels change
        self.labels_model.dataChanged.connect(self.on_labels_changed)
        self.labels_model.rowsInserted.connect(self.on_labels_changed)
        self.labels_model.rowsRemoved.connect(self.on_labels_changed)

        vlayout.addWidget(self.labels_edit)
        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        hlayout.setSpacing(1)
        self.add_label_action = QAction(
            "+", self,
            toolTip="Add a new label.",
            triggered=self.on_add_label,
            enabled=False,
            shortcut=QKeySequence(QKeySequence.New))

        self.remove_label_action = QAction(
            unicodedata.lookup("MINUS SIGN"), self,
            toolTip="Remove selected label.",
            triggered=self.on_remove_label,
            enabled=False,
            shortcut=QKeySequence(QKeySequence.Delete))

        button_size = gui.toolButtonSizeHint()
        button_size = QSize(button_size, button_size)

        button = QToolButton(self)
        button.setFixedSize(button_size)
        button.setDefaultAction(self.add_label_action)
        hlayout.addWidget(button)

        button = QToolButton(self)
        button.setFixedSize(button_size)
        button.setDefaultAction(self.remove_label_action)
        hlayout.addWidget(button)
        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)

        self.main_form.addRow("Labels:", vlayout)

    def set_data(self, var):
        """Set the variable to edit.
        """
        self.clear()
        self.var = var

        if var is not None:
            self.name_edit.setText(var.name)
            self.labels_model.set_dict(dict(var.attributes))
            self.add_label_action.setEnabled(True)
        else:
            self.add_label_action.setEnabled(False)
            self.remove_label_action.setEnabled(False)

    def get_data(self):
        """Retrieve the modified variable.
        """
        name = str(self.name_edit.text())
        labels = self.labels_model.get_dict()

        # Is the variable actually changed.
        if self.var is not None and not self.is_same():
            var = type(self.var)(name)
            var.attributes.update(labels)
            self.var = var
        else:
            var = self.var

        return var

    def is_same(self):
        """Is the current model state the same as the input.
        """
        name = str(self.name_edit.text())
        labels = self.labels_model.get_dict()

        return (self.var is not None and name == self.var.name and
                labels == self.var.attributes)

    def clear(self):
        """Clear the editor state.
        """
        self.var = None
        self.name_edit.setText("")
        self.labels_model.set_dict({})

    def maybe_commit(self):
        if not self.is_same():
            self.commit()

    def commit(self):
        """Emit a ``variable_changed()`` signal.
        """
        self.variable_changed.emit()

    @Slot()
    def on_name_changed(self):
        self.maybe_commit()

    @Slot()
    def on_labels_changed(self, *args):
        self.maybe_commit()

    @Slot()
    def on_add_label(self):
        self.labels_model.appendRow([QStandardItem(""), QStandardItem("")])
        row = self.labels_model.rowCount() - 1
        index = self.labels_model.index(row, 0)
        self.labels_edit.edit(index)

    @Slot()
    def on_remove_label(self):
        rows = self.labels_edit.selectionModel().selectedRows()
        if rows:
            row = rows[0]
            self.labels_model.removeRow(row.row())

    @Slot()
    def on_label_selection_changed(self):
        selected = self.labels_edit.selectionModel().selectedRows()
        self.remove_label_action.setEnabled(bool(len(selected)))
class VariableEditor(QWidget):
    """An editor widget for a variable.

    Can edit the variable name, and its attributes dictionary.

    """

    variable_changed = Signal()

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.var = None
        self.setup_gui()

    def setup_gui(self):
        layout = QVBoxLayout()
        self.setLayout(layout)

        self.main_form = QFormLayout()
        self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        layout.addLayout(self.main_form)

        self._setup_gui_name()
        self._setup_gui_labels()

    def _setup_gui_name(self):
        class OrangeLineEdit(QLineEdit):
            def keyPressEvent(self, event):
                if event.key() in [Qt.Key_Return, Qt.Key_Enter]:
                    self.parent().on_name_changed()
                else:
                    super().keyPressEvent(event)

        self.name_edit = OrangeLineEdit()
        self.main_form.addRow("Name:", self.name_edit)
        self.name_edit.editingFinished.connect(self.on_name_changed)

    def _setup_gui_labels(self):
        vlayout = QVBoxLayout()
        vlayout.setContentsMargins(0, 0, 0, 0)
        vlayout.setSpacing(1)

        self.labels_edit = QTreeView()
        self.labels_edit.setEditTriggers(QTreeView.CurrentChanged)
        self.labels_edit.setRootIsDecorated(False)

        self.labels_model = DictItemsModel()
        self.labels_edit.setModel(self.labels_model)

        self.labels_edit.selectionModel().selectionChanged.connect(
            self.on_label_selection_changed)

        # Necessary signals to know when the labels change
        self.labels_model.dataChanged.connect(self.on_labels_changed)
        self.labels_model.rowsInserted.connect(self.on_labels_changed)
        self.labels_model.rowsRemoved.connect(self.on_labels_changed)

        vlayout.addWidget(self.labels_edit)
        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        hlayout.setSpacing(1)
        self.add_label_action = QAction(
            "+",
            self,
            toolTip="Add a new label.",
            triggered=self.on_add_label,
            enabled=False,
            shortcut=QKeySequence(QKeySequence.New),
        )

        self.remove_label_action = QAction(
            unicodedata.lookup("MINUS SIGN"),
            self,
            toolTip="Remove selected label.",
            triggered=self.on_remove_label,
            enabled=False,
            shortcut=QKeySequence(QKeySequence.Delete),
        )

        button_size = gui.toolButtonSizeHint()
        button_size = QSize(button_size, button_size)

        button = QToolButton(self)
        button.setFixedSize(button_size)
        button.setDefaultAction(self.add_label_action)
        hlayout.addWidget(button)

        button = QToolButton(self)
        button.setFixedSize(button_size)
        button.setDefaultAction(self.remove_label_action)
        hlayout.addWidget(button)
        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)

        self.main_form.addRow("Labels:", vlayout)

    def set_data(self, var):
        """Set the variable to edit.
        """
        self.clear()
        self.var = var

        if var is not None:
            self.name_edit.setText(var.name)
            self.labels_model.set_dict(dict(var.attributes))
            self.add_label_action.setEnabled(True)
        else:
            self.add_label_action.setEnabled(False)
            self.remove_label_action.setEnabled(False)

    def get_data(self):
        """Retrieve the modified variable.
        """
        name = str(self.name_edit.text()).strip()
        labels = self.labels_model.get_dict()

        # Is the variable actually changed.
        if self.var is not None and not self.is_same():
            var = type(self.var)(name)
            var.attributes.update(labels)
            self.var = var
        else:
            var = self.var

        return var

    def is_legal(self):
        name = str(self.name_edit.text()).strip()
        return not len(name) == 0

    def is_same(self):
        """Is the current model state the same as the input.
        """
        name = str(self.name_edit.text()).strip()
        labels = self.labels_model.get_dict()
        return (self.var is not None and name == self.var.name
                and labels == self.var.attributes)

    def clear(self):
        """Clear the editor state.
        """
        self.var = None
        self.name_edit.setText("")
        self.labels_model.set_dict({})

    def maybe_commit(self):
        if not self.is_same() and self.is_legal():
            self.commit()

    def commit(self):
        """Emit a ``variable_changed()`` signal.
        """
        self.variable_changed.emit()

    @Slot()
    def on_name_changed(self):
        self.maybe_commit()

    @Slot()
    def on_labels_changed(self, *args):
        self.maybe_commit()

    @Slot()
    def on_add_label(self):
        self.labels_model.appendRow([QStandardItem(""), QStandardItem("")])
        row = self.labels_model.rowCount() - 1
        index = self.labels_model.index(row, 0)
        self.labels_edit.edit(index)

    @Slot()
    def on_remove_label(self):
        rows = self.labels_edit.selectionModel().selectedRows()
        if rows:
            row = rows[0]
            self.labels_model.removeRow(row.row())

    @Slot()
    def on_label_selection_changed(self):
        selected = self.labels_edit.selectionModel().selectedRows()
        self.remove_label_action.setEnabled(bool(len(selected)))
Пример #4
0
class OWPaintData(OWWidget):
    TOOLS = [("Brush", "Create multiple instances", AirBrushTool,
              _icon("brush.svg")),
             ("Put", "Put individual instances", PutInstanceTool,
              _icon("put.svg")),
             ("Select", "Select and move instances", SelectTool,
              _icon("select-transparent_42px.png")),
             ("Jitter", "Jitter instances", JitterTool, _icon("jitter.svg")),
             ("Magnet", "Attract multiple instances", MagnetTool,
              _icon("magnet.svg")),
             ("Clear", "Clear the plot", ClearTool,
              _icon("../../../icons/Dlg_clear.png"))]

    name = "Paint Data"
    description = "Create data by painting data points on a plane."
    icon = "icons/PaintData.svg"
    priority = 60
    keywords = ["create", "draw"]

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

    class Outputs:
        data = Output("Data", Table)

    autocommit = Setting(True)
    table_name = Setting("Painted data")
    attr1 = Setting("x")
    attr2 = Setting("y")
    hasAttr2 = Setting(True)

    brushRadius = Setting(75)
    density = Setting(7)
    symbol_size = Setting(10)

    #: current data array (shape=(N, 3)) as presented on the output
    data = Setting(None, schema_only=True)
    labels = Setting(["C1", "C2"], schema_only=True)

    buttons_area_orientation = Qt.Vertical
    graph_name = "plot"

    class Warning(OWWidget.Warning):
        no_input_variables = Msg("Input data has no variables")
        continuous_target = Msg("Numeric target value can not be used.")
        sparse_not_supported = Msg("Sparse data is ignored.")
        renamed_vars = Msg("Some variables have been renamed "
                           "to avoid duplicates.\n{}")

    class Information(OWWidget.Information):
        use_first_two = \
            Msg("Paint Data uses data from the first two attributes.")

    def __init__(self):
        super().__init__()

        self.input_data = None
        self.input_classes = []
        self.input_colors = None
        self.input_has_attr2 = True
        self.current_tool = None
        self._selected_indices = None
        self._scatter_item = None
        #: A private data buffer (can be modified in place). `self.data` is
        #: a copy of this array (as seen when the `invalidate` method is
        #: called
        self.__buffer = None

        self.undo_stack = QUndoStack(self)

        self.class_model = ColoredListModel(
            self.labels,
            self,
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)

        self.class_model.dataChanged.connect(self._class_value_changed)
        self.class_model.rowsInserted.connect(self._class_count_changed)
        self.class_model.rowsRemoved.connect(self._class_count_changed)

        # if self.data: raises Deprecation warning in older workflows, where
        # data could be a np.array. This would raise an error in the future.
        if self.data is None or len(self.data) == 0:
            self.data = []
            self.__buffer = np.zeros((0, 3))
        elif isinstance(self.data, np.ndarray):
            self.__buffer = self.data.copy()
            self.data = self.data.tolist()
        else:
            self.__buffer = np.array(self.data)

        self.colors = colorpalettes.DefaultRGBColors
        self.tools_cache = {}

        self._init_ui()
        self.commit()

    def _init_ui(self):
        namesBox = gui.vBox(self.controlArea, "Names")

        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        gui.lineEdit(hbox,
                     self,
                     "attr1",
                     "Variable X: ",
                     controlWidth=80,
                     orientation=Qt.Horizontal,
                     callback=self._attr_name_changed)
        gui.separator(hbox, 21)
        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        attr2 = gui.lineEdit(hbox,
                             self,
                             "attr2",
                             "Variable Y: ",
                             controlWidth=80,
                             orientation=Qt.Horizontal,
                             callback=self._attr_name_changed)
        gui.separator(hbox)
        gui.checkBox(hbox,
                     self,
                     "hasAttr2",
                     '',
                     disables=attr2,
                     labelWidth=0,
                     callback=self.set_dimensions)

        gui.widgetLabel(namesBox, "Labels")
        self.classValuesView = listView = gui.ListViewWithSizeHint(
            preferred_size=(-1, 30))
        listView.setModel(self.class_model)
        itemmodels.select_row(listView, 0)
        namesBox.layout().addWidget(listView)

        self.addClassLabel = QAction("+",
                                     self,
                                     toolTip="Add new class label",
                                     triggered=self.add_new_class_label)

        self.removeClassLabel = QAction(
            unicodedata.lookup("MINUS SIGN"),
            self,
            toolTip="Remove selected class label",
            triggered=self.remove_selected_class_label)

        actionsWidget = itemmodels.ModelActionsWidget(
            [self.addClassLabel, self.removeClassLabel], self)
        actionsWidget.layout().addStretch(10)
        actionsWidget.layout().setSpacing(1)
        namesBox.layout().addWidget(actionsWidget)

        tBox = gui.vBox(self.buttonsArea, "Tools")
        toolsBox = gui.widgetBox(tBox, orientation=QGridLayout())

        self.toolActions = QActionGroup(self)
        self.toolActions.setExclusive(True)
        self.toolButtons = []

        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
            action = QAction(
                name,
                self,
                toolTip=tooltip,
                checkable=tool.checkable,
                icon=QIcon(icon),
            )
            action.triggered.connect(partial(self.set_current_tool, tool))

            button = QToolButton(iconSize=QSize(24, 24),
                                 toolButtonStyle=Qt.ToolButtonTextUnderIcon,
                                 sizePolicy=QSizePolicy(
                                     QSizePolicy.MinimumExpanding,
                                     QSizePolicy.Fixed))
            button.setDefaultAction(action)
            self.toolButtons.append((button, tool))

            toolsBox.layout().addWidget(button, i / 3, i % 3)
            self.toolActions.addAction(action)

        for column in range(3):
            toolsBox.layout().setColumnMinimumWidth(column, 10)
            toolsBox.layout().setColumnStretch(column, 1)

        undo = self.undo_stack.createUndoAction(self)
        redo = self.undo_stack.createRedoAction(self)

        undo.setShortcut(QKeySequence.Undo)
        redo.setShortcut(QKeySequence.Redo)

        self.addActions([undo, redo])
        self.undo_stack.indexChanged.connect(self.invalidate)

        indBox = gui.indentedBox(tBox, sep=8)
        form = QFormLayout(formAlignment=Qt.AlignLeft,
                           labelAlignment=Qt.AlignLeft,
                           fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow)
        indBox.layout().addLayout(form)
        slider = gui.hSlider(indBox,
                             self,
                             "brushRadius",
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             addToLayout=False)
        form.addRow("Radius:", slider)

        slider = gui.hSlider(indBox,
                             self,
                             "density",
                             None,
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             addToLayout=False)

        form.addRow("Intensity:", slider)

        slider = gui.hSlider(indBox,
                             self,
                             "symbol_size",
                             None,
                             minValue=1,
                             maxValue=100,
                             createLabel=False,
                             callback=self.set_symbol_size,
                             addToLayout=False)

        form.addRow("Symbol:", slider)

        self.btResetToInput = gui.button(tBox, self, "Reset to Input Data",
                                         self.reset_to_input)
        self.btResetToInput.setDisabled(True)

        gui.auto_send(self.buttonsArea, self, "autocommit")

        # main area GUI
        viewbox = PaintViewBox(enableMouse=False)
        self.plotview = pg.PlotWidget(background="w", viewBox=viewbox)
        self.plotview.sizeHint = lambda: QSize(
            200, 100)  # Minimum size for 1-d painting
        self.plot = self.plotview.getPlotItem()

        axis_color = self.palette().color(QPalette.Text)
        axis_pen = QPen(axis_color)

        tickfont = QFont(self.font())
        tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))

        axis = self.plot.getAxis("bottom")
        axis.setLabel(self.attr1)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)

        axis = self.plot.getAxis("left")
        axis.setLabel(self.attr2)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)
        if not self.hasAttr2:
            self.plot.hideAxis('left')

        self.plot.hideButtons()
        self.plot.setXRange(0, 1, padding=0.01)

        self.mainArea.layout().addWidget(self.plotview)

        # enable brush tool
        self.toolActions.actions()[0].setChecked(True)
        self.set_current_tool(self.TOOLS[0][2])

        self.set_dimensions()

    def set_symbol_size(self):
        if self._scatter_item:
            self._scatter_item.setSize(self.symbol_size)

    def set_dimensions(self):
        if self.hasAttr2:
            self.plot.setYRange(0, 1, padding=0.01)
            self.plot.showAxis('left')
            self.plotview.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Minimum)
        else:
            self.plot.setYRange(-.5, .5, padding=0.01)
            self.plot.hideAxis('left')
            self.plotview.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Maximum)
        self._replot()
        for button, tool in self.toolButtons:
            if tool.only2d:
                button.setDisabled(not self.hasAttr2)

    @Inputs.data
    def set_data(self, data):
        """Set the input_data and call reset_to_input"""
        def _check_and_set_data(data):
            self.clear_messages()
            if data and data.is_sparse():
                self.Warning.sparse_not_supported()
                return False
            if data:
                if not data.domain.attributes:
                    self.Warning.no_input_variables()
                    data = None
                elif len(data.domain.attributes) > 2:
                    self.Information.use_first_two()
            self.input_data = data
            self.btResetToInput.setDisabled(data is None)
            return bool(data)

        if not _check_and_set_data(data):
            return

        X = np.array([scale(vals) for vals in data.X[:, :2].T]).T
        try:
            y = next(cls for cls in data.domain.class_vars if cls.is_discrete)
        except StopIteration:
            if data.domain.class_vars:
                self.Warning.continuous_target()
            self.input_classes = ["C1"]
            self.input_colors = None
            y = np.zeros(len(data))
        else:
            self.input_classes = y.values
            self.input_colors = y.palette

            y = data[:, y].Y

        self.input_has_attr2 = len(data.domain.attributes) >= 2
        if not self.input_has_attr2:
            self.input_data = np.column_stack((X, np.zeros(len(data)), y))
        else:
            self.input_data = np.column_stack((X, y))
        self.reset_to_input()
        self.unconditional_commit()

    def reset_to_input(self):
        """Reset the painting to input data if present."""
        if self.input_data is None:
            return
        self.undo_stack.clear()

        index = self.selected_class_label()
        if self.input_colors is not None:
            palette = self.input_colors
        else:
            palette = colorpalettes.DefaultRGBColors
        self.colors = palette
        self.class_model.colors = palette
        self.class_model[:] = self.input_classes

        newindex = min(max(index, 0), len(self.class_model) - 1)
        itemmodels.select_row(self.classValuesView, newindex)

        self.data = self.input_data.tolist()
        self.__buffer = self.input_data.copy()

        prev_attr2 = self.hasAttr2
        self.hasAttr2 = self.input_has_attr2
        if prev_attr2 != self.hasAttr2:
            self.set_dimensions()
        else:  # set_dimensions already calls _replot, no need to call it again
            self._replot()

        self.commit()

    def add_new_class_label(self, undoable=True):

        newlabel = next(label for label in namegen('C', 1)
                        if label not in self.class_model)

        command = SimpleUndoCommand(lambda: self.class_model.append(newlabel),
                                    lambda: self.class_model.__delitem__(-1))
        if undoable:
            self.undo_stack.push(command)
        else:
            command.redo()

    def remove_selected_class_label(self):
        index = self.selected_class_label()

        if index is None:
            return

        label = self.class_model[index]
        mask = self.__buffer[:, 2] == index
        move_mask = self.__buffer[~mask][:, 2] > index

        self.undo_stack.beginMacro("Delete class label")
        self.undo_stack.push(UndoCommand(DeleteIndices(mask), self))
        self.undo_stack.push(UndoCommand(Move((move_mask, 2), -1), self))
        self.undo_stack.push(
            SimpleUndoCommand(lambda: self.class_model.__delitem__(index),
                              lambda: self.class_model.insert(index, label)))
        self.undo_stack.endMacro()

        newindex = min(max(index - 1, 0), len(self.class_model) - 1)
        itemmodels.select_row(self.classValuesView, newindex)

    def _class_count_changed(self):
        self.labels = list(self.class_model)
        self.removeClassLabel.setEnabled(len(self.class_model) > 1)
        self.addClassLabel.setEnabled(len(self.class_model) < len(self.colors))
        if self.selected_class_label() is None:
            itemmodels.select_row(self.classValuesView, 0)

    def _class_value_changed(self, index, _):
        index = index.row()
        newvalue = self.class_model[index]
        oldvalue = self.labels[index]
        if newvalue != oldvalue:
            self.labels[index] = newvalue


#             command = Command(
#                 lambda: self.class_model.__setitem__(index, newvalue),
#                 lambda: self.class_model.__setitem__(index, oldvalue),
#             )
#             self.undo_stack.push(command)

    def selected_class_label(self):
        rows = self.classValuesView.selectedIndexes()
        if rows:
            return rows[0].row()
        return None

    def set_current_tool(self, tool):
        prev_tool = self.current_tool.__class__

        if self.current_tool is not None:
            self.current_tool.deactivate()
            self.current_tool.editingStarted.disconnect(
                self._on_editing_started)
            self.current_tool.editingFinished.disconnect(
                self._on_editing_finished)
            self.current_tool = None
            self.plot.getViewBox().tool = None

        if tool not in self.tools_cache:
            newtool = tool(self, self.plot)
            self.tools_cache[tool] = newtool
            newtool.issueCommand.connect(self._add_command)

        self.current_tool = tool = self.tools_cache[tool]
        self.plot.getViewBox().tool = tool
        tool.editingStarted.connect(self._on_editing_started)
        tool.editingFinished.connect(self._on_editing_finished)
        tool.activate()

        if not tool.checkable:
            self.set_current_tool(prev_tool)

    def _on_editing_started(self):
        self.undo_stack.beginMacro("macro")

    def _on_editing_finished(self):
        self.undo_stack.endMacro()

    def execute(self, command):
        assert isinstance(command, (Append, DeleteIndices, Insert, Move)), \
            "Non normalized command"
        if isinstance(command, (DeleteIndices, Insert)):
            self._selected_indices = None

            if isinstance(self.current_tool, SelectTool):
                self.current_tool.reset()

        self.__buffer, undo = transform(command, self.__buffer)
        self._replot()
        return undo

    def _add_command(self, cmd):
        # pylint: disable=too-many-branches
        name = "Name"

        if (not self.hasAttr2
                and isinstance(cmd, (Move, MoveSelection, Jitter, Magnet))):
            # tool only supported if both x and y are enabled
            return

        if isinstance(cmd, Append):
            cls = self.selected_class_label()
            points = np.array([(p.x(), p.y() if self.hasAttr2 else 0, cls)
                               for p in cmd.points])
            self.undo_stack.push(UndoCommand(Append(points), self, text=name))
        elif isinstance(cmd, Move):
            self.undo_stack.push(UndoCommand(cmd, self, text=name))
        elif isinstance(cmd, SelectRegion):
            indices = [
                i for i, (x, y) in enumerate(self.__buffer[:, :2])
                if cmd.region.contains(QPointF(x, y))
            ]
            indices = np.array(indices, dtype=int)
            self._selected_indices = indices
        elif isinstance(cmd, DeleteSelection):
            indices = self._selected_indices
            if indices is not None and indices.size:
                self.undo_stack.push(
                    UndoCommand(DeleteIndices(indices), self, text="Delete"))
        elif isinstance(cmd, MoveSelection):
            indices = self._selected_indices
            if indices is not None and indices.size:
                self.undo_stack.push(
                    UndoCommand(Move((self._selected_indices, slice(0, 2)),
                                     np.array([cmd.delta.x(),
                                               cmd.delta.y()])),
                                self,
                                text="Move"))
        elif isinstance(cmd, DeleteIndices):
            self.undo_stack.push(UndoCommand(cmd, self, text="Delete"))
        elif isinstance(cmd, Insert):
            self.undo_stack.push(UndoCommand(cmd, self))
        elif isinstance(cmd, AirBrush):
            data = create_data(cmd.pos.x(), cmd.pos.y(),
                               self.brushRadius / 1000,
                               int(1 + self.density / 20), cmd.rstate)
            self._add_command(Append([QPointF(*p) for p in zip(*data.T)]))
        elif isinstance(cmd, Jitter):
            point = np.array([cmd.pos.x(), cmd.pos.y()])
            delta = -apply_jitter(self.__buffer[:, :2], point,
                                  self.density / 100.0, 0, cmd.rstate)
            self._add_command(Move((..., slice(0, 2)), delta))
        elif isinstance(cmd, Magnet):
            point = np.array([cmd.pos.x(), cmd.pos.y()])
            delta = -apply_attractor(self.__buffer[:, :2], point,
                                     self.density / 100.0, 0)
            self._add_command(Move((..., slice(0, 2)), delta))
        else:
            assert False, "unreachable"

    def _replot(self):
        def pen(color):
            pen = QPen(color, 1)
            pen.setCosmetic(True)
            return pen

        if self._scatter_item is not None:
            self.plot.removeItem(self._scatter_item)
            self._scatter_item = None

        x = self.__buffer[:, 0].copy()
        if self.hasAttr2:
            y = self.__buffer[:, 1].copy()
        else:
            y = np.zeros(self.__buffer.shape[0])

        colors = self.colors[self.__buffer[:, 2]]
        pens = [pen(c) for c in colors]
        brushes = [QBrush(c) for c in colors]

        self._scatter_item = pg.ScatterPlotItem(x,
                                                y,
                                                symbol="+",
                                                brush=brushes,
                                                pen=pens)
        self.plot.addItem(self._scatter_item)
        self.set_symbol_size()

    def _attr_name_changed(self):
        self.plot.getAxis("bottom").setLabel(self.attr1)
        self.plot.getAxis("left").setLabel(self.attr2)
        self.invalidate()

    def invalidate(self):
        self.data = self.__buffer.tolist()
        self.commit()

    def commit(self):
        self.Warning.renamed_vars.clear()

        if not self.data:
            self.Outputs.data.send(None)
            return
        data = np.array(self.data)
        if self.hasAttr2:
            X, Y = data[:, :2], data[:, 2]
            proposed = [self.attr1.strip(), self.attr2.strip()]
        else:
            X, Y = data[:, np.newaxis, 0], data[:, 2]
            proposed = [self.attr1.strip()]

        if len(np.unique(Y)) >= 2:
            proposed.append("Class")
            unique_names, renamed = get_unique_names_duplicates(proposed, True)
            domain = Domain((map(ContinuousVariable, unique_names[:-1])),
                            DiscreteVariable(unique_names[-1],
                                             values=tuple(self.class_model)))
            data = Table.from_numpy(domain, X, Y)
        else:
            unique_names, renamed = get_unique_names_duplicates(proposed, True)
            domain = Domain(map(ContinuousVariable, unique_names))
            data = Table.from_numpy(domain, X)

        if renamed:
            self.Warning.renamed_vars(", ".join(renamed))
            self.plot.getAxis("bottom").setLabel(unique_names[0])
            self.plot.getAxis("left").setLabel(unique_names[1])

        data.name = self.table_name
        self.Outputs.data.send(data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(570, 690))

    def onDeleteWidget(self):
        self.undo_stack.indexChanged.disconnect(self.invalidate)
        self.plot.clear()

    def send_report(self):
        if self.data is None:
            return
        settings = []
        if self.attr1 != "x" or self.attr2 != "y":
            settings += [("Axis x", self.attr1), ("Axis y", self.attr2)]
        settings += [("Number of points", len(self.data))]
        self.report_items("Painted data", settings)
        self.report_plot()
Пример #5
0
class OWPaintData(OWWidget):
    TOOLS = [
        ("Brush", "Create multiple instances", AirBrushTool, _i("brush.svg")),
        ("Put", "Put individual instances", PutInstanceTool, _i("put.svg")),
        ("Select", "Select and move instances", SelectTool,
            _i("select-transparent_42px.png")),
        ("Jitter", "Jitter instances", JitterTool, _i("jitter.svg")),
        ("Magnet", "Attract multiple instances", MagnetTool, _i("magnet.svg")),
        ("Clear", "Clear the plot", ClearTool, _i("../../../icons/Dlg_clear.png"))
    ]

    name = "Paint Data"
    description = "Create data by painting data points on a plane."
    icon = "icons/PaintData.svg"
    priority = 15
    keywords = ["data", "paint", "create"]

    outputs = [("Data", Orange.data.Table)]
    inputs = [("Data", Orange.data.Table, "set_data")]

    autocommit = Setting(False)
    table_name = Setting("Painted data")
    attr1 = Setting("x")
    attr2 = Setting("y")
    hasAttr2 = Setting(True)

    brushRadius = Setting(75)
    density = Setting(7)

    data = Setting(None, schema_only=True)

    graph_name = "plot"

    class Warning(OWWidget.Warning):
        no_input_variables = Msg("Input data has no variables")
        continuous_target = Msg("Continuous target value can not be used.")

    class Information(OWWidget.Information):
        use_first_two = \
            Msg("Paint Data uses data from the first two attributes.")

    def __init__(self):
        super().__init__()

        self.input_data = None
        self.input_classes = []
        self.input_has_attr2 = True
        self.current_tool = None
        self._selected_indices = None
        self._scatter_item = None

        self.labels = ["C1", "C2"]

        self.undo_stack = QUndoStack(self)

        self.class_model = ColoredListModel(
            self.labels, self,
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled |
                  Qt.ItemIsEditable)

        self.class_model.dataChanged.connect(self._class_value_changed)
        self.class_model.rowsInserted.connect(self._class_count_changed)
        self.class_model.rowsRemoved.connect(self._class_count_changed)

        if self.data is None:
            self.data = np.zeros((0, 3))
        self.colors = colorpalette.ColorPaletteGenerator(
            len(colorpalette.DefaultRGBColors))
        self.tools_cache = {}

        self._init_ui()
        self.commit()

    def _init_ui(self):
        namesBox = gui.vBox(self.controlArea, "Names")

        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        gui.lineEdit(hbox, self, "attr1", "Variable X: ",
                     controlWidth=80, orientation=Qt.Horizontal,
                     callback=self._attr_name_changed)
        gui.separator(hbox, 21)
        hbox = gui.hBox(namesBox, margin=0, spacing=0)
        attr2 = gui.lineEdit(hbox, self, "attr2", "Variable Y: ",
                             controlWidth=80, orientation=Qt.Horizontal,
                             callback=self._attr_name_changed)
        gui.separator(hbox)
        gui.checkBox(hbox, self, "hasAttr2", '', disables=attr2,
                     labelWidth=0,
                     callback=self.set_dimensions)
        gui.separator(namesBox)

        gui.widgetLabel(namesBox, "Labels")
        self.classValuesView = listView = QListView(
            selectionMode=QListView.SingleSelection,
            sizePolicy=QSizePolicy(QSizePolicy.Ignored,
                                   QSizePolicy.Maximum)
        )
        listView.setModel(self.class_model)
        itemmodels.select_row(listView, 0)
        namesBox.layout().addWidget(listView)

        self.addClassLabel = QAction(
            "+", self,
            toolTip="Add new class label",
            triggered=self.add_new_class_label
        )

        self.removeClassLabel = QAction(
            unicodedata.lookup("MINUS SIGN"), self,
            toolTip="Remove selected class label",
            triggered=self.remove_selected_class_label
        )

        actionsWidget = itemmodels.ModelActionsWidget(
            [self.addClassLabel, self.removeClassLabel], self
        )
        actionsWidget.layout().addStretch(10)
        actionsWidget.layout().setSpacing(1)
        namesBox.layout().addWidget(actionsWidget)

        tBox = gui.vBox(self.controlArea, "Tools", addSpace=True)
        buttonBox = gui.hBox(tBox)
        toolsBox = gui.widgetBox(buttonBox, orientation=QGridLayout())

        self.toolActions = QActionGroup(self)
        self.toolActions.setExclusive(True)
        self.toolButtons = []

        for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
            action = QAction(
                name, self,
                toolTip=tooltip,
                checkable=tool.checkable,
                icon=QIcon(icon),
            )
            action.triggered.connect(partial(self.set_current_tool, tool))

            button = QToolButton(
                iconSize=QSize(24, 24),
                toolButtonStyle=Qt.ToolButtonTextUnderIcon,
                sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                       QSizePolicy.Fixed)
            )
            button.setDefaultAction(action)
            self.toolButtons.append((button, tool))

            toolsBox.layout().addWidget(button, i / 3, i % 3)
            self.toolActions.addAction(action)

        for column in range(3):
            toolsBox.layout().setColumnMinimumWidth(column, 10)
            toolsBox.layout().setColumnStretch(column, 1)

        undo = self.undo_stack.createUndoAction(self)
        redo = self.undo_stack.createRedoAction(self)

        undo.setShortcut(QKeySequence.Undo)
        redo.setShortcut(QKeySequence.Redo)

        self.addActions([undo, redo])
        self.undo_stack.indexChanged.connect(lambda _: self.invalidate())

        gui.separator(tBox)
        indBox = gui.indentedBox(tBox, sep=8)
        form = QFormLayout(
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        indBox.layout().addLayout(form)
        slider = gui.hSlider(
            indBox, self, "brushRadius", minValue=1, maxValue=100,
            createLabel=False
        )
        form.addRow("Radius:", slider)

        slider = gui.hSlider(
            indBox, self, "density", None, minValue=1, maxValue=100,
            createLabel=False
        )

        form.addRow("Intensity:", slider)
        self.btResetToInput = gui.button(
            tBox, self, "Reset to Input Data", self.reset_to_input)
        self.btResetToInput.setDisabled(True)

        gui.rubber(self.controlArea)
        gui.auto_commit(self.left_side, self, "autocommit",
                        "Send")

        # main area GUI
        viewbox = PaintViewBox(enableMouse=False)
        self.plotview = pg.PlotWidget(background="w", viewBox=viewbox)
        self.plotview.sizeHint = lambda: QSize(200, 100)  # Minimum size for 1-d painting
        self.plot = self.plotview.getPlotItem()

        axis_color = self.palette().color(QPalette.Text)
        axis_pen = QPen(axis_color)

        tickfont = QFont(self.font())
        tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))

        axis = self.plot.getAxis("bottom")
        axis.setLabel(self.attr1)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)

        axis = self.plot.getAxis("left")
        axis.setLabel(self.attr2)
        axis.setPen(axis_pen)
        axis.setTickFont(tickfont)
        if not self.hasAttr2:
            self.plot.hideAxis('left')

        self.plot.hideButtons()
        self.plot.setXRange(0, 1, padding=0.01)

        self.mainArea.layout().addWidget(self.plotview)

        # enable brush tool
        self.toolActions.actions()[0].setChecked(True)
        self.set_current_tool(self.TOOLS[0][2])

        self.set_dimensions()

    def set_dimensions(self):
        if self.hasAttr2:
            self.plot.setYRange(0, 1, padding=0.01)
            self.plot.showAxis('left')
            self.plotview.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum)
        else:
            self.plot.setYRange(-.5, .5, padding=0.01)
            self.plot.hideAxis('left')
            self.plotview.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum)
        self._replot()
        for button, tool in self.toolButtons:
            if tool.only2d:
                button.setDisabled(not self.hasAttr2)

    def set_data(self, data):
        """Set the input_data and call reset_to_input"""
        def _check_and_set_data(data):
            self.clear_messages()
            if data is not None:
                if not data.domain.attributes:
                    self.Warning.no_input_variables()
                    data = None
                elif len(data.domain.attributes) > 2:
                    self.Information.use_first_two()
            self.input_data = data
            self.btResetToInput.setDisabled(data is None)
            return data is not None

        if not _check_and_set_data(data):
            return

        X = np.array([scale(vals) for vals in data.X[:, :2].T]).T
        try:
            y = next(cls for cls in data.domain.class_vars if cls.is_discrete)
        except StopIteration:
            if data.domain.class_vars:
                self.Warning.continuous_target()
            self.input_classes = ["C1"]
            y = np.zeros(len(data))
        else:
            self.input_classes = y.values
            y = data[:, y].Y

        self.input_has_attr2 = len(data.domain.attributes) >= 2
        if not self.input_has_attr2:
            self.input_data = np.column_stack((X, np.zeros(len(data)), y))
        else:
            self.input_data = np.column_stack((X, y))
        self.reset_to_input()

    def reset_to_input(self):
        """Reset the painting to input data if present."""
        if self.input_data is None:
            return
        self.undo_stack.clear()

        index = self.selected_class_label()
        self.class_model[:] = self.input_classes
        newindex = min(max(index, 0), len(self.class_model) - 1)
        itemmodels.select_row(self.classValuesView, newindex)

        self.data = self.input_data
        prev_attr2 = self.hasAttr2
        self.hasAttr2 = self.input_has_attr2
        if prev_attr2 != self.hasAttr2:
            self.set_dimensions()
        else:  # set_dimensions already calls _replot, no need to call it again
            self._replot()

    def add_new_class_label(self, undoable=True):

        newlabel = next(label for label in namegen('C', 1)
                        if label not in self.class_model)

        command = SimpleUndoCommand(
            lambda: self.class_model.append(newlabel),
            lambda: self.class_model.__delitem__(-1)
        )
        if undoable:
            self.undo_stack.push(command)
        else:
            command.redo()

    def remove_selected_class_label(self):
        index = self.selected_class_label()

        if index is None:
            return

        label = self.class_model[index]
        mask = self.data[:, 2] == index
        move_mask = self.data[~mask][:, 2] > index

        self.undo_stack.beginMacro("Delete class label")
        self.undo_stack.push(UndoCommand(DeleteIndices(mask), self))
        self.undo_stack.push(UndoCommand(Move((move_mask, 2), -1), self))
        self.undo_stack.push(
            SimpleUndoCommand(lambda: self.class_model.__delitem__(index),
                              lambda: self.class_model.insert(index, label)))
        self.undo_stack.endMacro()

        newindex = min(max(index - 1, 0), len(self.class_model) - 1)
        itemmodels.select_row(self.classValuesView, newindex)

    def _class_count_changed(self):
        self.labels = list(self.class_model)
        self.removeClassLabel.setEnabled(len(self.class_model) > 1)
        self.addClassLabel.setEnabled(
            len(self.class_model) < self.colors.number_of_colors)
        if self.selected_class_label() is None:
            itemmodels.select_row(self.classValuesView, 0)

    def _class_value_changed(self, index, _):
        index = index.row()
        newvalue = self.class_model[index]
        oldvalue = self.labels[index]
        if newvalue != oldvalue:
            self.labels[index] = newvalue
#             command = Command(
#                 lambda: self.class_model.__setitem__(index, newvalue),
#                 lambda: self.class_model.__setitem__(index, oldvalue),
#             )
#             self.undo_stack.push(command)

    def selected_class_label(self):
        rows = self.classValuesView.selectedIndexes()
        if rows:
            return rows[0].row()
        else:
            return None

    def set_current_tool(self, tool):
        prev_tool = self.current_tool.__class__

        if self.current_tool is not None:
            self.current_tool.deactivate()
            self.current_tool.editingStarted.disconnect(
                self._on_editing_started)
            self.current_tool.editingFinished.disconnect(
                self._on_editing_finished)
            self.current_tool = None
            self.plot.getViewBox().tool = None

        if tool not in self.tools_cache:
            newtool = tool(self, self.plot)
            self.tools_cache[tool] = newtool
            newtool.issueCommand.connect(self._add_command)

        self._selected_region = QRectF()
        self.current_tool = tool = self.tools_cache[tool]
        self.plot.getViewBox().tool = tool
        tool.editingStarted.connect(self._on_editing_started)
        tool.editingFinished.connect(self._on_editing_finished)
        tool.activate()

        if not tool.checkable:
            self.set_current_tool(prev_tool)

    def _on_editing_started(self):
        self.undo_stack.beginMacro("macro")

    def _on_editing_finished(self):
        self.undo_stack.endMacro()

    def execute(self, command):
        if isinstance(command, (Append, DeleteIndices, Insert, Move)):
            if isinstance(command, (DeleteIndices, Insert)):
                self._selected_indices = None

                if isinstance(self.current_tool, SelectTool):
                    self.current_tool._reset()

            self.data, undo = transform(command, self.data)
            self._replot()
            return undo
        else:
            assert False, "Non normalized command"

    def _add_command(self, cmd):
        name = "Name"

        if (not self.hasAttr2 and
            isinstance(cmd, (Move, MoveSelection, Jitter, Magnet))):
            # tool only supported if both x and y are enabled
            return

        if isinstance(cmd, Append):
            cls = self.selected_class_label()
            points = np.array([(p.x(), p.y() if self.hasAttr2 else 0, cls)
                                  for p in cmd.points])
            self.undo_stack.push(UndoCommand(Append(points), self, text=name))
        elif isinstance(cmd, Move):
            self.undo_stack.push(UndoCommand(cmd, self, text=name))
        elif isinstance(cmd, SelectRegion):
            indices = [i for i, (x, y) in enumerate(self.data[:, :2])
                       if cmd.region.contains(QPointF(x, y))]
            indices = np.array(indices, dtype=int)
            self._selected_indices = indices
        elif isinstance(cmd, DeleteSelection):
            indices = self._selected_indices
            if indices is not None and indices.size:
                self.undo_stack.push(
                    UndoCommand(DeleteIndices(indices), self, text="Delete")
                )
        elif isinstance(cmd, MoveSelection):
            indices = self._selected_indices
            if indices is not None and indices.size:
                self.undo_stack.push(
                    UndoCommand(
                        Move((self._selected_indices, slice(0, 2)),
                             np.array([cmd.delta.x(), cmd.delta.y()])),
                        self, text="Move")
                )
        elif isinstance(cmd, DeleteIndices):
            self.undo_stack.push(UndoCommand(cmd, self, text="Delete"))
        elif isinstance(cmd, Insert):
            self.undo_stack.push(UndoCommand(cmd, self))
        elif isinstance(cmd, AirBrush):
            data = create_data(cmd.pos.x(), cmd.pos.y(),
                               self.brushRadius / 1000,
                               int(1 + self.density / 20), cmd.rstate)
            self._add_command(Append([QPointF(*p) for p in zip(*data.T)]))
        elif isinstance(cmd, Jitter):
            point = np.array([cmd.pos.x(), cmd.pos.y()])
            delta = - apply_jitter(self.data[:, :2], point,
                                   self.density / 100.0, 0, cmd.rstate)
            self._add_command(Move((..., slice(0, 2)), delta))
        elif isinstance(cmd, Magnet):
            point = np.array([cmd.pos.x(), cmd.pos.y()])
            delta = - apply_attractor(self.data[:, :2], point,
                                      self.density / 100.0, 0)
            self._add_command(Move((..., slice(0, 2)), delta))
        else:
            assert False, "unreachable"

    def _replot(self):
        def pen(color):
            pen = QPen(color, 1)
            pen.setCosmetic(True)
            return pen

        if self._scatter_item is not None:
            self.plot.removeItem(self._scatter_item)
            self._scatter_item = None

        nclasses = len(self.class_model)
        pens = [pen(self.colors[i]) for i in range(nclasses)]

        self._scatter_item = pg.ScatterPlotItem(
            self.data[:, 0],
            self.data[:, 1] if self.hasAttr2 else np.zeros(self.data.shape[0]),
            symbol="+",
            pen=[pens[int(ci)] for ci in self.data[:, 2]]
        )

        self.plot.addItem(self._scatter_item)

    def _attr_name_changed(self):
        self.plot.getAxis("bottom").setLabel(self.attr1)
        self.plot.getAxis("left").setLabel(self.attr2)
        self.invalidate()

    def invalidate(self):
        self.commit()

    def commit(self):
        if len(self.data) == 0:
            self.send("Data", None)
            return
        if self.hasAttr2:
            X, Y = self.data[:, :2], self.data[:, 2]
            attrs = (Orange.data.ContinuousVariable(self.attr1),
                     Orange.data.ContinuousVariable(self.attr2))
        else:
            X, Y = self.data[:, np.newaxis, 0], self.data[:, 2]
            attrs = (Orange.data.ContinuousVariable(self.attr1),)
        if len(np.unique(Y)) >= 2:
            domain = Orange.data.Domain(
                attrs,
                Orange.data.DiscreteVariable(
                    "Class", values=list(self.class_model))
            )
            data = Orange.data.Table.from_numpy(domain, X, Y)
        else:
            domain = Orange.data.Domain(attrs)
            data = Orange.data.Table.from_numpy(domain, X)
        data.name = self.table_name
        self.send("Data", data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(1200, 800))

    def onDeleteWidget(self):
        self.plot.clear()

    def send_report(self):
        if self.data is None:
            return
        settings = []
        if self.attr1 != "x" or self.attr2 != "y":
            settings += [("Axis x", self.attr1), ("Axis y", self.attr2)]
        settings += [("Number of points", len(self.data))]
        self.report_items("Painted data", settings)
        self.report_plot()
Пример #6
0
class DiscreteVariableEditor(VariableEditor):
    """An editor widget for editing a discrete variable.

    Extends the :class:`VariableEditor` to enable editing of
    variables values.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        form = self.layout().itemAt(0)
        assert isinstance(form, QFormLayout)

        #: A list model of discrete variable's values.
        self.values_model = itemmodels.PyListModel(
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)

        vlayout = QVBoxLayout(spacing=1, margin=0)
        self.values_edit = QListView(editTriggers=QListView.DoubleClicked
                                     | QListView.EditKeyPressed)
        self.values_edit.setItemDelegate(CategoriesEditDelegate(self))
        self.values_edit.setModel(self.values_model)
        self.values_model.dataChanged.connect(self.on_values_changed)

        self.values_edit.selectionModel().selectionChanged.connect(
            self.on_value_selection_changed)
        self.values_model.layoutChanged.connect(
            self.on_value_selection_changed)
        self.values_model.rowsMoved.connect(self.on_value_selection_changed)

        vlayout.addWidget(self.values_edit)
        hlayout = QHBoxLayout(spacing=1, margin=0)

        self.categories_action_group = group = QActionGroup(
            self, objectName="action-group-categories", enabled=False)
        self.move_value_up = QAction(
            "\N{UPWARDS ARROW}",
            group,
            toolTip="Move the selected item up.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier
                                  | Qt.Key_BracketLeft),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_up.triggered.connect(self.move_up)

        self.move_value_down = QAction(
            "\N{DOWNWARDS ARROW}",
            group,
            toolTip="Move the selected item down.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier
                                  | Qt.Key_BracketRight),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_down.triggered.connect(self.move_down)

        self.add_new_item = QAction(
            "+",
            group,
            objectName="action-add-item",
            toolTip="Append a new item.",
            shortcut=QKeySequence(QKeySequence.New),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.remove_item = QAction(
            "\N{MINUS SIGN}",
            group,
            objectName="action-remove-item",
            toolTip="Delete the selected item.",
            shortcut=QKeySequence(QKeySequence.Delete),
            shortcutContext=Qt.WidgetShortcut,
        )

        self.add_new_item.triggered.connect(self._add_category)
        self.remove_item.triggered.connect(self._remove_category)

        button1 = FixedSizeButton(self,
                                  defaultAction=self.move_value_up,
                                  accessibleName="Move up")
        button2 = FixedSizeButton(self,
                                  defaultAction=self.move_value_down,
                                  accessibleName="Move down")
        button3 = FixedSizeButton(self,
                                  defaultAction=self.add_new_item,
                                  accessibleName="Add")
        button4 = FixedSizeButton(self,
                                  defaultAction=self.remove_item,
                                  accessibleName="Remove")
        self.values_edit.addActions([
            self.move_value_up, self.move_value_down, self.add_new_item,
            self.remove_item
        ])
        hlayout.addWidget(button1)
        hlayout.addWidget(button2)
        hlayout.addSpacing(3)
        hlayout.addWidget(button3)
        hlayout.addWidget(button4)

        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)

        form.insertRow(1, "Values:", vlayout)

        QWidget.setTabOrder(self.name_edit, self.values_edit)
        QWidget.setTabOrder(self.values_edit, button1)
        QWidget.setTabOrder(button1, button2)
        QWidget.setTabOrder(button2, button3)
        QWidget.setTabOrder(button3, button4)

    def set_data(self, var, transform=()):
        # type: (Optional[Categorical], Sequence[Transform]) -> None
        """
        Set the variable to edit.
        """
        super().set_data(var, transform)
        tr = None  # type: Optional[CategoriesMapping]
        for tr_ in transform:
            if isinstance(tr_, CategoriesMapping):
                tr = tr_

        items = []
        if tr is not None:
            ci_index = {c: i for i, c in enumerate(var.categories)}
            for ci, cj in tr.mapping:
                if ci is None and cj is not None:
                    # level added
                    item = {
                        Qt.EditRole: cj,
                        EditStateRole: ItemEditState.Added,
                        SourcePosRole: None
                    }
                elif ci is not None and cj is None:
                    # ci level dropped
                    item = {
                        Qt.EditRole: ci,
                        EditStateRole: ItemEditState.Dropped,
                        SourcePosRole: ci_index[ci],
                        SourceNameRole: ci
                    }
                elif ci is not None and cj is not None:
                    # rename or reorder
                    item = {
                        Qt.EditRole: cj,
                        EditStateRole: ItemEditState.NoState,
                        SourcePosRole: ci_index[ci],
                        SourceNameRole: ci
                    }
                else:
                    assert False, "invalid mapping: {!r}".format(tr.mapping)
                items.append(item)
        elif var is not None:
            items = [{
                Qt.EditRole: c,
                EditStateRole: ItemEditState.NoState,
                SourcePosRole: i,
                SourceNameRole: c
            } for i, c in enumerate(var.categories)]
        else:
            items = []

        with disconnected(self.values_model.dataChanged,
                          self.on_values_changed):
            self.values_model.clear()
            self.values_model.insertRows(0, len(items))
            for i, item in enumerate(items):
                self.values_model.setItemData(self.values_model.index(i, 0),
                                              item)
        self.add_new_item.actionGroup().setEnabled(var is not None)

    def __categories_mapping(self):
        # type: () -> CategoriesMappingType
        model = self.values_model
        source = self.var.categories

        res = []
        for i in range(model.rowCount()):
            midx = model.index(i, 0)
            category = midx.data(Qt.EditRole)
            source_pos = midx.data(SourcePosRole)  # type: Optional[int]
            if source_pos is not None:
                source_name = source[source_pos]
            else:
                source_name = None
            state = midx.data(EditStateRole)
            if state == ItemEditState.Dropped:
                res.append((source_name, None))
            elif state == ItemEditState.Added:
                res.append((None, category))
            else:
                res.append((source_name, category))
        return res

    def get_data(self):
        """Retrieve the modified variable
        """
        var, tr = super().get_data()
        if var is None:
            return var, tr
        mapping = self.__categories_mapping()
        if any(_1 != _2 or _2 != _3
               for (_1, _2), _3 in zip_longest(mapping, var.categories)):
            tr.append(CategoriesMapping(mapping))
        return var, tr

    def clear(self):
        """Clear the model state.
        """
        super().clear()
        self.values_model.clear()

    def move_rows(self, rows, offset):
        if not rows:
            return
        assert len(rows) == 1
        i = rows[0].row()
        if offset > 0:
            offset += 1
        self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(),
                                   i + offset)
        self.variable_changed.emit()

    def move_up(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, -1)

    def move_down(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, 1)

    @Slot()
    def on_values_changed(self):
        self.variable_changed.emit()

    @Slot()
    def on_value_selection_changed(self):
        rows = self.values_edit.selectionModel().selectedRows()
        if rows:
            i = rows[0].row()
            self.move_value_up.setEnabled(i)
            self.move_value_down.setEnabled(
                i != self.values_model.rowCount() - 1)
        else:
            self.move_value_up.setEnabled(False)
            self.move_value_down.setEnabled(False)

    def _remove_category(self):
        """
        Remove the current selected category.

        If the item is an existing category present in the source variable it
        is marked as removed in the view. But if it was added in the set
        transformation it is removed entirely from the model and view.
        """
        view = self.values_edit
        rows = view.selectionModel().selectedRows(0)
        if not rows:
            return
        assert len(rows) == 1
        index = rows[0]  # type: QModelIndex
        model = index.model()
        state = index.data(EditStateRole)
        pos = index.data(Qt.UserRole)
        if pos is not None and pos >= 0:
            # existing level -> only mark/toggle its dropped state
            model.setData(
                index, ItemEditState.Dropped if state != ItemEditState.Dropped
                else ItemEditState.NoState, EditStateRole)
        elif state == ItemEditState.Added:
            # new level -> remove it
            model.removeRow(index.row())
        else:
            assert False, "invalid state '{}' for {}" \
                .format(state, index.row())

    def _add_category(self):
        """
        Add a new category
        """
        view = self.values_edit
        model = view.model()

        with disconnected(model.dataChanged, self.on_values_changed,
                          Qt.UniqueConnection):
            row = model.rowCount()
            if not model.insertRow(model.rowCount()):
                return
            index = model.index(row, 0)
            model.setItemData(
                index, {
                    Qt.EditRole: "",
                    SourcePosRole: None,
                    EditStateRole: ItemEditState.Added
                })
            view.setCurrentIndex(index)
            view.edit(index)
        self.on_values_changed()
Пример #7
0
    class Frame(QDockWidget):
        """
        Widget frame with a handle.
        """
        closeRequested = Signal()

        def __init__(self, parent=None, widget=None, title=None, **kwargs):

            super().__init__(parent, **kwargs)
            self.setFeatures(QDockWidget.DockWidgetClosable)
            self.setAllowedAreas(Qt.NoDockWidgetArea)

            self.__title = ""
            self.__icon = ""
            self.__focusframe = None

            self.__deleteaction = QAction("Remove",
                                          self,
                                          shortcut=QKeySequence.Delete,
                                          enabled=False,
                                          triggered=self.closeRequested)
            self.addAction(self.__deleteaction)

            if widget is not None:
                self.setWidget(widget)
            self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)

            if title:
                self.setTitle(title)

            self.setFocusPolicy(Qt.StrongFocus)

        def setTitle(self, title):
            if self.__title != title:
                self.__title = title
                self.setWindowTitle(title)
                self.update()

        def setIcon(self, icon):
            icon = QIcon(icon)
            if self.__icon != icon:
                self.__icon = icon
                self.setWindowIcon(icon)
                self.update()

        def paintEvent(self, event):
            super().paintEvent(event)
            painter = QStylePainter(self)
            opt = QStyleOptionFrame()
            opt.initFrom(self)
            painter.drawPrimitive(QStyle.PE_FrameDockWidget, opt)
            painter.end()

        def focusInEvent(self, event):
            event.accept()
            self.__focusframe = QFocusFrame(self)
            self.__focusframe.setWidget(self)
            self.__deleteaction.setEnabled(True)

        def focusOutEvent(self, event):
            event.accept()
            if self.__focusframe is not None:
                self.__focusframe.deleteLater()
                self.__focusframe = None
            self.__deleteaction.setEnabled(False)

        def closeEvent(self, event):
            super().closeEvent(event)
            event.ignore()
            self.closeRequested.emit()
Пример #8
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """

    def __init__(self, *args):
        super().__init__(*args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)

        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        # scale factor accumulating partial increments from wheel events
        self.__zoomLevel = 100
        # effective scale level(rounded to whole integers)
        self.__effectiveZoomLevel = 100

        self.__zoomInAction = QAction(
            self.tr("Zoom in"), self, objectName="action-zoom-in",
            shortcut=QKeySequence.ZoomIn,
            triggered=self.zoomIn,
        )

        self.__zoomOutAction = QAction(
            self.tr("Zoom out"), self, objectName="action-zoom-out",
            shortcut=QKeySequence.ZoomOut,
            triggered=self.zoomOut
        )
        self.__zoomResetAction = QAction(
            self.tr("Reset Zoom"), self, objectName="action-zoom-reset",
            triggered=self.zoomReset,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0)
        )

    def setScene(self, scene):
        super().setScene(scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()
        return super().mouseReleaseEvent(event)

    def wheelEvent(self, event: QWheelEvent):
        if event.modifiers() & Qt.ControlModifier \
                and event.buttons() == Qt.NoButton:
            delta = event.angleDelta().y()
            # use mouse position as anchor while zooming
            anchor = self.transformationAnchor()
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120)
            self.setTransformationAnchor(anchor)
            event.accept()
        else:
            super().wheelEvent(event)

    def zoomIn(self):
        self.__setZoomLevel(self.__zoomLevel + 10)

    def zoomOut(self):
        self.__setZoomLevel(self.__zoomLevel - 10)

    def zoomReset(self):
        """
        Reset the zoom level.
        """
        self.__setZoomLevel(100)

    def zoomLevel(self):
        # type: () -> float
        """
        Return the current zoom level.

        Level is expressed in percentages; 100 is unscaled, 50 is half size, ...
        """
        return self.__effectiveZoomLevel

    def setZoomLevel(self, level):
        self.__setZoomLevel(level)

    def __setZoomLevel(self, scale):
        self.__zoomLevel = max(30, min(scale, 300))
        scale = round(self.__zoomLevel)
        self.__zoomOutAction.setEnabled(scale != 30)
        self.__zoomInAction.setEnabled(scale != 300)
        if self.__effectiveZoomLevel != scale:
            self.__effectiveZoomLevel = scale
            transform = QTransform()
            transform.scale(scale / 100, scale / 100)
            self.setTransform(transform)
            self.zoomLevelChanged.emit(scale)

    zoomLevelChanged = Signal(float)
    zoomLevel_ = Property(
        float, zoomLevel, setZoomLevel, notify=zoomLevelChanged
    )

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        super().drawBackground(painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(
                vrect.size().toSize().boundedTo(QSize(200, 200))
            )
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
Пример #9
0
class DiscreteVariableEditor(VariableEditor):
    """An editor widget for editing a discrete variable.

    Extends the :class:`VariableEditor` to enable editing of
    variables values.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        form = self.layout().itemAt(0)
        assert isinstance(form, QFormLayout)

        #: A list model of discrete variable's values.
        self.values_model = itemmodels.PyListModel(
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
        )

        vlayout = QVBoxLayout(spacing=1, margin=0)
        self.values_edit = QListView(
            editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed
        )
        self.values_edit.setItemDelegate(CategoriesEditDelegate(self))
        self.values_edit.setModel(self.values_model)
        self.values_model.dataChanged.connect(self.on_values_changed)

        self.values_edit.selectionModel().selectionChanged.connect(
            self.on_value_selection_changed)
        self.values_model.layoutChanged.connect(self.on_value_selection_changed)
        self.values_model.rowsMoved.connect(self.on_value_selection_changed)

        vlayout.addWidget(self.values_edit)
        hlayout = QHBoxLayout(spacing=1, margin=0)

        self.categories_action_group = group = QActionGroup(
            self, objectName="action-group-categories", enabled=False
        )
        self.move_value_up = QAction(
            "\N{UPWARDS ARROW}", group,
            toolTip="Move the selected item up.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |
                                  Qt.Key_BracketLeft),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_up.triggered.connect(self.move_up)

        self.move_value_down = QAction(
            "\N{DOWNWARDS ARROW}", group,
            toolTip="Move the selected item down.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |
                                  Qt.Key_BracketRight),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_down.triggered.connect(self.move_down)

        self.add_new_item = QAction(
            "+", group,
            objectName="action-add-item",
            toolTip="Append a new item.",
            shortcut=QKeySequence(QKeySequence.New),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.remove_item = QAction(
            "\N{MINUS SIGN}", group,
            objectName="action-remove-item",
            toolTip="Delete the selected item.",
            shortcut=QKeySequence(QKeySequence.Delete),
            shortcutContext=Qt.WidgetShortcut,
        )

        self.add_new_item.triggered.connect(self._add_category)
        self.remove_item.triggered.connect(self._remove_category)

        button1 = FixedSizeButton(
            self, defaultAction=self.move_value_up,
            accessibleName="Move up"
        )
        button2 = FixedSizeButton(
            self, defaultAction=self.move_value_down,
            accessibleName="Move down"
        )
        button3 = FixedSizeButton(
            self, defaultAction=self.add_new_item,
            accessibleName="Add"
        )
        button4 = FixedSizeButton(
            self, defaultAction=self.remove_item,
            accessibleName="Remove"
        )
        self.values_edit.addActions([self.move_value_up, self.move_value_down,
                                     self.add_new_item, self.remove_item])
        hlayout.addWidget(button1)
        hlayout.addWidget(button2)
        hlayout.addSpacing(3)
        hlayout.addWidget(button3)
        hlayout.addWidget(button4)

        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)

        form.insertRow(1, "Values:", vlayout)

        QWidget.setTabOrder(self.name_edit, self.values_edit)
        QWidget.setTabOrder(self.values_edit, button1)
        QWidget.setTabOrder(button1, button2)
        QWidget.setTabOrder(button2, button3)
        QWidget.setTabOrder(button3, button4)

    def set_data(self, var, transform=()):
        # type: (Optional[Categorical], Sequence[Transform]) -> None
        """
        Set the variable to edit.
        """
        super().set_data(var, transform)
        tr = None  # type: Optional[CategoriesMapping]
        for tr_ in transform:
            if isinstance(tr_, CategoriesMapping):
                tr = tr_

        items = []
        if tr is not None:
            ci_index = {c: i for i, c in enumerate(var.categories)}
            for ci, cj in tr.mapping:
                if ci is None and cj is not None:
                    # level added
                    item = {
                        Qt.EditRole: cj,
                        EditStateRole: ItemEditState.Added,
                        SourcePosRole: None
                    }
                elif ci is not None and cj is None:
                    # ci level dropped
                    item = {
                        Qt.EditRole: ci,
                        EditStateRole: ItemEditState.Dropped,
                        SourcePosRole: ci_index[ci],
                        SourceNameRole: ci
                    }
                elif ci is not None and cj is not None:
                    # rename or reorder
                    item = {
                        Qt.EditRole: cj,
                        EditStateRole: ItemEditState.NoState,
                        SourcePosRole: ci_index[ci],
                        SourceNameRole: ci
                    }
                else:
                    assert False, "invalid mapping: {!r}".format(tr.mapping)
                items.append(item)
        elif var is not None:
            items = [
                {Qt.EditRole: c,
                 EditStateRole: ItemEditState.NoState,
                 SourcePosRole: i,
                 SourceNameRole: c}
                for i, c in enumerate(var.categories)
            ]
        else:
            items = []

        with disconnected(self.values_model.dataChanged,
                          self.on_values_changed):
            self.values_model.clear()
            self.values_model.insertRows(0, len(items))
            for i, item in enumerate(items):
                self.values_model.setItemData(
                    self.values_model.index(i, 0),
                    item
                )
        self.add_new_item.actionGroup().setEnabled(var is not None)

    def __categories_mapping(self):
        # type: () -> CategoriesMappingType
        model = self.values_model
        source = self.var.categories

        res = []
        for i in range(model.rowCount()):
            midx = model.index(i, 0)
            category = midx.data(Qt.EditRole)
            source_pos = midx.data(SourcePosRole)  # type: Optional[int]
            if source_pos is not None:
                source_name = source[source_pos]
            else:
                source_name = None
            state = midx.data(EditStateRole)
            if state == ItemEditState.Dropped:
                res.append((source_name, None))
            elif state == ItemEditState.Added:
                res.append((None, category))
            else:
                res.append((source_name, category))
        return res

    def get_data(self):
        """Retrieve the modified variable
        """
        var, tr = super().get_data()
        if var is None:
            return var, tr
        mapping = self.__categories_mapping()
        if any(_1 != _2 or _2 != _3
               for (_1, _2), _3 in zip_longest(mapping, var.categories)):
            tr.append(CategoriesMapping(mapping))
        return var, tr

    def clear(self):
        """Clear the model state.
        """
        super().clear()
        self.values_model.clear()

    def move_rows(self, rows, offset):
        if not rows:
            return
        assert len(rows) == 1
        i = rows[0].row()
        if offset > 0:
            offset += 1
        self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset)
        self.variable_changed.emit()

    def move_up(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, -1)

    def move_down(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, 1)

    @Slot()
    def on_values_changed(self):
        self.variable_changed.emit()

    @Slot()
    def on_value_selection_changed(self):
        rows = self.values_edit.selectionModel().selectedRows()
        if rows:
            i = rows[0].row()
            self.move_value_up.setEnabled(i)
            self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1)
        else:
            self.move_value_up.setEnabled(False)
            self.move_value_down.setEnabled(False)

    def _remove_category(self):
        """
        Remove the current selected category.

        If the item is an existing category present in the source variable it
        is marked as removed in the view. But if it was added in the set
        transformation it is removed entirely from the model and view.
        """
        view = self.values_edit
        rows = view.selectionModel().selectedRows(0)
        if not rows:
            return
        assert len(rows) == 1
        index = rows[0]  # type: QModelIndex
        model = index.model()
        state = index.data(EditStateRole)
        pos = index.data(Qt.UserRole)
        if pos is not None and pos >= 0:
            # existing level -> only mark/toggle its dropped state
            model.setData(
                index,
                ItemEditState.Dropped if state != ItemEditState.Dropped
                else ItemEditState.NoState,
                EditStateRole)
        elif state == ItemEditState.Added:
            # new level -> remove it
            model.removeRow(index.row())
        else:
            assert False, "invalid state '{}' for {}" \
                .format(state, index.row())

    def _add_category(self):
        """
        Add a new category
        """
        view = self.values_edit
        model = view.model()

        with disconnected(model.dataChanged, self.on_values_changed,
                          Qt.UniqueConnection):
            row = model.rowCount()
            if not model.insertRow(model.rowCount()):
                return
            index = model.index(row, 0)
            model.setItemData(
                index, {
                    Qt.EditRole: "",
                    SourcePosRole: None,
                    EditStateRole: ItemEditState.Added
                }
            )
            view.setCurrentIndex(index)
            view.edit(index)
        self.on_values_changed()
Пример #10
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.grabGesture(Qt.PinchGesture)
        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        # scale factor accumulating partial increments from wheel events
        self.__zoomLevel = 100
        # effective scale level(rounded to whole integers)
        self.__effectiveZoomLevel = 100

        self.__zoomInAction = QAction(
            self.tr("Zoom in"),
            self,
            objectName="action-zoom-in",
            shortcut=QKeySequence.ZoomIn,
            triggered=self.zoomIn,
        )

        self.__zoomOutAction = QAction(self.tr("Zoom out"),
                                       self,
                                       objectName="action-zoom-out",
                                       shortcut=QKeySequence.ZoomOut,
                                       triggered=self.zoomOut)
        self.__zoomResetAction = QAction(
            self.tr("Reset Zoom"),
            self,
            objectName="action-zoom-reset",
            triggered=self.zoomReset,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0))

    def setScene(self, scene):
        super().setScene(scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()
        return super().mouseReleaseEvent(event)

    def __should_scroll_horizontally(self, event: QWheelEvent):
        if event.source() != Qt.MouseEventNotSynthesized:
            return False
        if (event.modifiers() & Qt.ShiftModifier and sys.platform == 'darwin'
                or event.modifiers() & Qt.AltModifier
                and sys.platform != 'darwin'):
            return True
        if event.angleDelta().x() == 0:
            vBar = self.verticalScrollBar()
            yDelta = event.angleDelta().y()
            direction = yDelta >= 0
            edgeVBarValue = vBar.minimum() if direction else vBar.maximum()
            return vBar.value() == edgeVBarValue
        return False

    def wheelEvent(self, event: QWheelEvent):
        # Zoom
        if event.modifiers() & Qt.ControlModifier \
                and event.buttons() == Qt.NoButton:
            delta = event.angleDelta().y()
            # use mouse position as anchor while zooming
            anchor = self.transformationAnchor()
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120)
            self.setTransformationAnchor(anchor)
            event.accept()
        # Scroll horizontally
        elif self.__should_scroll_horizontally(event):
            x, y = event.angleDelta().x(), event.angleDelta().y()
            sign_value = x if x != 0 else y
            sign = 1 if sign_value >= 0 else -1
            new_angle_delta = QPoint(sign * max(abs(x), abs(y), sign_value), 0)
            new_pixel_delta = QPoint(0, 0)
            new_modifiers = event.modifiers() & ~(Qt.ShiftModifier
                                                  | Qt.AltModifier)
            new_event = QWheelEvent(event.pos(), event.globalPos(),
                                    new_pixel_delta, new_angle_delta,
                                    event.buttons(), new_modifiers,
                                    event.phase(), event.inverted(),
                                    event.source())
            event.accept()
            super().wheelEvent(new_event)
        else:
            super().wheelEvent(event)

    def gestureEvent(self, event: QGestureEvent):
        gesture = event.gesture(Qt.PinchGesture)
        if gesture is None:
            return
        if gesture.state() == Qt.GestureStarted:
            event.accept(gesture)
        elif gesture.changeFlags() & QPinchGesture.ScaleFactorChanged:
            anchor = gesture.centerPoint().toPoint()
            anchor = self.mapToScene(anchor)
            self.__setZoomLevel(self.__zoomLevel * gesture.scaleFactor(),
                                anchor=anchor)
            event.accept()
        elif gesture.state() == Qt.GestureFinished:
            event.accept()

    def event(self, event: QEvent) -> bool:
        if event.type() == QEvent.Gesture:
            self.gestureEvent(cast(QGestureEvent, event))
        return super().event(event)

    def zoomIn(self):
        self.__setZoomLevel(self.__zoomLevel + 10)

    def zoomOut(self):
        self.__setZoomLevel(self.__zoomLevel - 10)

    def zoomReset(self):
        """
        Reset the zoom level.
        """
        self.__setZoomLevel(100)

    def zoomLevel(self):
        # type: () -> float
        """
        Return the current zoom level.

        Level is expressed in percentages; 100 is unscaled, 50 is half size, ...
        """
        return self.__effectiveZoomLevel

    def setZoomLevel(self, level):
        self.__setZoomLevel(level)

    def __setZoomLevel(self, scale, anchor=None):
        # type: (float, Optional[QPointF]) -> None
        self.__zoomLevel = max(30, min(scale, 300))
        scale = round(self.__zoomLevel)
        self.__zoomOutAction.setEnabled(scale != 30)
        self.__zoomInAction.setEnabled(scale != 300)
        if self.__effectiveZoomLevel != scale:
            self.__effectiveZoomLevel = scale
            transform = QTransform()
            transform.scale(scale / 100, scale / 100)
            if anchor is not None:
                anchor = self.mapFromScene(anchor)
            self.setTransform(transform)
            if anchor is not None:
                center = self.viewport().rect().center()
                diff = self.mapToScene(center) - self.mapToScene(anchor)
                self.centerOn(anchor + diff)
            self.zoomLevelChanged.emit(scale)

    zoomLevelChanged = Signal(float)
    zoomLevel_ = Property(float,
                          zoomLevel,
                          setZoomLevel,
                          notify=zoomLevelChanged)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        super().drawBackground(painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo(
                QSize(200, 200)))
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
Пример #11
0
class DiscreteVariableEditor(VariableEditor):
    """An editor widget for editing a discrete variable.

    Extends the :class:`VariableEditor` to enable editing of
    variables values.

    """
    def setup_gui(self):
        layout = QVBoxLayout()
        self.setLayout(layout)

        self.main_form = QFormLayout()
        self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        layout.addLayout(self.main_form)

        self._setup_gui_name()
        self._setup_gui_values()
        self._setup_gui_labels()

    def _setup_gui_values(self):
        vlayout = QVBoxLayout()
        vlayout.setContentsMargins(0, 0, 0, 0)
        vlayout.setSpacing(1)

        self.values_edit = QListView()
        self.values_edit.setEditTriggers(QTreeView.CurrentChanged)
        self.values_model = itemmodels.PyListModel(flags=Qt.ItemIsSelectable | \
                                        Qt.ItemIsEnabled | Qt.ItemIsEditable)
        self.values_edit.setModel(self.values_model)

        self.values_edit.selectionModel().selectionChanged.connect(
            self.on_value_selection_changed)

        self.values_model.dataChanged.connect(self.on_values_changed)

        vlayout.addWidget(self.values_edit)
        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        hlayout.setSpacing(1)
        self.move_value_up = QAction(unicodedata.lookup("UPWARDS ARROW"),
                                     self,
                                     toolTip="Move up.",
                                     triggered=self.move_up,
                                     enabled=False,
                                     shortcut=QKeySequence(QKeySequence.New))

        self.move_value_down = QAction(unicodedata.lookup("DOWNWARDS ARROW"),
                                       self,
                                       toolTip="Move down.",
                                       triggered=self.move_down,
                                       enabled=False,
                                       shortcut=QKeySequence(
                                           QKeySequence.Delete))

        button_size = gui.toolButtonSizeHint()
        button_size = QSize(button_size, button_size)

        button = QToolButton(self)
        button.setFixedSize(button_size)
        button.setDefaultAction(self.move_value_up)
        hlayout.addWidget(button)

        button = QToolButton(self)
        button.setFixedSize(button_size)
        button.setDefaultAction(self.move_value_down)
        hlayout.addWidget(button)
        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)

        self.main_form.addRow("Values:", vlayout)

    def set_data(self, var):
        """Set the variable to edit
        """
        VariableEditor.set_data(self, var)
        self.values_model[:] = list(var.values) if var is not None else []

    def get_data(self):
        """Retrieve the modified variable
        """
        name = str(self.name_edit.text()).strip()
        labels = self.labels_model.get_dict()
        values = map(str, self.values_model)

        if self.var is not None and not self.is_same():
            var = type(self.var)(name, values=values)
            var.attributes.update(labels)
            self.var = var
        else:
            var = self.var

        return var

    def is_same(self):
        """Is the current model state the same as the input.
        """
        values = list(map(str, self.values_model))
        return (VariableEditor.is_same(self) and self.var is not None
                and self.var.values == values)

    def clear(self):
        """Clear the model state.
        """
        VariableEditor.clear(self)
        self.values_model.clear()

    def move_rows(self, rows, offset):
        i = rows[0].row()
        self.values_model[i], self.values_model[i+offset] = \
            self.values_model[i+offset], self.values_model[i]
        self.maybe_commit()

    def move_up(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, -1)

    def move_down(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, 1)

    @Slot()
    def on_values_changed(self):
        self.maybe_commit()

    @Slot()
    def on_value_selection_changed(self):
        rows = self.values_edit.selectionModel().selectedRows()
        if rows:
            i = rows[0].row()
            self.move_value_up.setEnabled(i)
            self.move_value_down.setEnabled(i != len(self.var.values) - 1)
        else:
            self.move_value_up.setEnabled(False)
            self.move_value_down.setEnabled(False)
Пример #12
0
    class Frame(QDockWidget):
        """
        Widget frame with a handle.
        """
        closeRequested = Signal()

        def __init__(self, parent=None, widget=None, title=None, **kwargs):

            super().__init__(parent, **kwargs)
            self.setFeatures(QDockWidget.DockWidgetClosable)
            self.setAllowedAreas(Qt.NoDockWidgetArea)

            self.__title = ""
            self.__icon = ""
            self.__focusframe = None

            self.__deleteaction = QAction(
                "Remove", self, shortcut=QKeySequence.Delete,
                enabled=False, triggered=self.closeRequested
            )
            self.addAction(self.__deleteaction)

            if widget is not None:
                self.setWidget(widget)
            self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)

            if title:
                self.setTitle(title)

            self.setFocusPolicy(Qt.ClickFocus | Qt.TabFocus)

        def setTitle(self, title):
            if self.__title != title:
                self.__title = title
                self.setWindowTitle(title)
                self.update()

        def setIcon(self, icon):
            icon = QIcon(icon)
            if self.__icon != icon:
                self.__icon = icon
                self.setWindowIcon(icon)
                self.update()

        def paintEvent(self, event):
            super().paintEvent(event)
            painter = QStylePainter(self)
            opt = QStyleOptionFrame()
            opt.initFrom(self)
            painter.drawPrimitive(QStyle.PE_FrameDockWidget, opt)
            painter.end()

        def focusInEvent(self, event):
            event.accept()
            self.__focusframe = QFocusFrame(self)
            self.__focusframe.setWidget(self)
            self.__deleteaction.setEnabled(True)

        def focusOutEvent(self, event):
            event.accept()
            if self.__focusframe is not None:
                self.__focusframe.deleteLater()
                self.__focusframe = None
            self.__deleteaction.setEnabled(False)

        def closeEvent(self, event):
            super().closeEvent(event)
            event.ignore()
            self.closeRequested.emit()
Пример #13
0
class OWHeatMap(widget.OWWidget):
    name = "Heat Map"
    description = "Plot a data matrix heatmap."
    icon = "icons/Heatmap.svg"
    priority = 260
    keywords = []

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

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

    settings_version = 3

    settingsHandler = settings.DomainContextHandler()

    # Disable clustering for inputs bigger than this
    MaxClustering = 25000
    # Disable cluster leaf ordering for inputs bigger than this
    MaxOrderedClustering = 1000

    threshold_low = settings.Setting(0.0)
    threshold_high = settings.Setting(1.0)

    merge_kmeans = settings.Setting(False)
    merge_kmeans_k = settings.Setting(50)

    # Display column with averages
    averages: bool = settings.Setting(True)
    # Display legend
    legend: bool = settings.Setting(True)
    # Annotations
    #: text row annotation (row names)
    annotation_var = settings.ContextSetting(None)
    #: color row annotation
    annotation_color_var = settings.ContextSetting(None)
    # Discrete variable used to split that data/heatmaps (vertically)
    split_by_var = settings.ContextSetting(None)
    # Selected row/column clustering method (name)
    col_clustering_method: str = settings.Setting(Clustering.None_.name)
    row_clustering_method: str = settings.Setting(Clustering.None_.name)

    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)
    column_label_pos: int = settings.Setting(1)
    selected_rows: List[int] = settings.Setting(None, schema_only=True)

    auto_commit = settings.Setting(True)

    graph_name = "scene"

    left_side_scrolling = True

    class Information(widget.OWWidget.Information):
        sampled = Msg("Data has been sampled")
        discrete_ignored = Msg("{} categorical feature{} ignored")
        row_clust = Msg("{}")
        col_clust = Msg("{}")
        sparse_densified = Msg("Showing this data may require a lot of memory")

    class Error(widget.OWWidget.Error):
        no_continuous = Msg("No numeric features")
        not_enough_features = Msg("Not enough features for column clustering")
        not_enough_instances = Msg("Not enough instances for clustering")
        not_enough_instances_k_means = Msg(
            "Not enough instances for k-means merging")
        not_enough_memory = Msg("Not enough memory to show this data")

    class Warning(widget.OWWidget.Warning):
        empty_clusters = Msg("Empty clusters were removed")

    def __init__(self):
        super().__init__()
        self.__pending_selection = self.selected_rows

        # A kingdom for a save_state/restore_state
        self.col_clustering = enum_get(Clustering, self.col_clustering_method,
                                       Clustering.None_)
        self.row_clustering = enum_get(Clustering, self.row_clustering_method,
                                       Clustering.None_)

        @self.settingsAboutToBePacked.connect
        def _():
            self.col_clustering_method = self.col_clustering.name
            self.row_clustering_method = self.row_clustering.name

        self.keep_aspect = False

        #: The original data with all features (retained to
        #: preserve the domain on the output)
        self.input_data = None
        #: The effective data striped of discrete features, and often
        #: merged using k-means
        self.data = None
        self.effective_data = None
        #: kmeans model used to merge rows of input_data
        self.kmeans_model = None
        #: merge indices derived from kmeans
        #: a list (len==k) of int ndarray where the i-th item contains
        #: the indices which merge the input_data into the heatmap row i
        self.merge_indices = None
        self.parts: Optional[Parts] = None
        self.__rows_cache = {}
        self.__columns_cache = {}

        # GUI definition
        colorbox = gui.vBox(self.controlArea, "Color")
        self.color_cb = gui.palette_combo_box(self.palette_name)
        self.color_cb.currentIndexChanged.connect(self.update_color_schema)
        colorbox.layout().addWidget(self.color_cb)

        form = QFormLayout(formAlignment=Qt.AlignLeft,
                           labelAlignment=Qt.AlignLeft,
                           fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow)
        lowslider = gui.hSlider(colorbox,
                                self,
                                "threshold_low",
                                minValue=0.0,
                                maxValue=1.0,
                                step=0.05,
                                ticks=True,
                                intOnly=False,
                                createLabel=False,
                                callback=self.update_lowslider)
        highslider = gui.hSlider(colorbox,
                                 self,
                                 "threshold_high",
                                 minValue=0.0,
                                 maxValue=1.0,
                                 step=0.05,
                                 ticks=True,
                                 intOnly=False,
                                 createLabel=False,
                                 callback=self.update_highslider)

        form.addRow("Low:", lowslider)
        form.addRow("High:", highslider)

        colorbox.layout().addLayout(form)

        mergebox = gui.vBox(
            self.controlArea,
            "Merge",
        )
        gui.checkBox(mergebox,
                     self,
                     "merge_kmeans",
                     "Merge by k-means",
                     callback=self.__update_row_clustering)
        ibox = gui.indentedBox(mergebox)
        gui.spin(ibox,
                 self,
                 "merge_kmeans_k",
                 minv=5,
                 maxv=500,
                 label="Clusters:",
                 keyboardTracking=False,
                 callbackOnReturn=True,
                 callback=self.update_merge)

        cluster_box = gui.vBox(self.controlArea, "Clustering")
        # Row clustering
        self.row_cluster_cb = cb = ComboBox()
        cb.setModel(create_list_model(ClusteringModelData, self))
        cbselect(cb, self.row_clustering, ClusteringRole)
        self.connect_control(
            "row_clustering",
            lambda value, cb=cb: cbselect(cb, value, ClusteringRole))

        @cb.activated.connect
        def _(idx, cb=cb):
            self.set_row_clustering(cb.itemData(idx, ClusteringRole))

        # Column clustering
        self.col_cluster_cb = cb = ComboBox()
        cb.setModel(create_list_model(ClusteringModelData, self))
        cbselect(cb, self.col_clustering, ClusteringRole)
        self.connect_control(
            "col_clustering",
            lambda value, cb=cb: cbselect(cb, value, ClusteringRole))

        @cb.activated.connect
        def _(idx, cb=cb):
            self.set_col_clustering(cb.itemData(idx, ClusteringRole))

        form = QFormLayout(
            labelAlignment=Qt.AlignLeft,
            formAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
        )
        form.addRow("Rows:", self.row_cluster_cb)
        form.addRow("Columns:", self.col_cluster_cb)
        cluster_box.layout().addLayout(form)
        box = gui.vBox(self.controlArea, "Split By")

        self.row_split_model = DomainModel(
            placeholder="(None)",
            valid_types=(Orange.data.DiscreteVariable, ),
            parent=self,
        )
        self.row_split_cb = cb = ComboBox(
            enabled=not self.merge_kmeans,
            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,
            minimumContentsLength=14,
            toolTip="Split the heatmap vertically by a categorical column")
        self.row_split_cb.setModel(self.row_split_model)
        self.connect_control("split_by_var",
                             lambda value, cb=cb: cbselect(cb, value))
        self.connect_control("merge_kmeans", self.row_split_cb.setDisabled)
        self.split_by_var = None

        self.row_split_cb.activated.connect(self.__on_split_rows_activated)
        box.layout().addWidget(self.row_split_cb)

        box = gui.vBox(self.controlArea, 'Annotation && Legends')

        gui.checkBox(box,
                     self,
                     'legend',
                     'Show legend',
                     callback=self.update_legend)

        gui.checkBox(box,
                     self,
                     'averages',
                     'Stripes with averages',
                     callback=self.update_averages_stripe)
        annotbox = QGroupBox("Row Annotations", flat=True)
        form = QFormLayout(annotbox,
                           formAlignment=Qt.AlignLeft,
                           labelAlignment=Qt.AlignLeft,
                           fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow)
        self.annotation_model = DomainModel(placeholder="(None)")
        self.annotation_text_cb = ComboBoxSearch(
            minimumContentsLength=12,
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength)
        self.annotation_text_cb.setModel(self.annotation_model)
        self.annotation_text_cb.activated.connect(self.set_annotation_var)
        self.connect_control("annotation_var", self.annotation_var_changed)

        self.row_side_color_model = DomainModel(
            order=(DomainModel.CLASSES, DomainModel.Separator,
                   DomainModel.METAS),
            placeholder="(None)",
            valid_types=DomainModel.PRIMITIVE,
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,
            parent=self,
        )
        self.row_side_color_cb = ComboBoxSearch(
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,
            minimumContentsLength=12)
        self.row_side_color_cb.setModel(self.row_side_color_model)
        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)
        self.connect_control("annotation_color_var",
                             self.annotation_color_var_changed)
        form.addRow("Text", self.annotation_text_cb)
        form.addRow("Color", self.row_side_color_cb)
        box.layout().addWidget(annotbox)
        posbox = gui.vBox(box, "Column Labels Position", addSpace=False)
        posbox.setFlat(True)
        cb = gui.comboBox(posbox,
                          self,
                          "column_label_pos",
                          callback=self.update_column_annotations)
        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))
        cb.setCurrentIndex(self.column_label_pos)
        gui.checkBox(self.controlArea,
                     self,
                     "keep_aspect",
                     "Keep aspect ratio",
                     box="Resize",
                     callback=self.__aspect_mode_changed)

        gui.rubber(self.controlArea)
        gui.auto_send(self.controlArea, self, "auto_commit")

        # Scene with heatmap
        class HeatmapScene(QGraphicsScene):
            widget: Optional[HeatmapGridWidget] = None

        self.scene = self.scene = HeatmapScene(parent=self)
        self.view = GraphicsView(
            self.scene,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            viewportUpdateMode=QGraphicsView.FullViewportUpdate,
            widgetResizable=True,
        )
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
        self.view.customContextMenuRequested.connect(
            self._on_view_context_menu)
        self.mainArea.layout().addWidget(self.view)
        self.selected_rows = []
        self.__font_inc = QAction("Increase Font",
                                  self,
                                  shortcut=QKeySequence("ctrl+>"))
        self.__font_dec = QAction("Decrease Font",
                                  self,
                                  shortcut=QKeySequence("ctrl+<"))
        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))
        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))
        if hasattr(QAction, "setShortcutVisibleInContextMenu"):
            apply_all([self.__font_inc, self.__font_dec],
                      lambda a: a.setShortcutVisibleInContextMenu(True))
        self.addActions([self.__font_inc, self.__font_dec])

    @property
    def center_palette(self):
        palette = self.color_cb.currentData()
        return bool(palette.flags & palette.Diverging)

    @property
    def _column_label_pos(self) -> HeatmapGridWidget.Position:
        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]

    def annotation_color_var_changed(self, value):
        cbselect(self.row_side_color_cb, value, Qt.EditRole)

    def annotation_var_changed(self, value):
        cbselect(self.annotation_text_cb, value, Qt.EditRole)

    def set_row_clustering(self, method: Clustering) -> None:
        assert isinstance(method, Clustering)
        if self.row_clustering != method:
            self.row_clustering = method
            cbselect(self.row_cluster_cb, method, ClusteringRole)
            self.__update_row_clustering()

    def set_col_clustering(self, method: Clustering) -> None:
        assert isinstance(method, Clustering)
        if self.col_clustering != method:
            self.col_clustering = method
            cbselect(self.col_cluster_cb, method, ClusteringRole)
            self.__update_column_clustering()

    def sizeHint(self) -> QSize:
        return super().sizeHint().expandedTo(QSize(900, 700))

    def color_palette(self):
        return self.color_cb.currentData().lookup_table()

    def color_map(self) -> GradientColorMap:
        return GradientColorMap(self.color_palette(),
                                (self.threshold_low, self.threshold_high),
                                0 if self.center_palette else None)

    def clear(self):
        self.data = None
        self.input_data = None
        self.effective_data = None
        self.kmeans_model = None
        self.merge_indices = None
        self.annotation_model.set_domain(None)
        self.annotation_var = None
        self.row_side_color_model.set_domain(None)
        self.annotation_color_var = None
        self.row_split_model.set_domain(None)
        self.split_by_var = None
        self.parts = None
        self.clear_scene()
        self.selected_rows = []
        self.__columns_cache.clear()
        self.__rows_cache.clear()
        self.__update_clustering_enable_state(None)

    def clear_scene(self):
        if self.scene.widget is not None:
            self.scene.widget.layoutDidActivate.disconnect(
                self.__on_layout_activate)
            self.scene.widget.selectionFinished.disconnect(
                self.on_selection_finished)
        self.scene.widget = None
        self.scene.clear()

        self.view.setSceneRect(QRectF())
        self.view.setHeaderSceneRect(QRectF())
        self.view.setFooterSceneRect(QRectF())

    @Inputs.data
    def set_dataset(self, data=None):
        """Set the input dataset to display."""
        self.closeContext()
        self.clear()
        self.clear_messages()

        if isinstance(data, SqlTable):
            if data.approx_len() < 4000:
                data = Table(data)
            else:
                self.Information.sampled()
                data_sample = data.sample_time(1, no_cache=True)
                data_sample.download_data(2000, partial=True)
                data = Table(data_sample)

        if data is not None and not len(data):
            data = None

        if data is not None and sp.issparse(data.X):
            try:
                data = data.to_dense()
            except MemoryError:
                data = None
                self.Error.not_enough_memory()
            else:
                self.Information.sparse_densified()

        input_data = data

        # Data contains no attributes or meta attributes only
        if data is not None and len(data.domain.attributes) == 0:
            self.Error.no_continuous()
            input_data = data = None

        # Data contains some discrete attributes which must be filtered
        if data is not None and \
                any(var.is_discrete for var in data.domain.attributes):
            ndisc = sum(var.is_discrete for var in data.domain.attributes)
            data = data.transform(
                Domain([
                    var for var in data.domain.attributes if var.is_continuous
                ], data.domain.class_vars, data.domain.metas))
            if not data.domain.attributes:
                self.Error.no_continuous()
                input_data = data = None
            else:
                self.Information.discrete_ignored(ndisc,
                                                  "s" if ndisc > 1 else "")

        self.data = data
        self.input_data = input_data

        if data is not None:
            self.annotation_model.set_domain(self.input_data.domain)
            self.row_side_color_model.set_domain(self.input_data.domain)
            self.annotation_var = None
            self.annotation_color_var = None
            self.row_split_model.set_domain(data.domain)
            if data.domain.has_discrete_class:
                self.split_by_var = data.domain.class_var
            else:
                self.split_by_var = None
            self.openContext(self.input_data)
            if self.split_by_var not in self.row_split_model:
                self.split_by_var = None

        self.update_heatmaps()
        if data is not None and self.__pending_selection is not None:
            assert self.scene.widget is not None
            self.scene.widget.selectRows(self.__pending_selection)
            self.selected_rows = self.__pending_selection
            self.__pending_selection = None

        self.unconditional_commit()

    def __on_split_rows_activated(self):
        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))

    def set_split_variable(self, var):
        if var != self.split_by_var:
            self.split_by_var = var
            self.update_heatmaps()

    def update_heatmaps(self):
        if self.data is not None:
            self.clear_scene()
            self.clear_messages()
            if self.col_clustering != Clustering.None_ and \
                    len(self.data.domain.attributes) < 2:
                self.Error.not_enough_features()
            elif (self.col_clustering != Clustering.None_ or
                  self.row_clustering != Clustering.None_) and \
                    len(self.data) < 2:
                self.Error.not_enough_instances()
            elif self.merge_kmeans and len(self.data) < 3:
                self.Error.not_enough_instances_k_means()
            else:
                parts = self.construct_heatmaps(self.data, self.split_by_var)
                self.construct_heatmaps_scene(parts, self.effective_data)
                self.selected_rows = []
        else:
            self.clear()

    def update_merge(self):
        self.kmeans_model = None
        self.merge_indices = None
        if self.data is not None and self.merge_kmeans:
            self.update_heatmaps()
            self.commit()

    def _make_parts(self, data, group_var=None):
        """
        Make initial `Parts` for data, split by group_var, group_key
        """
        if group_var is not None:
            assert group_var.is_discrete
            _col_data = table_column_data(data, group_var)
            row_indices = [
                np.flatnonzero(_col_data == i)
                for i in range(len(group_var.values))
            ]
            row_groups = [
                RowPart(title=name,
                        indices=ind,
                        cluster=None,
                        cluster_ordered=None)
                for name, ind in zip(group_var.values, row_indices)
            ]
        else:
            row_groups = [
                RowPart(title=None,
                        indices=range(0, len(data)),
                        cluster=None,
                        cluster_ordered=None)
            ]

        col_groups = [
            ColumnPart(title=None,
                       indices=range(0, len(data.domain.attributes)),
                       domain=data.domain,
                       cluster=None,
                       cluster_ordered=None)
        ]

        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)
        return Parts(row_groups, col_groups, span=(minv, maxv))

    def cluster_rows(self,
                     data: Table,
                     parts: 'Parts',
                     ordered=False) -> 'Parts':
        row_groups = []
        for row in parts.rows:
            if row.cluster is not None:
                cluster = row.cluster
            else:
                cluster = None
            if row.cluster_ordered is not None:
                cluster_ord = row.cluster_ordered
            else:
                cluster_ord = None

            if row.can_cluster:
                matrix = None
                need_dist = cluster is None or (ordered
                                                and cluster_ord is None)
                if need_dist:
                    subset = data[row.indices]
                    matrix = Orange.distance.Euclidean(subset)

                if cluster is None:
                    assert len(matrix) < self.MaxClustering
                    cluster = hierarchical.dist_matrix_clustering(
                        matrix, linkage=hierarchical.WARD)
                if ordered and cluster_ord is None:
                    assert len(matrix) < self.MaxOrderedClustering
                    cluster_ord = hierarchical.optimal_leaf_ordering(
                        cluster,
                        matrix,
                    )
            row_groups.append(
                row._replace(cluster=cluster, cluster_ordered=cluster_ord))

        return parts._replace(rows=row_groups)

    def cluster_columns(self, data, parts, ordered=False):
        assert len(parts.columns) == 1, "columns split is no longer supported"
        assert all(var.is_continuous for var in data.domain.attributes)

        col0 = parts.columns[0]
        if col0.cluster is not None:
            cluster = col0.cluster
        else:
            cluster = None
        if col0.cluster_ordered is not None:
            cluster_ord = col0.cluster_ordered
        else:
            cluster_ord = None
        need_dist = cluster is None or (ordered and cluster_ord is None)
        matrix = None
        if need_dist:
            data = Orange.distance._preprocess(data)
            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))
            # nan values break clustering below
            matrix = np.nan_to_num(matrix)

        if cluster is None:
            assert matrix is not None
            assert len(matrix) < self.MaxClustering
            cluster = hierarchical.dist_matrix_clustering(
                matrix, linkage=hierarchical.WARD)
        if ordered and cluster_ord is None:
            assert len(matrix) < self.MaxOrderedClustering
            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)

        col_groups = [
            col._replace(cluster=cluster, cluster_ordered=cluster_ord)
            for col in parts.columns
        ]
        return parts._replace(columns=col_groups)

    def construct_heatmaps(self, data, group_var=None) -> 'Parts':
        if self.merge_kmeans:
            if self.kmeans_model is None:
                effective_data = self.input_data.transform(
                    Orange.data.Domain([
                        var for var in self.input_data.domain.attributes
                        if var.is_continuous
                    ], self.input_data.domain.class_vars,
                                       self.input_data.domain.metas))
                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)
                self.kmeans_model = kmeans_compress(effective_data, k=nclust)
                effective_data.domain = self.kmeans_model.domain
                merge_indices = [
                    np.flatnonzero(self.kmeans_model.labels == ind)
                    for ind in range(nclust)
                ]
                not_empty_indices = [
                    i for i, x in enumerate(merge_indices) if len(x) > 0
                ]
                self.merge_indices = \
                    [merge_indices[i] for i in not_empty_indices]
                if len(merge_indices) != len(self.merge_indices):
                    self.Warning.empty_clusters()
                effective_data = Orange.data.Table(
                    Orange.data.Domain(effective_data.domain.attributes),
                    self.kmeans_model.centroids[not_empty_indices])
            else:
                effective_data = self.effective_data

            group_var = None
        else:
            self.kmeans_model = None
            self.merge_indices = None
            effective_data = data

        self.effective_data = effective_data

        self.__update_clustering_enable_state(effective_data)

        parts = self._make_parts(effective_data, group_var)
        # Restore/update the row/columns items descriptions from cache if
        # available
        rows_cache_key = (group_var,
                          self.merge_kmeans_k if self.merge_kmeans else None)
        if rows_cache_key in self.__rows_cache:
            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)

        if self.row_clustering != Clustering.None_:
            parts = self.cluster_rows(
                effective_data,
                parts,
                ordered=self.row_clustering == Clustering.OrderedClustering)
        if self.col_clustering != Clustering.None_:
            parts = self.cluster_columns(
                effective_data,
                parts,
                ordered=self.col_clustering == Clustering.OrderedClustering)

        # Cache the updated parts
        self.__rows_cache[rows_cache_key] = parts
        return parts

    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:
        _T = TypeVar("_T", bound=Union[RowPart, ColumnPart])

        def select_cluster(clustering: Clustering, item: _T) -> _T:
            if clustering == Clustering.None_:
                return item._replace(cluster=None, cluster_ordered=None)
            elif clustering == Clustering.Clustering:
                return item._replace(cluster=item.cluster,
                                     cluster_ordered=None)
            elif clustering == Clustering.OrderedClustering:
                return item._replace(cluster=item.cluster_ordered,
                                     cluster_ordered=None)
            else:  # pragma: no cover
                raise TypeError()

        rows = [
            select_cluster(self.row_clustering, rowitem)
            for rowitem in parts.rows
        ]
        cols = [
            select_cluster(self.col_clustering, colitem)
            for colitem in parts.columns
        ]
        parts = Parts(columns=cols, rows=rows, span=parts.span)

        self.setup_scene(parts, data)

    def setup_scene(self, parts, data):
        # type: (Parts, Table) -> None
        widget = HeatmapGridWidget()
        widget.setColorMap(self.color_map())
        self.scene.addItem(widget)
        self.scene.widget = widget
        columns = [v.name for v in data.domain.attributes]
        parts = HeatmapGridWidget.Parts(
            rows=[
                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)
                for r in parts.rows
            ],
            columns=[
                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)
                for c in parts.columns
            ],
            data=data.X,
            span=parts.span,
            row_names=None,
            col_names=columns,
        )
        widget.setHeatmaps(parts)
        side = self.row_side_colors()
        if side is not None:
            widget.setRowSideColorAnnotations(side[0],
                                              side[1],
                                              name=side[2].name)
        widget.setColumnLabelsPosition(self._column_label_pos)
        widget.setAspectRatioMode(
            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio)
        widget.setShowAverages(self.averages)
        widget.setLegendVisible(self.legend)

        widget.layoutDidActivate.connect(self.__on_layout_activate)
        widget.selectionFinished.connect(self.on_selection_finished)

        self.update_annotations()
        self.view.setCentralWidget(widget)
        self.parts = parts

    def __update_scene_rects(self):
        widget = self.scene.widget
        if widget is None:
            return
        rect = widget.geometry()
        self.scene.setSceneRect(rect)
        self.view.setSceneRect(rect)
        self.view.setHeaderSceneRect(widget.headerGeometry())
        self.view.setFooterSceneRect(widget.footerGeometry())

    def __on_layout_activate(self):
        self.__update_scene_rects()

    def __aspect_mode_changed(self):
        widget = self.scene.widget
        if widget is None:
            return
        widget.setAspectRatioMode(
            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio)
        # when aspect fixed the vertical sh is fixex, when not, it can
        # shrink vertically
        sp = widget.sizePolicy()
        if self.keep_aspect:
            sp.setVerticalPolicy(QSizePolicy.Fixed)
        else:
            sp.setVerticalPolicy(QSizePolicy.Preferred)
        widget.setSizePolicy(sp)

    def __update_clustering_enable_state(self, data):
        if data is not None:
            N = len(data)
            M = len(data.domain.attributes)
        else:
            N = M = 0

        rc_enabled = N <= self.MaxClustering
        rco_enabled = N <= self.MaxOrderedClustering
        cc_enabled = M <= self.MaxClustering
        cco_enabled = M <= self.MaxOrderedClustering
        row_clust, col_clust = self.row_clustering, self.col_clustering

        row_clust_msg = ""
        col_clust_msg = ""

        if not rco_enabled and row_clust == Clustering.OrderedClustering:
            row_clust = Clustering.Clustering
            row_clust_msg = "Row cluster ordering was disabled due to the " \
                            "input matrix being to big"
        if not rc_enabled and row_clust == Clustering.Clustering:
            row_clust = Clustering.None_
            row_clust_msg = "Row clustering was was disabled due to the " \
                            "input matrix being to big"

        if not cco_enabled and col_clust == Clustering.OrderedClustering:
            col_clust = Clustering.Clustering
            col_clust_msg = "Column cluster ordering was disabled due to " \
                            "the input matrix being to big"
        if not cc_enabled and col_clust == Clustering.Clustering:
            col_clust = Clustering.None_
            col_clust_msg = "Column clustering was disabled due to the " \
                            "input matrix being to big"

        self.col_clustering = col_clust
        self.row_clustering = row_clust

        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))
        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))

        # Disable/enable the combobox items for the clustering methods
        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):
            model = cb.model()
            assert isinstance(model, QStandardItemModel)
            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)
            assert idx != -1
            model.item(idx).setEnabled(clu_op)
            idx = cb.findData(Clustering.Clustering, ClusteringRole)
            assert idx != -1
            model.item(idx).setEnabled(clu)

        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)
        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)

    def update_averages_stripe(self):
        """Update the visibility of the averages stripe.
        """
        widget = self.scene.widget
        if widget is not None:
            widget.setShowAverages(self.averages)

    def update_lowslider(self):
        low, high = self.controls.threshold_low, self.controls.threshold_high
        if low.value() >= high.value():
            low.setSliderPosition(high.value() - 1)
        self.update_color_schema()

    def update_highslider(self):
        low, high = self.controls.threshold_low, self.controls.threshold_high
        if low.value() >= high.value():
            high.setSliderPosition(low.value() + 1)
        self.update_color_schema()

    def update_color_schema(self):
        self.palette_name = self.color_cb.currentData().name
        w = self.scene.widget
        if w is not None:
            w.setColorMap(self.color_map())

    def __update_column_clustering(self):
        self.update_heatmaps()
        self.commit()

    def __update_row_clustering(self):
        self.update_heatmaps()
        self.commit()

    def update_legend(self):
        widget = self.scene.widget
        if widget is not None:
            widget.setLegendVisible(self.legend)

    def row_annotation_var(self):
        return self.annotation_var

    def row_annotation_data(self):
        var = self.row_annotation_var()
        if var is None:
            return None
        return column_str_from_table(self.input_data, var)

    def _merge_row_indices(self):
        if self.merge_kmeans and self.kmeans_model is not None:
            return self.merge_indices
        else:
            return None

    def set_annotation_var(self, var: Union[None, Variable, int]):
        if isinstance(var, int):
            var = self.annotation_model[var]
        if self.annotation_var != var:
            self.annotation_var = var
            self.update_annotations()

    def update_annotations(self):
        widget = self.scene.widget
        if widget is not None:
            annot_col = self.row_annotation_data()
            merge_indices = self._merge_row_indices()
            if merge_indices is not None and annot_col is not None:
                join = lambda _1: join_elided(", ", 42, _1, " ({} more)")
                annot_col = aggregate_apply(join, annot_col, merge_indices)
            if annot_col is not None:
                widget.setRowLabels(annot_col)
                widget.setRowLabelsVisible(True)
            else:
                widget.setRowLabelsVisible(False)
                widget.setRowLabels(None)

    def row_side_colors(self):
        var = self.annotation_color_var
        if var is None:
            return None
        column_data = column_data_from_table(self.input_data, var)
        span = (np.nanmin(column_data), np.nanmax(column_data))
        merges = self._merge_row_indices()
        if merges is not None:
            column_data = aggregate(var, column_data, merges)
        data, colormap = self._colorize(var, column_data)
        if var.is_continuous:
            colormap.span = span
        return data, colormap, var

    def set_annotation_color_var(self, var: Union[None, Variable, int]):
        """Set the current side color annotation variable."""
        if isinstance(var, int):
            var = self.row_side_color_model[var]
        if self.annotation_color_var != var:
            self.annotation_color_var = var
            self.update_row_side_colors()

    def update_row_side_colors(self):
        widget = self.scene.widget
        if widget is None:
            return
        colors = self.row_side_colors()
        if colors is None:
            widget.setRowSideColorAnnotations(None)
        else:
            widget.setRowSideColorAnnotations(colors[0], colors[1],
                                              colors[2].name)

    def _colorize(self, var: Variable,
                  data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:
        palette = var.palette  # type: Palette
        colors = np.array(
            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],
            dtype=np.uint8,
        )
        if var.is_discrete:
            mask = np.isnan(data)
            data[mask] = -1
            data = data.astype(int)
            if mask.any():
                values = (*var.values, "N/A")
            else:
                values = var.values
                colors = colors[:-1]
            return data, CategoricalColorMap(colors, values)
        elif var.is_continuous:
            cmap = GradientColorMap(colors[:-1])
            return data, cmap
        else:
            raise TypeError

    def update_column_annotations(self):
        widget = self.scene.widget
        if self.data is not None and widget is not None:
            widget.setColumnLabelsPosition(self._column_label_pos)

    def __adjust_font_size(self, diff):
        widget = self.scene.widget
        if widget is None:
            return
        curr = widget.font().pointSizeF()
        new = curr + diff

        self.__font_dec.setEnabled(new > 1.0)
        self.__font_inc.setEnabled(new <= 32)
        if new > 1.0:
            font = QFont()
            font.setPointSizeF(new)
            widget.setFont(font)

    def _on_view_context_menu(self, pos):
        widget = self.scene.widget
        if widget is None:
            return
        assert isinstance(widget, HeatmapGridWidget)
        menu = QMenu(self.view.viewport())
        menu.setAttribute(Qt.WA_DeleteOnClose)
        menu.addActions(self.view.actions())
        menu.addSeparator()
        menu.addActions([self.__font_inc, self.__font_dec])
        menu.addSeparator()
        a = QAction("Keep aspect ratio", menu, checkable=True)
        a.setChecked(self.keep_aspect)

        def ontoggled(state):
            self.keep_aspect = state
            self.__aspect_mode_changed()

        a.toggled.connect(ontoggled)
        menu.addAction(a)
        menu.popup(self.view.viewport().mapToGlobal(pos))

    def on_selection_finished(self):
        if self.scene.widget is not None:
            self.selected_rows = list(self.scene.widget.selectedRows())
        else:
            self.selected_rows = []
        self.commit()

    def commit(self):
        data = None
        indices = None
        if self.merge_kmeans:
            merge_indices = self.merge_indices
        else:
            merge_indices = None

        if self.input_data is not None and self.selected_rows:
            indices = self.selected_rows
            if merge_indices is not None:
                # expand merged indices
                indices = np.hstack([merge_indices[i] for i in indices])

            data = self.input_data[indices]

        self.Outputs.selected_data.send(data)
        self.Outputs.annotated_data.send(
            create_annotated_table(self.input_data, indices))

    def onDeleteWidget(self):
        self.clear()
        super().onDeleteWidget()

    def send_report(self):
        self.report_items((
            ("Columns:",
             "Clustering" if self.col_clustering else "No sorting"),
            ("Rows:", "Clustering" if self.row_clustering else "No sorting"),
            ("Split:", self.split_by_var is not None
             and self.split_by_var.name),
            ("Row annotation", self.annotation_var is not None
             and self.annotation_var.name),
        ))
        self.report_plot()

    @classmethod
    def migrate_settings(cls, settings, version):
        if version is not None and version < 3:

            def st2cl(state: bool) -> Clustering:
                return Clustering.OrderedClustering if state else \
                    Clustering.None_

            rc = settings.pop("row_clustering", False)
            cc = settings.pop("col_clustering", False)
            settings["row_clustering_method"] = st2cl(rc).name
            settings["col_clustering_method"] = st2cl(cc).name