Esempio n. 1
0
class MarkerControl(StraditizerControlBase, QWidget):
    """Widget to control the appearance of the marks

    This widget controls the appearance of the
    :class:`straditize.cross_mark.CrossMarks` instances in the
    :attr:`~straditize.straditizer.Straditizer.marks` attribute of the
    :attr:`~straditize.widgets.StraditizerControlBase.straditizer`"""

    _toolbar = None

    @property
    def marks(self):
        """The :class:`~straditize.cross_mark.CrossMarks` of the straditizer
        """
        return chain(self.straditizer.marks, self.straditizer.magni_marks)

    @docstrings.dedent
    def __init__(self, straditizer_widgets, item, *args, **kwargs):
        """
        Parameters
        ----------
        %(StraditizerControlBase.init_straditizercontrol.parameters)s
        """
        super(MarkerControl, self).__init__(*args, **kwargs)
        self.init_straditizercontrol(straditizer_widgets, item)
        vbox = QVBoxLayout()

        # auto hide button
        box_hide = QGridLayout()
        self.cb_auto_hide = QCheckBox('Auto-hide')
        self.cb_show_connected = QCheckBox('Show additionals')
        self.cb_drag_hline = QCheckBox('Drag in y-direction')
        self.cb_drag_vline = QCheckBox('Drag in x-direction')
        self.cb_selectable_hline = QCheckBox('Horizontal lines selectable')
        self.cb_selectable_vline = QCheckBox('Vertical lines selectable')
        self.cb_show_hlines = QCheckBox('Show horizontal lines')
        self.cb_show_vlines = QCheckBox('Show vertical lines')
        box_hide.addWidget(self.cb_auto_hide, 0, 0)
        box_hide.addWidget(self.cb_show_connected, 0, 1)
        box_hide.addWidget(self.cb_show_vlines, 1, 0)
        box_hide.addWidget(self.cb_show_hlines, 1, 1)
        box_hide.addWidget(self.cb_drag_hline, 2, 0)
        box_hide.addWidget(self.cb_drag_vline, 2, 1)
        box_hide.addWidget(self.cb_selectable_hline, 3, 0)
        box_hide.addWidget(self.cb_selectable_vline, 3, 1)
        vbox.addLayout(box_hide)

        style_box = QGridLayout()
        style_box.addWidget(QLabel('Unselected:'), 0, 1)
        selected_label = QLabel('Selected:')
        style_box.addWidget(selected_label, 0, 2)
        max_width = selected_label.sizeHint().width()

        # line color
        self.lbl_color_select = ColorLabel()
        self.lbl_color_unselect = ColorLabel()
        self.lbl_color_select.setMaximumWidth(max_width)
        self.lbl_color_unselect.setMaximumWidth(max_width)
        style_box.addWidget(QLabel('Line color:'), 1, 0)
        style_box.addWidget(self.lbl_color_unselect, 1, 1)
        style_box.addWidget(self.lbl_color_select, 1, 2)

        # line width
        self.txt_line_width = QLineEdit()
        self.txt_line_width_select = QLineEdit()

        validator = QDoubleValidator()
        validator.setBottom(0)
        self.txt_line_width.setValidator(validator)
        self.txt_line_width_select.setValidator(validator)

        style_box.addWidget(QLabel('Line width:'), 2, 0)
        style_box.addWidget(self.txt_line_width, 2, 1)
        style_box.addWidget(self.txt_line_width_select, 2, 2)

        vbox.addLayout(style_box)

        # line style
        hbox_line_style = QHBoxLayout()
        hbox_line_style.addWidget(QLabel('Line style'))
        self.combo_line_style = QComboBox()
        hbox_line_style.addWidget(self.combo_line_style)
        vbox.addLayout(hbox_line_style)
        self.fill_linestyles()
        self.combo_line_style.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        # marker style
        hbox_marker_style = QHBoxLayout()
        hbox_marker_style.addWidget(QLabel('Marker size'))
        self.txt_marker_size = QLineEdit()
        self.txt_marker_size.setMinimumWidth(40)
        self.txt_marker_size.setText(str(mpl.rcParams['lines.markersize']))
        validator = QDoubleValidator()
        validator.setBottom(0)
        self.txt_marker_size.setValidator(validator)
        hbox_marker_style.addWidget(self.txt_marker_size)
        hbox_marker_style.addWidget(QLabel('Marker style'))
        self.combo_marker_style = QComboBox()
        hbox_marker_style.addWidget(self.combo_marker_style)
        vbox.addLayout(hbox_marker_style)

        self.setLayout(vbox)

        self.widgets2disable = [
            self.lbl_color_select, self.lbl_color_unselect, self.cb_auto_hide,
            self.txt_line_width, self.txt_line_width_select,
            self.combo_line_style, self.cb_show_connected,
            self.txt_marker_size, self.combo_marker_style, self.cb_drag_vline,
            self.cb_drag_hline, self.cb_selectable_vline,
            self.cb_selectable_hline, self.cb_show_hlines, self.cb_show_vlines
        ]

        self.fill_markerstyles()

        self.tb_actions = []

        # ---------------------------------------------------------------------
        # ---------------------------- connections ----------------------------
        # ---------------------------------------------------------------------

        self.lbl_color_select.color_changed.connect(self.change_select_colors)
        self.lbl_color_unselect.color_changed.connect(
            self.change_unselect_colors)
        self.txt_line_width.textChanged.connect(self.change_line_widths)
        self.txt_line_width_select.textChanged.connect(
            self.change_selection_line_widths)
        self.cb_auto_hide.stateChanged.connect(self.change_auto_hide)
        self.combo_marker_style.currentIndexChanged.connect(
            self.change_marker_style)
        self.combo_line_style.currentIndexChanged.connect(
            self.change_line_style)
        self.txt_marker_size.textChanged.connect(self.change_marker_size)
        self.cb_show_connected.stateChanged.connect(
            self.change_show_connected_artists)
        self.cb_drag_hline.stateChanged.connect(self.change_hline_draggable)
        self.cb_drag_vline.stateChanged.connect(self.change_vline_draggable)
        self.cb_selectable_hline.stateChanged.connect(
            self.change_hline_selectable)
        self.cb_selectable_vline.stateChanged.connect(
            self.change_vline_selectable)
        self.cb_show_vlines.stateChanged.connect(self.change_show_vlines)
        self.cb_show_hlines.stateChanged.connect(self.change_show_hlines)

    def draw_figs(self):
        """Draw the figures of the :attr:`marks`"""
        for canvas in {m.fig.canvas for m in self.marks}:
            canvas.draw_idle()

    def fill_linestyles(self):
        """Fill the :attr:`combo_line_style` combobox"""
        self.line_styles = [('-', 'solid'), ('--', 'dashed'),
                            ('-.', 'dashdot'), (':', 'dotted'),
                            ('None', None, '', ' ')]
        self.combo_line_style.addItems([t[0] for t in self.line_styles])

    def fill_markerstyles(self):
        """Fill the :attr:`combo_marker_style` combobox"""
        self.marker_styles = [('point', (".", )), ('pixel', (",", )),
                              ('circle', ("o", )),
                              ('triangle down', ("v", "1")),
                              ('triangle up', ("^", "2")),
                              ('triangle left', ("<", "3")),
                              ('triangle right', (">", "4")),
                              ('octagon', ("8", )), ('square', ("s", )),
                              ('pentagon', ("p", )),
                              ('plus (filled)', ("P", )), ('star', ("*", )),
                              ('hexagon1', ("h", )), ('hexagon2', ("H", )),
                              ('plus', ("+", )), ('x', ("x", )),
                              ('x (filled)', ("X", )), ('diamond', ("D", )),
                              ('thin diamond', ("d", )), ('vline', ("|", )),
                              ('hline', ("_", )),
                              ('no marker', ('None', None, '', ' '))]
        self.combo_marker_style.addItems([
            '%s (%s)' % (key.capitalize(), ','.join(map(str, filter(None, t))))
            for key, t in self.marker_styles
        ])

    def set_marker_item(self, marker):
        """Switch the :attr:`combo_marker_style` to the given `marker`

        Parameters
        ----------
        marker: str
            A matplotlib marker string"""
        for i, (key, t) in enumerate(self.marker_styles):
            if marker in t:
                block = self.combo_marker_style.blockSignals(True)
                self.combo_marker_style.setCurrentIndex(i)
                self.combo_marker_style.blockSignals(block)
                break

    def set_line_style_item(self, ls):
        """Switch the :attr:`combo_line_style` to the given linestyle

        Parameters
        ----------
        ls: str
            The matplotlib linestyle string
        """
        for i, t in enumerate(self.line_styles):
            if ls in t:
                block = self.combo_line_style.blockSignals(True)
                self.combo_line_style.setCurrentIndex(i)
                self.combo_line_style.blockSignals(block)
                break

    def should_be_enabled(self, w):
        """Check if a widget `w` should be enabled or disabled

        Parameters
        ----------
        w: PyQt5.QtWidgets.QWidget
            The widget to (potentially) enable

        Returns
        -------
        bool
            True if the :attr:`straditizer` of this instance has marks.
            Otherwise False"""
        return self.straditizer is not None and bool(self.straditizer.marks)

    def change_select_colors(self, color):
        """Change the selection color of the marks

        Change the selection color of the marks to the given color.

        Parameters
        ----------
        color: PyQt5.QtGui.QColor or a matplotlib color
            The color to use
        """
        if isinstance(color, QtGui.QColor):
            color = np.array(color.getRgb()) / 255.
        for mark in self.marks:
            key = 'color' if 'color' in mark._select_props else 'c'
            mark._select_props[key] = color
            mark._unselect_props[key] = mark.hline.get_c()
        self.draw_figs()

    def change_unselect_colors(self, color):
        """Change the :attr:`straditize.cross_mark.CrossMark.cunselect` color

        Change the unselection color of the marks to the given color.

        Parameters
        ----------
        color: PyQt5.QtGui.QColor or a matplotlib color
            The color to use
        """
        if isinstance(color, QtGui.QColor):
            color = np.array(color.getRgb()) / 255.
        for mark in self.marks:
            key = 'color' if 'color' in mark._unselect_props else 'c'
            mark._unselect_props[key] = color
            for l in chain(mark.hlines, mark.vlines, mark.line_connections):
                l.set_color(color)
        self.draw_figs()

    def change_line_widths(self, lw):
        """Change the linewidth of the marks

        Parameters
        ----------
        lw: float
            The line width to use
        """
        lw = float(lw or 0)
        for mark in self.marks:
            key = 'lw' if 'lw' in mark._unselect_props else 'linewidth'
            mark._unselect_props[key] = lw
            if not mark.auto_hide:
                for l in chain(mark.hlines, mark.vlines,
                               mark.line_connections):
                    l.set_lw(lw)
        self.draw_figs()

    def change_selection_line_widths(self, lw):
        """Change the linewidth for selected marks

        Parameters
        ----------
        lw: float
            The linewidth for selected marks"""
        lw = float(lw or 0)
        for mark in self.marks:
            key = 'lw' if 'lw' in mark._select_props else 'linewidth'
            mark._select_props[key] = lw

    def change_auto_hide(self, auto_hide):
        """Toggle the :attr:`~straditize.cross_mark.CrossMark.auto_hide`

        This method disables or enables the
        :attr:`~straditize.cross_mark.CrossMark.auto_hide` of the marks

        Parameters
        ----------
        auto_hide: bool or PyQt5.QtGui.Qt.Checked or PyQt5.QtGui.Qt.Unchecked
            The value to use for the auto_hide. :data:`PyQt5.QtGui.Qt.Checked`
            is equivalent to ``True``
        """
        if auto_hide is Qt.Checked:
            auto_hide = True
        elif auto_hide is Qt.Unchecked:
            auto_hide = False
        for mark in self.marks:
            mark.auto_hide = auto_hide
            if auto_hide:
                for l in chain(mark.hlines, mark.vlines,
                               mark.line_connections):
                    l.set_lw(0)
            else:
                lw = mark._unselect_props.get(
                    'lw', mark._unselect_props.get('linewidth'))
                for l in chain(mark.hlines, mark.vlines,
                               mark.line_connections):
                    l.set_lw(lw)
        self.draw_figs()

    def change_show_connected_artists(self, show):
        """Change the visibility of connected artists

        Parameters
        ----------
        show: bool
            The visibility for the
            :meth:`straditize.cross_mark.CrossMarks.set_connected_artists_visible`
            method"""
        if show is Qt.Checked:
            show = True
        elif show is Qt.Unchecked:
            show = False
        for mark in self.marks:
            mark.set_connected_artists_visible(show)
        self.draw_figs()

    def change_line_style(self, i):
        """Change the line style of the marks

        Parameters
        ----------
        i: int
            The index of the line style in the :attr:`line_styles` attribute
            to use
        """
        ls = self.line_styles[i][0]
        for mark in self.marks:
            for l in chain(mark.hlines, mark.vlines, mark.line_connections):
                l.set_ls(ls)
        self.draw_figs()

    def change_marker_style(self, i):
        """Change the marker style of the marks

        Parameters
        ----------
        i: int
            The index of the marker style in the :attr:`marker_styles`
            attribute to use
        """
        marker = self.marker_styles[i][1][0]
        for mark in self.marks:
            for l in chain(mark.hlines, mark.vlines, mark.line_connections):
                l.set_marker(marker)
        self.draw_figs()

    def change_marker_size(self, markersize):
        """Change the size of the markers

        Parameters
        ----------
        markersize: float
            The size of the marker to use"""
        markersize = float(markersize or 0)
        for mark in self.marks:
            for l in chain(mark.hlines, mark.vlines, mark.line_connections):
                l.set_markersize(markersize)
        self.draw_figs()

    def fill_from_mark(self, mark):
        """Set the widgets of this :class:`MarkerControl` from a mark

        This method sets the color labels, combo boxes, check boxes and
        text edits to match the properties of the given `mark`

        Parameters
        ----------
        mark: straditize.cross_mark.CrossMark
            The mark to use the attributes from
        """
        line = mark.hline
        try:
            cselect = mark._select_props['c']
        except KeyError:
            try:
                cselect = mark._select_props['color']
            except KeyError:
                cselect = mark.hline.get_c()
        lw_key = 'lw' if 'lw' in mark._unselect_props else 'linewidth'
        self.set_line_style_item(line.get_linestyle())
        self.set_marker_item(line.get_marker())
        self.txt_line_width.setText(str(mark._unselect_props.get(lw_key, 0)))
        self.txt_line_width_select.setText(
            str(mark._select_props.get(lw_key, 0)))
        self.txt_marker_size.setText(str(line.get_markersize()))
        self.lbl_color_select._set_color(cselect)
        self.lbl_color_unselect._set_color(mark.hline.get_c())
        self.cb_auto_hide.setChecked(mark.auto_hide)
        self.cb_show_connected.setChecked(mark.show_connected_artists)
        try:
            mark.x
        except NotImplementedError:
            self.cb_drag_vline.setEnabled(False)
            self.cb_show_vlines.setEnabled(False)
            self.cb_selectable_vline.setEnabled(False)
        else:
            self.cb_drag_vline.setEnabled(True)
            self.cb_show_vlines.setEnabled(True)
            self.cb_selectable_vline.setEnabled(True)
        try:
            mark.y
        except NotImplementedError:
            self.cb_drag_hline.setEnabled(False)
            self.cb_show_hlines.setEnabled(False)
            self.cb_selectable_hline.setEnabled(False)
        else:
            self.cb_drag_hline.setEnabled(True)
            self.cb_show_hlines.setEnabled(True)
            self.cb_selectable_hline.setEnabled(True)
        self.cb_drag_hline.setChecked('h' in mark.draggable)
        self.cb_drag_vline.setChecked('v' in mark.draggable)
        self.cb_show_hlines.setChecked(not mark.hide_horizontal)
        self.cb_show_vlines.setChecked(not mark.hide_vertical)
        self.cb_selectable_hline.setChecked('h' in mark.selectable)
        self.cb_selectable_vline.setChecked('v' in mark.selectable)

    @property
    def line_props(self):
        """The properties of the lines as a :class:`dict`"""
        return {
            'ls':
            self.combo_line_style.currentText(),
            'marker':
            self.marker_styles[self.combo_marker_style.currentIndex()][1][0],
            'lw':
            float(self.txt_line_width.text().strip() or 0),
            'markersize':
            float(self.txt_marker_size.text().strip() or 0),
            'c':
            self.lbl_color_unselect.color.getRgbF(),
        }

    @property
    def select_props(self):
        """The properties of selected marks as a :class:`dict`"""
        return {
            'c': self.lbl_color_select.color.getRgbF(),
            'lw': float(self.txt_line_width_select.text().strip() or 0)
        }

    def update_mark(self, mark):
        """Update the properties of a mark to match the settings

        Parameters
        ----------
        mark: straditize.cross_mark.CrossMarks
            The mark to update
        """
        if len(self.straditizer.marks) < 2:
            return
        # line properties
        props = self.line_props
        mark._unselect_props.update(props)
        mark._select_props.update(self.select_props)

        # auto_hide
        auto_hide = self.cb_auto_hide.isChecked()
        mark.auto_hide = auto_hide
        if auto_hide:
            props['lw'] = 0

        # show_connected
        show_connected = self.cb_show_connected.isChecked()
        mark.set_connected_artists_visible(show_connected)

        # drag hline
        drag_hline = self.cb_drag_hline.isChecked()
        if drag_hline and 'h' not in mark.draggable:
            mark.draggable.append('h')
        elif not drag_hline and 'h' in mark.draggable:
            mark.draggable.remove('h')

        # drag vline
        drag_vline = self.cb_drag_vline.isChecked()
        if drag_vline and 'v' not in mark.draggable:
            mark.draggable.append('v')
        elif not drag_vline and 'v' in mark.draggable:
            mark.draggable.remove('v')

        # select hline
        select_hline = self.cb_selectable_hline.isChecked()
        if select_hline and 'h' not in mark.selectable:
            mark.selectable.append('h')
        elif not select_hline and 'h' in mark.selectable:
            mark.selectable.remove('h')

        # select hline
        select_vline = self.cb_selectable_vline.isChecked()
        if select_vline and 'v' not in mark.selectable:
            mark.selectable.append('v')
        elif not select_vline and 'v' in mark.selectable:
            mark.selectable.remove('v')

        show_horizontal = self.cb_show_hlines.isChecked()
        mark.hide_horizontal = ~show_horizontal
        show_vertical = self.cb_show_vlines.isChecked()
        mark.hide_vertical = ~show_vertical

        for l in chain(mark.hlines, mark.vlines):
            l.update(props)
        for l in mark.hlines:
            l.set_visible(show_horizontal)
        for l in mark.vlines:
            l.set_visible(show_vertical)

    def change_hline_draggable(self, state):
        """Enable or disable the dragging of horizontal lines

        Parameters
        ----------
        state: Qt.Checked or Qt.Unchecked
            If Qt.Checked, the horizontal lines can be dragged and dropped"""
        if state == Qt.Checked:
            for mark in self.marks:
                mark.draggable = np.unique(np.r_[['h'], mark.draggable])
        else:
            for mark in self.marks:
                mark.draggable = np.array(list(set(mark.draggable) - {'h'}))

    def change_hline_selectable(self, state):
        """Enable or disable the selection of horizontal lines

        Parameters
        ----------
        state: Qt.Checked or Qt.Unchecked
            If Qt.Checked, the horizontal lines can be selected"""
        if state == Qt.Checked:
            for mark in self.marks:
                mark.selectable = np.unique(np.r_[['h'], mark.selectable])
        else:
            for mark in self.marks:
                mark.selectable = np.array(list(set(mark.selectable) - {'h'}))

    def change_vline_draggable(self, state):
        """Enable or disable the dragging of vertical lines

        Parameters
        ----------
        state: Qt.Checked or Qt.Unchecked
            If Qt.Checked, the vertical lines can be dragged and dropped"""
        if state == Qt.Checked:
            for mark in self.marks:
                mark.draggable = np.unique(np.r_[['v'], mark.draggable])
        else:
            for mark in self.marks:
                mark.draggable = np.array(list(set(mark.draggable) - {'v'}))

    def change_vline_selectable(self, state):
        """Enable or disable the selection of vertical lines

        Parameters
        ----------
        state: Qt.Checked or Qt.Unchecked
            If Qt.Checked, the vertical lines can be selected"""
        if state == Qt.Checked:
            for mark in self.marks:
                mark.selectable = np.unique(np.r_[['v'], mark.selectable])
        else:
            for mark in self.marks:
                mark.selectable = np.array(list(set(mark.selectable) - {'v'}))

    def change_show_hlines(self, state):
        """Enable of disable the visibility of horizontal lines

        Parameters
        ----------
        state: Qt.Checked or Qt.Unchecked
            If Qt.Checked, all horizontal lines are hidden"""
        if state == Qt.Checked:
            for mark in self.marks:
                mark.hide_horizontal = False
                mark.set_visible(True)
        else:
            for mark in self.marks:
                mark.hide_horizontal = True
                mark.set_visible(True)
        self.draw_figs()

    def change_show_vlines(self, state):
        """Enable of disable the visibility of vertical lines

        Parameters
        ----------
        state: Qt.Checked or Qt.Unchecked
            If Qt.Checked, all vertical lines are hidden"""
        if state == Qt.Checked:
            for mark in self.marks:
                mark.hide_vertical = False
                mark.set_visible(True)
        else:
            for mark in self.marks:
                mark.hide_vertical = True
                mark.set_visible(True)
        self.draw_figs()

    def enable_or_disable_widgets(self, b):
        """Renabled to use the :meth:`refresh` method
        """
        self.refresh()

    def fill_after_adding(self, mark):
        if not self._filled:
            self.refresh()

    def go_to_right_mark(self):
        """Move the plot to the next right cross mark"""
        ax = self.straditizer.marks[0].ax
        if ax.xaxis_inverted():
            return self.go_to_smaller_x_mark(min(ax.get_xlim()))
        else:
            return self.go_to_greater_x_mark(max(ax.get_xlim()))

    def go_to_left_mark(self):
        """Move the plot to the previous left cross mark"""
        ax = self.straditizer.marks[0].ax
        if ax.xaxis_inverted():
            return self.go_to_greater_x_mark(max(ax.get_xlim()))
        else:
            return self.go_to_smaller_x_mark(min(ax.get_xlim()))

    def go_to_greater_x_mark(self, x):
        """Move the plot to the next mark with a x-position greater than `x`

        Parameters
        ----------
        x: float
            The reference x-position that shall be smaller than the new
            centered mark"""
        def is_visible(mark):
            return (np.searchsorted(np.sort(xlim),
                                    mark.xa) == 1).any() and (np.searchsorted(
                                        np.sort(ylim), mark.ya) == 1).any()

        ax = next(self.marks).ax
        xlim = np.asarray(ax.get_xlim())
        ylim = np.asarray(ax.get_ylim())
        marks = self.straditizer.marks
        if len(marks[0].xa) > 1:
            # get the mark in the center
            yc = ylim.mean()
            try:
                mark = min(filter(is_visible, marks),
                           key=lambda m: np.abs(m.ya - yc).min())
            except ValueError:  # empty sequence
                mark = min(marks, key=lambda m: np.abs(m.ya - yc).min())
            # if all edges are visible already, we return
            if (np.searchsorted(xlim, mark.xa) == 1).all():
                return
            mask = mark.xa > x
            if not mask.any():  # already on the right side
                return
            i = mark.xa[mask].argmin()
            x = mark.xa[mask][i]
            dx = np.diff(xlim) / 2.
            ax.set_xlim(x - dx, x + dx)
            ax.set_ylim(*ax.get_ylim())
        else:
            distances = ((mark.xa > x).any()
                         and (mark.xa[mark.xa > x] - x).min()
                         for mark in marks)
            try:
                dist, mark = min((t for t in zip(distances, marks) if t[0]),
                                 key=lambda t: t[0])
            except ValueError:  # empty sequence
                return
            mask = mark.xa > x
            i = mark.xa[mask].argmin()
            x = mark.xa[mask][i]
            j = np.abs(mark.ya - ylim.mean()).argmin()
            y = mark.ya[j]
            dx = np.diff(xlim) / 2.
            ax.set_xlim(x - dx, x + dx)
            dy = np.diff(ylim) / 2.
            ax.set_ylim(y - dy, y + dy)
        self.straditizer.draw_figure()

    def go_to_smaller_x_mark(self, x):
        """Move the plot to the next mark with a x-position smaller than `x`

        Parameters
        ----------
        x: float
            The reference x-position that shall be greater than the new
            centered mark"""
        def is_visible(mark):
            return (np.searchsorted(np.sort(xlim),
                                    mark.xa) == 1).any() and (np.searchsorted(
                                        np.sort(ylim), mark.ya) == 1).any()

        ax = next(self.marks).ax
        xlim = np.asarray(ax.get_xlim())
        ylim = np.asarray(ax.get_ylim())
        marks = self.straditizer.marks
        if len(marks[0].xa) > 1:
            # get the mark in the center
            yc = ylim.mean()
            try:
                mark = min(filter(is_visible, marks),
                           key=lambda m: np.abs(m.ya - yc).min())
            except ValueError:  # empty sequence
                mark = min(marks, key=lambda m: np.abs(m.ya - yc).min())
            # if all edges are visible already, we return
            if (np.searchsorted(xlim, mark.xa) == 1).all():
                return
            mask = mark.xa < x
            if not mask.any():  # already on the right side
                return
            i = mark.xa[mask].argmin()
            x = mark.xa[mask][i]
            dx = np.diff(xlim) / 2.
            ax.set_xlim(x - dx, x + dx)
            ax.set_ylim(*ax.get_ylim())
        else:
            distances = ((mark.xa < x).any()
                         and (mark.xa[mark.xa < x] - x).min()
                         for mark in marks)
            try:
                dist, mark = min((t for t in zip(distances, marks) if t[0]),
                                 key=lambda t: t[0])
            except ValueError:  # empty sequence
                return
            mask = mark.xa < x
            i = mark.xa[mask].argmin()
            x = mark.xa[mask][i]
            j = np.abs(mark.ya - ylim.mean()).argmin()
            y = mark.ya[j]
            dx = np.diff(xlim) / 2.
            ax.set_xlim(x - dx, x + dx)
            dy = np.diff(ylim) / 2.
            ax.set_ylim(y - dy, y + dy)
        self.straditizer.draw_figure()

    def go_to_upper_mark(self):
        """Go to the next mark above the current y-limits"""
        ax = self.straditizer.marks[0].ax
        if ax.xaxis_inverted():
            return self.go_to_greater_y_mark(max(ax.get_ylim()))
        else:
            return self.go_to_smaller_y_mark(min(ax.get_ylim()))

    def go_to_lower_mark(self):
        """Go to the next mark below the current y-limits"""
        ax = self.straditizer.marks[0].ax
        if ax.xaxis_inverted():
            return self.go_to_smaller_y_mark(min(ax.get_ylim()))
        else:
            return self.go_to_greater_y_mark(max(ax.get_ylim()))

    def go_to_greater_y_mark(self, y):
        """Move the plot to the next mark with a y-position greater than `y`

        Parameters
        ----------
        y: float
            The reference y-position that shall be smaller than the new
            centered mark"""
        def is_visible(mark):
            return (np.searchsorted(np.sort(xlim),
                                    mark.xa) == 1).any() and (np.searchsorted(
                                        np.sort(ylim), mark.ya) == 1).any()

        ax = next(self.marks).ax
        xlim = np.asarray(ax.get_xlim())
        ylim = np.asarray(ax.get_ylim())
        marks = self.straditizer.marks
        if len(marks[0].ya) > 1:
            # get the mark in the center
            xc = xlim.mean()
            try:
                mark = min(filter(is_visible, marks),
                           key=lambda m: np.abs(m.xa - xc).min())
            except ValueError:  # empty sequence
                mark = min(marks, key=lambda m: np.abs(m.xa - xc).min())
            # if all edges are visible already, we return
            if (np.searchsorted(ylim, mark.ya) == 1).all():
                return
            mask = mark.ya > y
            if not mask.any():  # already on the right side
                return
            i = mark.ya[mask].argmin()
            y = mark.ya[mask][i]
            dy = np.diff(ylim) / 2.
            ax.set_ylim(y - dy, y + dy)
        else:
            distances = ((mark.ya > y).any()
                         and (mark.xa[mark.ya > y] - y).min()
                         for mark in marks)
            try:
                dist, mark = min((t for t in zip(distances, marks) if t[0]),
                                 key=lambda t: t[0])
            except ValueError:  # empty sequence
                return
            mask = mark.ya > y
            i = mark.ya[mask].argmin()
            y = mark.ya[mask][i]
            j = np.abs(mark.xa - xlim.mean()).argmin()
            x = mark.xa[j]
            dx = np.diff(xlim) / 2.
            ax.set_xlim(x - dx, x + dx)
            dy = np.diff(ylim) / 2.
            ax.set_ylim(y - dy, y + dy)
        self.straditizer.draw_figure()

    def go_to_smaller_y_mark(self, y):
        """Move the plot to the next mark with a y-position smaller than `x`

        Parameters
        ----------
        y: float
            The reference y-position that shall be smaller than the new
            centered mark"""
        def is_visible(mark):
            return (np.searchsorted(np.sort(xlim),
                                    mark.xa) == 1).any() and (np.searchsorted(
                                        np.sort(ylim), mark.ya) == 1).any()

        ax = next(self.marks).ax
        xlim = np.asarray(ax.get_xlim())
        ylim = np.asarray(ax.get_ylim())
        marks = self.straditizer.marks
        if len(marks[0].ya) > 1:
            # get the mark in the center
            xc = xlim.mean()
            try:
                mark = min(filter(is_visible, marks),
                           key=lambda m: np.abs(m.xa - xc).min())
            except ValueError:  # empty sequence
                mark = min(marks, key=lambda m: np.abs(m.xa - xc).min())
            # if all edges are visible already, we return
            if (np.searchsorted(ylim, mark.ya) == 1).all():
                return
            mask = mark.ya < y
            if not mask.any():  # already on the right side
                return
            i = mark.ya[mask].argmin()
            y = mark.ya[mask][i]
            dy = np.diff(ylim) / 2.
            ax.set_ylim(y - dy, y + dy)
        else:
            distances = ((mark.ya < y).any()
                         and (mark.ya[mark.ya < y] - y).min()
                         for mark in marks)
            try:
                dist, mark = min((t for t in zip(distances, marks) if t[0]),
                                 key=lambda t: t[0])
            except ValueError:  # empty sequence
                return
            mask = mark.ya < y
            i = mark.ya[mask].argmin()
            y = mark.ya[mask][i]
            j = np.abs(mark.xa - xlim.mean()).argmin()
            x = mark.xa[j]
            dx = np.diff(xlim) / 2.
            ax.set_xlim(x - dx, x + dx)
            dy = np.diff(ylim) / 2.
            ax.set_ylim(y - dy, y + dy)
        self.straditizer.draw_figure()

    def add_toolbar_widgets(self, mark):
        """Add the navigation actions to the toolbar"""
        tb = self.straditizer.marks[0].ax.figure.canvas.toolbar
        if not isinstance(tb, QToolBar):
            return
        if self.tb_actions:
            self.remove_actions()

        self.tb_actions.append(tb.addSeparator())
        try:
            mark.x
        except NotImplementedError:
            add_right = False
        else:
            a = tb.addAction(QIcon(get_icon('left_mark.png')), 'left mark',
                             self.go_to_left_mark)
            a.setToolTip('Move to the next cross mark on the left')
            self.tb_actions.append(a)
            add_right = True

        try:
            mark.y
        except NotImplementedError:
            pass
        else:
            a = tb.addAction(QIcon(get_icon('upper_mark.png')), 'upper mark',
                             self.go_to_upper_mark)
            a.setToolTip('Move to the next cross mark above')
            self.tb_actions.append(a)

            a = tb.addAction(QIcon(get_icon('lower_mark.png')), 'lower mark',
                             self.go_to_lower_mark)
            a.setToolTip('Move to the next cross mark below')
            self.tb_actions.append(a)

        if add_right:
            a = tb.addAction(QIcon(get_icon('right_mark.png')), 'right mark',
                             self.go_to_right_mark)
            a.setToolTip('Move to the next cross mark on the right')
            self.tb_actions.append(a)
        self._toolbar = tb

    def remove_actions(self):
        """Remove the navigation actions from the toolbar"""
        if self._toolbar is None:
            return
        tb = self._toolbar
        for a in self.tb_actions:
            tb.removeAction(a)
        self.tb_actions.clear()

    def refresh(self):
        """Reimplemented to also set the properties of this widget
        """
        super(MarkerControl, self).refresh()
        if self.straditizer is not None and self.straditizer.marks:
            self._filled = True
            mark = self.straditizer.marks[0]
            self.fill_from_mark(mark)
            self.add_toolbar_widgets(mark)
        else:
            self.remove_actions()
            self._filled = False
        if self.straditizer is not None:
            self.straditizer.mark_added.connect(self.fill_after_adding)
            self.straditizer.mark_added.connect(self.update_mark)
Esempio n. 2
0
class SelectionToolbar(QToolBar, StraditizerControlBase):
    """A toolbar for selecting features in the straditizer and data image

    The current data object is set in the :attr:`combo` and can be accessed
    through the :attr:`data_obj` attribute. It's either the straditizer or the
    data_reader that is accessed"""

    _idPress = None

    _idRelease = None

    #: A signal that is emitted when something is selected
    selected = QtCore.pyqtSignal()

    set_cursor_id = None

    reset_cursor_id = None

    #: The QCombobox that defines the data object to be used
    combo = None

    @property
    def ax(self):
        """The :class:`matplotlib.axes.Axes` of the :attr:`data_obj`"""
        return self.data_obj.ax

    @property
    def data(self):
        """The np.ndarray of the :attr:`data_obj` image"""
        text = self.combo.currentText()
        if text == 'Reader':
            return self.straditizer.data_reader.binary
        elif text == 'Reader - Greyscale':
            return self.straditizer.data_reader.to_grey_pil(
                self.straditizer.data_reader.image)
        else:
            from straditize.binary import DataReader
            return DataReader.to_grey_pil(self.straditizer.image)

    @property
    def data_obj(self):
        """The data object as set in the :attr:`combo`.

        Either a :class:`~straditize.straditizer.Straditizer` or a
        :class:`straditize.binary.DataReader` instance. """
        text = self.combo.currentText()
        if text in ['Reader', 'Reader - Greyscale']:
            return self.straditizer.data_reader
        else:
            return self.straditizer

    @data_obj.setter
    def data_obj(self, value):
        """The data object as set in the :attr:`combo`.

        Either a :class:`~straditize.straditizer.Straditizer` or a
        :class:`straditize.binary.DataReader` instance. """
        if self.straditizer is None:
            return
        if isinstance(value, six.string_types):
            possible_values = {
                self.combo.itemText(i) for i in range(self.combo.count())}
            if value not in possible_values:
                raise ValueError(
                    'Do not understand %r! Please use one of %r' % (
                        value, possible_values))
            else:
                self.combo.setCurrentText(value)
        else:
            if value is self.straditizer:
                self.combo.setCurrentText('Straditizer')
            elif value and value is self.straditizer.data_reader:
                self.combo.setCurrentText('Reader')
            else:
                raise ValueError('Do not understand %r! Please either use '
                                 'the Straditizer or DataReader instance!' % (
                                     value, ))

    @property
    def fig(self):
        """The :class:`~matplotlib.figure.Figure` of the :attr:`data_obj`"""
        try:
            return self.ax.figure
        except AttributeError:
            return None

    @property
    def canvas(self):
        """The canvas of the :attr:`data_obj`"""
        try:
            return self.fig.canvas
        except AttributeError:
            return None

    @property
    def toolbar(self):
        """The toolbar of the :attr:`canvas`"""
        return self.canvas.toolbar

    @property
    def select_action(self):
        """The rectangle selection tool"""
        return self._actions['select']

    @property
    def wand_action(self):
        """The wand selection tool"""
        return self._actions['wand_select']

    @property
    def new_select_action(self):
        """The action to make new selection with one of the selection tools"""
        return self._type_actions['new_select']

    @property
    def add_select_action(self):
        """The action to add to the current selection with the selection tools
        """
        return self._type_actions['add_select']

    @property
    def remove_select_action(self):
        """
        An action to remove from the current selection with the selection tools
        """
        return self._type_actions['remove_select']

    @property
    def select_all_action(self):
        """An action to select all features in the :attr:`data`"""
        return self._actions['select_all']

    @property
    def expand_select_action(self):
        """An action to expand the current selection to the full feature"""
        return self._actions['expand_select']

    @property
    def invert_select_action(self):
        """An action to invert the current selection"""
        return self._actions['invert_select']

    @property
    def clear_select_action(self):
        """An action to clear the current selection"""
        return self._actions['clear_select']

    @property
    def select_right_action(self):
        """An action to select everything in the data column to the right"""
        return self._actions['select_right']

    @property
    def select_pattern_action(self):
        """An action to start a pattern selection"""
        return self._actions['select_pattern']

    @property
    def widgets2disable(self):
        if not self._actions:
            return []
        elif self._selecting:
            return [self.combo]
        else:
            return list(chain([self.combo],
                              self._actions.values(),
                              self._appearance_actions.values()))

    @property
    def labels(self):
        """The labeled data that is displayed"""
        if self.data_obj._selection_arr is not None:
            return self.data_obj._selection_arr
        text = self.combo.currentText()
        if text == 'Reader':
            return self.straditizer.data_reader.labels.copy()
        elif text == 'Reader - Greyscale':
            return self.straditizer.data_reader.color_labels()
        else:
            return self.straditizer.get_labels()

    @property
    def rect_callbacks(self):
        """The functions to call after the rectangle selection.

        If not set manually, it is the :meth:`select_rect` method. Note that
        this is cleared at every call of the :meth:`end_selection`.

        Callables in this list must accept two arguments ``(slx, sly)``:
        the first one is the x-slice, and the second one the y-slice. They both
        correspond to the :attr:`data` attribute."""
        return self._rect_callbacks or [self.select_rect]

    @rect_callbacks.setter
    def rect_callbacks(self, value):
        """The functions to call after the rectangle selection.

        If not set manually, it is the :meth:`select_rect` method. Note that
        this is cleared at every call of the :meth:`end_selection`.

        Callables in this list must accept two arguments ``(slx, sly)``:
        the first one is the x-slice, and the second one the y-slice. They both
        correspond to the :attr:`data` attribute."""
        self._rect_callbacks = value

    @property
    def poly_callbacks(self):
        """The functions to call after the polygon selection

        If not set manually, it is the :meth:`select_poly` method. Note that
        this is cleared at every call of the :meth:`end_selection`.

        Callables in this list must accept one argument, a ``np.ndarray``
        of shape ``(N, 2)``. This array defines the ``N`` x- and y-coordinates
        of the points of the polygon"""
        return self._poly_callbacks or [self.select_poly]

    @poly_callbacks.setter
    def poly_callbacks(self, value):
        """The functions to call after the polygon selection.

        If not set manually, it is the :meth:`poly_callbacks` method. Note that
        this is cleared at every call of the :meth:`end_selection`.

        Callables in this list must accept one argument, a ``np.ndarray``
        of shape ``(N, 2)``. This array defines the ``N`` x- and y-coordinates
        of the points of the polygon"""
        self._poly_callbacks = value

    #: A :class:`PointOrRectangleSelector` to select features in the image
    selector = None

    _pattern_selection = None

    def __init__(self, straditizer_widgets, *args, **kwargs):
        super(SelectionToolbar, self).__init__(*args, **kwargs)
        self._actions = {}
        self._wand_actions = {}
        self._pattern_actions = {}
        self._select_actions = {}
        self._appearance_actions = {}
        # Boolean that is True if we are in a selection process
        self._selecting = False
        self.init_straditizercontrol(straditizer_widgets)
        self._ids_select = []
        self._rect_callbacks = []
        self._poly_callbacks = []
        self._selection_mode = None
        self._lastCursor = None
        self.create_actions()
        self._changed_selection = False
        self._connected = []
        self._action_clicked = None
        self.wand_type = 'labels'
        self.select_type = 'rect'
        self.pattern_type = 'binary'
        self.auto_expand = False

    def create_actions(self):
        """Define the actions for the toolbar and set everything up"""
        # Reader toolbar
        self.combo = QComboBox()
        self.combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.addWidget(self.combo)

        select_group = QActionGroup(self)

        # select action
        self._actions['select'] = a = self.addAction(
            QIcon(get_icon('select.png')), 'select', self.toggle_selection)
        a.setToolTip('Select pixels within a rectangle')
        a.setCheckable(True)
        select_group.addAction(a)

        # select menu
        select_menu = QMenu(self)
        self._select_actions['rect_select'] = menu_a = select_menu.addAction(
            QIcon(get_icon('select.png')), 'rectangle',
            self.set_rect_select_mode)
        menu_a.setToolTip('Select a rectangle')
        a.setToolTip(menu_a.toolTip())

        self._select_actions['poly_select'] = menu_a = select_menu.addAction(
            QIcon(get_icon('poly_select.png')), 'polygon',
            self.set_poly_select_mode)
        menu_a.setToolTip('Select a rectangle')
        a.setToolTip(menu_a.toolTip())

        a.setMenu(select_menu)

        # wand_select action
        self._actions['wand_select'] = a = self.addAction(
            QIcon(get_icon('wand_select.png')), 'select',
            self.toggle_selection)
        a.setCheckable(True)
        select_group.addAction(a)

        # wand menu
        tool_menu = QMenu(self)
        self._wand_actions['wand_select'] = menu_a = tool_menu.addAction(
            QIcon(get_icon('wand_select.png')), 'wand',
            self.set_label_wand_mode)
        menu_a.setToolTip('Select labels within a rectangle')
        a.setToolTip(menu_a.toolTip())

        self._wand_actions['color_select'] = menu_a = tool_menu.addAction(
            QIcon(get_icon('color_select.png')), 'color wand',
            self.set_color_wand_mode)
        menu_a.setToolTip('Select colors')

        self._wand_actions['row_select'] = menu_a = tool_menu.addAction(
            QIcon(get_icon('row_select.png')), 'row selection',
            self.set_row_wand_mode)
        menu_a.setToolTip('Select pixel rows')

        self._wand_actions['col_select'] = menu_a = tool_menu.addAction(
            QIcon(get_icon('col_select.png')), 'column selection',
            self.set_col_wand_mode)
        menu_a.setToolTip('Select pixel columns')

        a.setMenu(tool_menu)

        # color_wand widgets
        self.distance_slider = slider = QSlider(Qt.Horizontal)
        slider.setMinimum(0)
        slider.setMaximum(255)
        slider.setValue(30)
        slider.setSingleStep(1)

        self.lbl_slider = QLabel('30')
        slider.valueChanged.connect(lambda i: self.lbl_slider.setText(str(i)))
        slider.setMaximumWidth(self.combo.sizeHint().width())

        self.cb_whole_fig = QCheckBox('Whole plot')
        self.cb_whole_fig.setToolTip('Select the colors on the entire plot')

        self.cb_use_alpha = QCheckBox('Use alpha')
        self.cb_use_alpha.setToolTip('Use the alpha channel, i.e. the '
                                     'transparency of the RGBA image.')

        self.color_wand_actions = [
                self.addWidget(slider), self.addWidget(self.lbl_slider),
                self.addWidget(self.cb_whole_fig),
                self.addWidget(self.cb_use_alpha)]

        self.set_label_wand_mode()

        self.addSeparator()
        type_group = QActionGroup(self)

        self._type_actions = {}

        # new selection action
        self._type_actions['new_select'] = a = self.addAction(
            QIcon(get_icon('new_selection.png')), 'Create a new selection')
        a.setToolTip('Select pixels within a rectangle and ignore the current '
                     'selection')
        a.setCheckable(True)
        type_group.addAction(a)

        # add to selection action
        self._type_actions['add_select'] = a = self.addAction(
            QIcon(get_icon('add_select.png')), 'Add to selection')
        a.setToolTip('Select pixels within a rectangle and add them to the '
                     'current selection')
        a.setCheckable(True)
        type_group.addAction(a)

        # remove action
        self._type_actions['remove_select'] = a = self.addAction(
            QIcon(get_icon('remove_select.png')), 'Remove from selection')
        a.setToolTip('Select pixels within a rectangle and remove them from '
                     'the current selection')
        a.setCheckable(True)
        type_group.addAction(a)

        # info button
        self.addSeparator()
        self.info_button = InfoButton(self, 'selection_toolbar.rst')
        self.addWidget(self.info_button)

        # selection appearence options
        self.addSeparator()
        self.sl_alpha = slider = QSlider(Qt.Horizontal)
        self._appearance_actions['alpha'] = self.addWidget(slider)
        slider.setMinimum(0)
        slider.setMaximum(100)
        slider.setValue(100)
        slider.setSingleStep(1)

        self.lbl_alpha_slider = QLabel('100 %')
        slider.valueChanged.connect(
            lambda i: self.lbl_alpha_slider.setText(str(i) + ' %'))
        slider.valueChanged.connect(self.update_alpha)
        slider.setMaximumWidth(self.combo.sizeHint().width())

        # Select all and invert selection buttons
        self.addSeparator()
        self._actions['select_all'] = a = self.addAction(
            QIcon(get_icon('select_all.png')), 'all', self.select_all)
        a.setToolTip('Select all labels')

        self._actions['expand_select'] = a = self.addAction(
            QIcon(get_icon('expand_select.png')), 'expand',
            self.expand_selection)
        a.setToolTip('Expand the selected areas to select the entire feature')

        self._actions['invert_select'] = a = self.addAction(
            QIcon(get_icon('invert_select.png')), 'invert',
            self.invert_selection)
        a.setToolTip('Invert selection')

        self._actions['clear_select'] = a = self.addAction(
            QIcon(get_icon('clear_select.png')), 'clear',
            self.clear_selection)
        a.setToolTip('Clear selection')

        self._actions['select_right'] = a = self.addAction(
            QIcon(get_icon('select_right.png')), 'right',
            self.select_everything_to_the_right)
        a.setToolTip('Select everything to the right of each column')

        self._actions['select_pattern'] = a = self.addAction(
            QIcon(get_icon('pattern.png')), 'pattern',
            self.start_pattern_selection)
        a.setCheckable(True)
        a.setToolTip(
            'Select a binary pattern/hatch within the current selection')

        # wand menu
        pattern_menu = QMenu(self)
        self._pattern_actions['binary'] = menu_a = pattern_menu.addAction(
            QIcon(get_icon('pattern.png')), 'Binary',
            self.set_binary_pattern_mode)
        menu_a.setToolTip(
            'Select a binary pattern/hatch within the current selection')
        a.setToolTip(menu_a.toolTip())

        self._pattern_actions['grey'] = menu_a = pattern_menu.addAction(
            QIcon(get_icon('pattern_grey.png')), 'Greyscale',
            self.set_grey_pattern_mode)
        menu_a.setToolTip(
            'Select a pattern/hatch within the current selection based on '
            'grey scale colors')

        a.setMenu(pattern_menu)

        self.new_select_action.setChecked(True)
        for a in self._type_actions.values():
            a.toggled.connect(self.add_or_remove_pattern)

        self.refresh()

    def should_be_enabled(self, w):
        if self.straditizer is None:
            return False
        elif (self._actions and
              w in [self.remove_select_action, self.invert_select_action,
                    self.clear_select_action, self.expand_select_action,
                    self.select_right_action] and
              not self._selecting):
            return False
        elif w in self._appearance_actions.values() and not self._selecting:
            return False
        elif (self.combo and not self.combo.currentText().startswith('Reader')
              and w is self.select_right_action):
            return False
        return True

    def disable_actions(self):
        if self._changed_selection:
            return
        for a in self._actions.values():
            if a.isChecked():
                a.setChecked(False)
                self.toggle_selection()
            else:
                a.setChecked(False)

    def select_all(self):
        """Select all features in the image

        See Also
        --------
        straditize.label_selection.LabelSelection.select_all_labels"""
        obj = self.data_obj
        if obj._selection_arr is None:
            rgba = obj.image_array() if hasattr(obj, 'image_array') else None
            self.start_selection(self.labels, rgba=rgba)
        obj.select_all_labels()
        self.canvas.draw()

    def invert_selection(self):
        """Invert the current selection"""
        obj = self.data_obj
        if obj._selection_arr is None:
            rgba = obj.image_array() if hasattr(obj, 'image_array') else None
            self.start_selection(self.labels, rgba=rgba)
        if (obj._selection_arr != obj._orig_selection_arr).any():
            selection = obj.selected_part

            # clear the current selection
            obj._selection_arr[:] = np.where(
                obj._selection_arr.astype(bool) & (~selection),
                obj._orig_selection_arr.max() + 1, obj._orig_selection_arr)
            obj._select_img.set_array(obj._selection_arr)
            obj.unselect_all_labels()
        else:
            obj.select_all_other_labels()
        self.canvas.draw()

    def clear_selection(self):
        """Clear the current selection"""
        obj = self.data_obj
        if obj._selection_arr is None:
            return
        obj._selection_arr[:] = obj._orig_selection_arr.copy()
        obj._select_img.set_array(obj._selection_arr)
        obj.unselect_all_labels()
        self.canvas.draw()

    def expand_selection(self):
        """Expand the selected areas to select the full labels"""
        obj = self.data_obj
        if obj._selection_arr is None:
            return
        arr = obj._orig_selection_arr.copy()
        selected_labels = np.unique(arr[obj.selected_part])
        obj._selection_arr = arr
        obj._select_img.set_array(arr)
        obj.unselect_all_labels()
        obj.select_labels(selected_labels)
        self.canvas.draw()

    def update_alpha(self, i):
        """Set the transparency of the selection image

        Parameters
        ----------
        i: int
            The transparency between 0 and 100"""
        self.data_obj._select_img.set_alpha(i / 100.)
        self.data_obj._update_magni_img()
        self.canvas.draw()

    def select_everything_to_the_right(self):
        """Selects everything to the right of the current selection"""
        reader = self.data_obj
        if reader._selection_arr is None:
            return
        bounds = reader.column_bounds
        selection = reader.selected_part
        new_select = np.zeros_like(selection)
        for start, end in bounds:
            can_be_selected = reader._selection_arr[:, start:end].astype(bool)
            end = start + can_be_selected.shape[1]
            last_in_row = selection[:, start:end].argmax(axis=-1).reshape(
                (-1, 1))
            dist2start = np.tile(np.arange(end - start)[np.newaxis],
                                 (len(selection), 1))
            can_be_selected[dist2start <= last_in_row] = False
            can_be_selected[~np.tile(last_in_row.astype(bool),
                                     (1, end - start))] = False
            new_select[:, start:end] = can_be_selected
        max_label = reader._orig_selection_arr.max()
        reader._selection_arr[new_select] = max_label + 1
        reader._select_img.set_array(reader._selection_arr)
        reader._update_magni_img()
        self.canvas.draw()

    def start_pattern_selection(self):
        """Open the pattern selection dialog

        This method will enable the pattern selection by starting a
        :class:`straditize.widgets.pattern_selection.PatternSelectionWidget`"""
        from straditize.widgets.pattern_selection import PatternSelectionWidget
        if self.select_pattern_action.isChecked():
            from straditize.binary import DataReader
            from psyplot_gui.main import mainwindow
            obj = self.data_obj
            if obj._selection_arr is None:
                if hasattr(obj, 'image_array'):
                    rgba = obj.image_array()
                else:
                    rgba = None
                self.start_selection(self.labels, rgba=rgba)
                self.select_all()
            if not obj.selected_part.any():
                self.select_pattern_action.setChecked(False)
                raise ValueError(
                    "No data in the image is selected. Please select the "
                    "coarse region in which the pattern should be searched.")
            if self.pattern_type == 'binary':
                arr = DataReader.to_binary_pil(obj.image)
            else:
                arr = DataReader.to_grey_pil(obj.image)
            self._pattern_selection = w = PatternSelectionWidget(
                arr, obj)
            w.to_dock(mainwindow, 'Pattern selection')
            w.btn_close.clicked.connect(self.uncheck_pattern_selection)
            w.btn_cancel.clicked.connect(self.uncheck_pattern_selection)
            self.disable_actions()
            pattern_action = self.select_pattern_action
            for a in self._actions.values():
                a.setEnabled(False)
            pattern_action.setEnabled(True)
            pattern_action.setChecked(True)
            w.show_plugin()
            w.maybe_tabify()
            w.raise_()
        elif self._pattern_selection is not None:
            self._pattern_selection.cancel()
            self._pattern_selection.close()
            self.uncheck_pattern_selection()
            del self._pattern_selection

    def uncheck_pattern_selection(self):
        """Disable the pattern selection"""
        self.select_pattern_action.setChecked(False)
        del self._pattern_selection
        for a in self._actions.values():
            a.setEnabled(self.should_be_enabled(a))

    def add_or_remove_pattern(self):
        """Enable the removing or adding of the pattern selection"""
        if getattr(self, '_pattern_selection', None) is None:
            return
        current = self._pattern_selection.remove_selection
        new = self.remove_select_action.isChecked()
        if new is not current:
            self._pattern_selection.remove_selection = new
            if self._pattern_selection.btn_select.isChecked():
                self._pattern_selection.modify_selection(
                    self._pattern_selection.sl_thresh.value())

    def set_rect_select_mode(self):
        """Set the current wand tool to the color wand"""
        self.select_type = 'rect'
        self.select_action.setIcon(QIcon(get_icon('select.png')))
        self._action_clicked = None
        self.toggle_selection()

    def set_poly_select_mode(self):
        """Set the current wand tool to the color wand"""
        self.select_type = 'poly'
        self.select_action.setIcon(QIcon(get_icon('poly_select.png')))
        self._action_clicked = None
        self.toggle_selection()

    def set_label_wand_mode(self):
        """Set the current wand tool to the color wand"""
        self.wand_type = 'labels'
        self.wand_action.setIcon(QIcon(get_icon('wand_select.png')))
        for a in self.color_wand_actions:
            a.setVisible(False)
        self._action_clicked = None
        self.toggle_selection()

    def set_color_wand_mode(self):
        """Set the current wand tool to the color wand"""
        self.wand_type = 'color'
        self.wand_action.setIcon(QIcon(get_icon('color_select.png')))
        for a in self.color_wand_actions:
            a.setVisible(True)
        self._action_clicked = None
        self.toggle_selection()

    def set_row_wand_mode(self):
        """Set the current wand tool to the color wand"""
        self.wand_type = 'rows'
        self.wand_action.setIcon(QIcon(get_icon('row_select.png')))
        for a in self.color_wand_actions:
            a.setVisible(False)
        self._action_clicked = None
        self.toggle_selection()

    def set_col_wand_mode(self):
        """Set the current wand tool to the color wand"""
        self.wand_type = 'cols'
        self.wand_action.setIcon(QIcon(get_icon('col_select.png')))
        for a in self.color_wand_actions:
            a.setVisible(False)
        self._action_clicked = None
        self.toggle_selection()

    def set_binary_pattern_mode(self):
        """Set the current pattern mode to the binary pattern"""
        self.pattern_type = 'binary'
        self.select_pattern_action.setIcon(QIcon(get_icon('pattern.png')))

    def set_grey_pattern_mode(self):
        """Set the current pattern mode to the binary pattern"""
        self.pattern_type = 'grey'
        self.select_pattern_action.setIcon(QIcon(get_icon('pattern_grey.png')))

    def disconnect(self):
        if self.set_cursor_id is not None:
            if self.canvas is None:
                self.canvas.mpl_disconnect(self.set_cursor_id)
                self.canvas.mpl_disconnect(self.reset_cursor_id)
            self.set_cursor_id = None
            self.reset_cursor_id = None

        if self.selector is not None:
            self.selector.disconnect_events()
            self.selector = None

    def toggle_selection(self):
        """Activate selection mode"""
        if self.canvas is None:
            return
        self.disconnect()

        key = next((key for key, a in self._actions.items() if a.isChecked()),
                   None)
        if key is None or key == self._action_clicked:
            self._action_clicked = None
            if key is not None:
                self._actions[key].setChecked(False)
        else:
            if self.wand_action.isChecked() and self.wand_type == 'color':
                self.selector = PointOrRectangleSelector(
                    self.ax, self.on_rect_select, rectprops=dict(fc='none'),
                    lineprops=dict(c='none'), useblit=True)
            elif self.select_action.isChecked() and self.select_type == 'poly':
                self.selector = mwid.LassoSelector(
                    self.ax, self.on_poly_select)
            else:
                self.selector = PointOrRectangleSelector(
                    self.ax, self.on_rect_select, useblit=True)
            self.set_cursor_id = self.canvas.mpl_connect(
                'axes_enter_event', self._on_axes_enter)
            self.reset_cursor_id = self.canvas.mpl_connect(
                'axes_leave_event', self._on_axes_leave)
            self._action_clicked = next(key for key, a in self._actions.items()
                                        if a.isChecked())

        self.toolbar.set_message(self.toolbar.mode)

    def enable_or_disable_widgets(self, b):
        super(SelectionToolbar, self).enable_or_disable_widgets(b)
        if not b:
            for w in [self.clear_select_action, self.invert_select_action,
                      self.expand_select_action]:
                w.setEnabled(self.should_be_enabled(w))
        if self._actions and not self.select_action.isEnabled():
            for a in self._actions.values():
                if a.isChecked():
                    a.setChecked(False)
                    self.toggle_selection()

    def refresh(self):
        super(SelectionToolbar, self).refresh()
        combo = self.combo
        if self.straditizer is None:
            combo.clear()
        else:
            if not combo.count():
                combo.addItem('Straditizer')
            if self.straditizer.data_reader is not None:
                if not any(combo.itemText(i) == 'Reader'
                           for i in range(combo.count())):
                    combo.addItem('Reader')
                    combo.addItem('Reader - Greyscale')
            else:
                for i in range(combo.count()):
                    if combo.itemText(i).startswith('Reader'):
                        combo.removeItem(i)

    def _on_axes_enter(self, event):
        ax = self.ax
        if ax is None:
            return
        if (event.inaxes is ax and self.toolbar._active == '' and
                self.selector is not None):
            if self._lastCursor != cursors.SELECT_REGION:
                self.toolbar.set_cursor(cursors.SELECT_REGION)
                self._lastCursor = cursors.SELECT_REGION

    def _on_axes_leave(self, event):
        ax = self.ax
        if ax is None:
            return
        if (event.inaxes is ax and self.toolbar._active == '' and
                self.selector is not None):
            if self._lastCursor != cursors.POINTER:
                self.toolbar.set_cursor(cursors.POINTER)
                self._lastCursor = cursors.POINTER

    def end_selection(self):
        """Finish the selection and disconnect everything"""
        if getattr(self, '_pattern_selection', None) is not None:
            self._pattern_selection.close()
            del self._pattern_selection
        self._selecting = False
        self._action_clicked = None
        self.toggle_selection()
        self.auto_expand = False
        self._labels = None
        self._rect_callbacks.clear()
        self._poly_callbacks.clear()
        self._wand_actions['color_select'].setEnabled(True)

    def get_xy_slice(self, lastx, lasty, x, y):
        """Transform x- and y-coordinates to :class:`slice` objects

        Parameters
        ----------
        lastx: int
            The initial x-coordinate
        lasty: int
            The initial y-coordinate
        x: int
            The final x-coordinate
        y: int
            The final y-coordinate

        Returns
        -------
        slice
            The ``slice(lastx, x)``
        slice
            The ``slice(lasty, y)``"""
        all_x = np.floor(np.sort([lastx, x])).astype(int)
        all_y = np.floor(np.sort([lasty, y])).astype(int)
        extent = getattr(self.data_obj, 'extent', None)
        if extent is not None:
            all_x -= np.ceil(extent[0]).astype(int)
            all_y -= np.ceil(min(extent[2:])).astype(int)
        if self.wand_action.isChecked() and self.wand_type == 'color':
            all_x[0] = all_x[1]
            all_y[0] = all_y[1]
        all_x[all_x < 0] = 0
        all_y[all_y < 0] = 0
        all_x[1] += 1
        all_y[1] += 1
        return slice(*all_x), slice(*all_y)

    def on_rect_select(self, e0, e1):
        """Call the :attr:`rect_callbacks` after a rectangle selection

        Parameters
        ----------
        e0: matplotlib.backend_bases.Event
            The initial event
        e1: matplotlib.backend_bases.Event
            The final event"""
        slx, sly = self.get_xy_slice(e0.xdata, e0.ydata, e1.xdata, e1.ydata)
        for func in self.rect_callbacks:
            func(slx, sly)

    def select_rect(self, slx, sly):
        """Select the data defined by a rectangle

        Parameters
        ----------
        slx: slice
            The x-slice of the rectangle
        sly: slice
            The y-slice of the rectangle

        See Also
        --------
        rect_callbacks"""
        obj = self.data_obj
        if obj._selection_arr is None:
            arr = self.labels
            rgba = obj.image_array() if hasattr(obj, 'image_array') else None
            self.start_selection(arr, rgba=rgba)
        expand = False
        if self.select_action.isChecked():
            arr = self._select_rectangle(slx, sly)
            expand = True
        elif self.wand_type == 'labels':
            arr = self._select_labels(slx, sly)
        elif self.wand_type == 'rows':
            arr = self._select_rows(slx, sly)
        elif self.wand_type == 'cols':
            arr = self._select_cols(slx, sly)
        else:
            arr = self._select_colors(slx, sly)
            expand = True
        if arr is not None:
            obj._selection_arr = arr
            obj._select_img.set_array(arr)
            obj._update_magni_img()
            if expand and self.auto_expand:
                self.expand_selection()
            else:
                self.canvas.draw()

    def on_poly_select(self, points):
        """Call the :attr:`poly_callbacks` after a polygon selection

        Parameters
        ----------
        e0: matplotlib.backend_bases.Event
            The initial event
        e1: matplotlib.backend_bases.Event
            The final event"""
        for func in self.poly_callbacks:
            func(points)

    def select_poly(self, points):
        """Select the data defined by a polygon

        Parameters
        ----------
        points: np.ndarray of shape (N, 2)
            The x- and y-coordinates of the vertices of the polygon

        See Also
        --------
        poly_callbacks"""
        obj = self.data_obj
        if obj._selection_arr is None:
            rgba = obj.image_array() if hasattr(obj, 'image_array') else None
            self.start_selection(self.labels, rgba=rgba)
        arr = self.labels
        mpath = mplp.Path(points)
        x = np.arange(obj._selection_arr.shape[1], dtype=int)
        y = np.arange(obj._selection_arr.shape[0], dtype=int)
        extent = getattr(obj, 'extent', None)
        if extent is not None:
            x += np.ceil(extent[0]).astype(int)
            y += np.ceil(min(extent[2:])).astype(int)
        pointsx, pointsy = np.array(points).T
        x0, x1 = x.searchsorted([pointsx.min(), pointsx.max()])
        y0, y1 = y.searchsorted([pointsy.min(), pointsy.max()])
        X, Y = np.meshgrid(x[x0:x1], y[y0:y1])
        points = np.array((X.flatten(), Y.flatten())).T
        mask = np.zeros_like(obj._selection_arr, dtype=bool)
        mask[y0:y1, x0:x1] = (
            mpath.contains_points(points).reshape(X.shape) &
            obj._selection_arr[y0:y1, x0:x1].astype(bool))
        if self.remove_select_action.isChecked():
            arr[mask] = -1
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[mask] = arr.max() + 1
        obj._selection_arr = arr
        obj._select_img.set_array(arr)
        obj._update_magni_img()
        if self.auto_expand:
            self.expand_selection()
        else:
            self.canvas.draw()

    def _select_rectangle(self, slx, sly):
        """Select a rectangle within the array"""
        obj = self.data_obj
        arr = self.labels
        data_mask = obj._selection_arr.astype(bool)
        if self.remove_select_action.isChecked():
            arr[sly, slx][data_mask[sly, slx]] = -1
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[sly, slx][data_mask[sly, slx]] = arr.max() + 1
        return arr

    def _select_labels(self, slx, sly):
        """Select the unique labels in the array"""
        obj = self.data_obj
        arr = self.labels
        data_mask = obj._selection_arr.astype(bool)
        current_selected = obj.selected_labels
        new_selected = np.unique(
            arr[sly, slx][data_mask[sly, slx]])
        valid_labels = np.unique(
            obj._orig_selection_arr[sly, slx][data_mask[sly, slx]])
        valid_labels = valid_labels[valid_labels > 0]
        if not len(valid_labels):
            return
        if new_selected[0] == -1 or new_selected[-1] > obj._select_nlabels:
            mask = np.isin(obj._orig_selection_arr, valid_labels)
            current_selected = np.unique(
                np.r_[current_selected,
                      obj._orig_selection_arr[sly, slx][
                          arr[sly, slx] > obj._select_nlabels]])
            arr[mask] = obj._orig_selection_arr[mask]
        curr = set(current_selected)
        valid = set(valid_labels)
        if self.remove_select_action.isChecked():
            new = curr - valid
        elif self.add_select_action.isChecked():
            new = curr | valid
        else:
            new = valid
            arr = obj._orig_selection_arr.copy()
        obj.select_labels(np.array(sorted(new)))
        return arr

    def _select_rows(self, slx, sly):
        """Select the pixel rows defined by `sly`

        Parameters
        ----------
        slx: slice
            The x-slice (is ignored)
        sly: slice
            The y-slice defining the rows to select"""
        obj = self.data_obj
        arr = self.labels
        rows = np.arange(arr.shape[0])[sly]
        if self.remove_select_action.isChecked():
            arr[rows, :] = np.where(arr[rows, :], -1, 0)
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[rows, :] = np.where(arr[rows, :], arr.max() + 1, 0)
        return arr

    def _select_cols(self, slx, sly):
        """Select the pixel columns defined by `slx`

        Parameters
        ----------
        slx: slice
            The x-slice defining the columns to select
        sly: slice
            The y-slice (is ignored)"""
        obj = self.data_obj
        arr = self.labels
        cols = np.arange(arr.shape[1])[slx]
        if self.remove_select_action.isChecked():
            arr[:, cols] = np.where(arr[:, cols], -1, 0)
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[:, cols] = np.where(arr[:, cols], arr.max() + 1, 0)
        return arr

    def _select_colors(self, slx, sly):
        """Select the array based on the colors"""
        if self.cb_use_alpha.isChecked():
            rgba = self._rgba
            n = 4
        else:
            rgba = self._rgba[..., :-1]
            n = 3
        rgba = rgba.astype(int)
        # get the unique colors
        colors = list(
            map(np.array, set(map(tuple, rgba[sly, slx].reshape((-1, n))))))
        obj = self.data_obj
        arr = self.labels
        mask = np.zeros_like(arr, dtype=bool)
        max_dist = self.distance_slider.value()
        data_mask = obj._selection_arr.astype(bool)
        for c in colors:
            mask[np.all(np.abs(rgba - c.reshape((1, 1, -1))) <= max_dist,
                        axis=-1)] = True
        if not self.cb_whole_fig.isChecked():
            import skimage.morphology as skim
            all_labels = skim.label(mask, 8, return_num=False)
            selected_labels = np.unique(all_labels[sly, slx])
            mask[~np.isin(all_labels, selected_labels)] = False
        if self.remove_select_action.isChecked():
            arr[mask & data_mask] = -1
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[mask & data_mask] = arr.max() + 1
        return arr

    def _remove_selected_labels(self):
        self.data_obj.remove_selected_labels(disable=True)

    def _disable_selection(self):
            return self.data_obj.disable_label_selection()

    def start_selection(self, arr=None, rgba=None,
                        rect_callbacks=None, poly_callbacks=None,
                        apply_funcs=(), cancel_funcs=(), remove_on_apply=True):
        """Start the selection in the current :attr:`data_obj`

        Parameters
        ----------
        arr: np.ndarray
            The labeled selection array that is used. If specified, the
            :meth:`~straditize.label_selection.enable_label_selection` method
            is called of the :attr:`data_obj` with the given `arr`. If this
            parameter is ``None``, then we expect that this method has already
            been called
        rgba: np.ndarray
            The RGBA image that shall be used for the color selection
            (see the :meth:`set_color_wand_mode`)
        rect_callbacks: list
            A list of callbacks that shall be called after a rectangle
            selection has been made by the user (see :attr:`rect_callbacks`)
        poly_callbacks: list
            A list of callbacks that shall be called after a polygon
            selection has been made by the user (see :attr:`poly_callbacks`)
        apply_funcs: list
            A list of callables that shall be connected to the
            :attr:`~straditize.widgets.StraditizerWidgets.apply_button`
        cancel_funcs: list
            A list of callables that shall be connected to the
            :attr:`~straditize.widgets.StraditizerWidgets.cancel_button`
        remove_on_apply: bool
            If True and the
            :attr:`~straditize.widgets.StraditizerWidgets.apply_button` is
            clicked, the selected labels will be removed."""
        obj = self.data_obj
        if arr is not None:
            obj.enable_label_selection(
                arr, arr.max(), set_picker=False,
                zorder=obj.plot_im.zorder + 0.1,
                extent=obj.plot_im.get_extent())
        self._selecting = True
        self._rgba = rgba
        if rgba is None:
            self.set_label_wand_mode()
            self._wand_actions['color_select'].setEnabled(False)
        else:
            self._wand_actions['color_select'].setEnabled(True)
        self.connect2apply(
            (self._remove_selected_labels if remove_on_apply else
             self._disable_selection),
            obj.remove_small_selection_ellipses, obj.draw_figure,
            self.end_selection, *apply_funcs)
        self.connect2cancel(self._disable_selection,
                            obj.remove_small_selection_ellipses,
                            obj.draw_figure,
                            self.end_selection, *cancel_funcs)
        if self.should_be_enabled(self._appearance_actions['alpha']):
            self.update_alpha(self.sl_alpha.value())
        for w in chain(self._actions.values(),
                       self._appearance_actions.values()):
            w.setEnabled(self.should_be_enabled(w))
        if remove_on_apply:
            self.straditizer_widgets.apply_button.setText('Remove')
        if rect_callbacks is not None:
            self._rect_callbacks = rect_callbacks[:]
        if poly_callbacks is not None:
            self._poly_callbacks = poly_callbacks[:]
        del obj