Beispiel #1
0
class ColumnNamesManager(StraditizerControlBase, DockMixin,
                         QtWidgets.QSplitter):
    """Manage the column names of the reader"""

    refreshing = _temp_bool_prop('refreshing',
                                 doc="True if the widget is refreshing")

    #: The matplotlib image of the
    #: :attr:`straditize.colnames.ColNamesReader.rotated_image`
    im_rotated = None

    #: The rectangle to highlight a column (see :meth:`highlight_selected_col`)
    rect = None

    #: The canvas to display the :attr:`im_rotated`
    main_canvas = None

    #: The :class:`matplotlib.axes.Axes` to display the :attr:`im_rotated`
    main_ax = None

    #: The original width of the :attr:`main_canvas`
    fig_w = None

    #: The original height of the :attr:`main_canvas`
    fig_h = None

    #: The matplotlib image of the :attr:`colpic`
    colpic_im = None

    #: The canvas to display the :attr:`colpic_im`
    colpic_canvas = None

    #: The :class:`matplotlib.axes.Axes` to display the :attr:`colpic_im`
    colpic_ax = None

    #: The extents of the :attr:`colpic` in the :attr:`im_rotated`
    colpic_extents = None

    #: A QTableWidget to display the column names
    colnames_table = None

    #: The :class:`matplotlib.widgets.RectangleSelector` to select the
    #: :attr:`colpic`
    selector = None

    #: The :class:`PIL.Image.Image` of the column name (see also
    #: :attr:`straditize.colnames.ColNamesReader.colpics`)
    colpic = None

    #: A QPushButton to load the highres image
    btn_load_image = None

    #: A QPushButton to find column names in the visible part of the
    #: :attr:`im_rotated`
    btn_find = None

    #: A QPushButton to recognize text in the :attr:`colpic`
    btn_recognize = None

    #: A checkable QPushButton to initialize a :attr:`selector` to select the
    #: :attr:`colpic`
    btn_select_colpic = None

    #: The QPushButton in the :class:`straditize.widgets.StraditizerWidgets`
    #: to toggle the column names dialog
    btn_select_names = None

    #: A QCheckBox to find the column names (see :attr:`btn_find`) for all
    #: columns and not just the one selected in the :attr:`colnames_table`
    cb_find_all_cols = None

    #: A QCheckBox to ignore the part within the
    #: :attr:`straditize.colnames.ColNamesReader.data_ylim`
    cb_ignore_data_part = None

    #: A QLineEdit to set the :attr:`straditize.colnames.ColNamesReader.rotate`
    txt_rotate = None

    #: A QCheckBox to set the :attr:`straditize.colnames.ColNamesReader.mirror`
    cb_fliph = None

    #: A QCheckBox to set the :attr:`straditize.colnames.ColNamesReader.flip`
    cb_flipv = None

    NAVIGATION_LABEL = ("Use left-click of your mouse to move the image below "
                        "and right-click to zoom in and out.")

    SELECT_LABEL = "Left-click and hold on the image to select the column name"

    @property
    def current_col(self):
        """The currently selected column"""
        indexes = self.colnames_table.selectedIndexes()
        if len(indexes):
            return indexes[0].row()

    @property
    def colnames_reader(self):
        """The :attr:`straditize.straditizer.Straditizer.colnames_reader`
        of the current straditizer"""
        return self.straditizer.colnames_reader

    @docstrings.dedent
    def __init__(self, straditizer_widgets, item=None, *args, **kwargs):
        """
        Parameters
        ----------
        %(StraditizerControlBase.init_straditizercontrol.parameters)s
        """
        # Create the button for the straditizer_widgets tree
        self.btn_select_names = QtWidgets.QPushButton('Edit column names')
        self.btn_select_names.setCheckable(True)

        self.btn_select_colpic = QtWidgets.QPushButton('Select column name')
        self.btn_select_colpic.setCheckable(True)
        self.btn_select_colpic.setEnabled(False)
        self.btn_cancel_colpic_selection = QtWidgets.QPushButton('Cancel')
        self.btn_cancel_colpic_selection.setVisible(False)

        self.btn_load_image = QtWidgets.QPushButton('Load HR image')
        self.btn_load_image.setToolTip(
            'Select a version of this image with a higher resolution to '
            'improve the text recognition')
        self.btn_load_image.setCheckable(True)

        self.btn_recognize = QtWidgets.QPushButton('Recognize')
        self.btn_recognize.setToolTip('Use tesserocr to recognize the column '
                                      'name in the given image')

        self.btn_find = QtWidgets.QPushButton('Find column names')
        self.btn_find.setToolTip(
            'Find the columns names automatically in the image above using '
            'tesserocr')

        self.cb_find_all_cols = QtWidgets.QCheckBox("all columns")
        self.cb_find_all_cols.setToolTip(
            "Find the column names in all columns or only in the selected one")
        self.cb_find_all_cols.setChecked(True)

        self.cb_ignore_data_part = QtWidgets.QCheckBox("ignore data part")
        self.cb_ignore_data_part.setToolTip("ignore everything from the top "
                                            "to the bottom of the data part")

        super().__init__(Qt.Horizontal)

        # centers of the image
        self.xc = self.yc = None

        self.txt_rotate = QtWidgets.QLineEdit()
        self.txt_rotate.setValidator(QtGui.QDoubleValidator(0., 90., 3))
        self.txt_rotate.setPlaceholderText('0˚..90˚')

        self.cb_fliph = QtWidgets.QCheckBox('Flip horizontally')
        self.cb_flipv = QtWidgets.QCheckBox('Flip vertically')

        self.info_label = QtWidgets.QLabel()
        self.info_label.setWordWrap(True)
        self.info_label.setStyleSheet('border: 1px solid black')

        self.main_canvas = EmbededMplCanvas()
        self.main_ax = self.main_canvas.figure.add_axes([0, 0, 1, 1])
        self.main_toolbar = DummyNavigationToolbar2(self.main_canvas)
        self.main_toolbar.pan()

        left_widget = QtWidgets.QWidget()
        layout = QtWidgets.QFormLayout()
        layout.addRow(self.btn_load_image)
        layout.addRow(QtWidgets.QLabel('Rotate:'), self.txt_rotate)
        layout.addRow(self.cb_fliph)
        layout.addRow(self.cb_flipv)
        layout.addRow(self.cb_ignore_data_part)
        layout.addRow(self.info_label)
        layout.addRow(self.main_canvas)
        hbox = QtWidgets.QHBoxLayout()
        hbox.addWidget(self.btn_select_colpic)
        hbox.addWidget(self.btn_cancel_colpic_selection)
        layout.addRow(hbox)
        hbox = QtWidgets.QHBoxLayout()
        hbox.addWidget(self.btn_find)
        hbox.addWidget(self.cb_find_all_cols)
        layout.addRow(hbox)
        left_widget.setLayout(layout)

        self.colpic_canvas = EmbededMplCanvas()
        self.colpic_ax = self.colpic_canvas.figure.add_subplot(111)
        self.colpic_ax.axis("off")
        self.colpic_ax.margins(0)
        self.colpic_canvas.figure.subplots_adjust(bottom=0.3)

        self.colnames_table = QtWidgets.QTableWidget()
        self.colnames_table.setColumnCount(1)
        self.colnames_table.horizontalHeader().setHidden(True)
        self.colnames_table.setSelectionMode(
            QtWidgets.QTableView.SingleSelection)
        self.colnames_table.horizontalHeader().setSectionResizeMode(
            QtWidgets.QHeaderView.Stretch)

        self.vsplit = QtWidgets.QSplitter(Qt.Vertical)

        self.addWidget(left_widget)
        self.addWidget(self.vsplit)
        self.vsplit.addWidget(self.colnames_table)
        w = QtWidgets.QWidget()
        vbox = QtWidgets.QVBoxLayout()
        vbox.addWidget(self.colpic_canvas)
        vbox.addWidget(self.btn_recognize)
        w.setLayout(vbox)
        self.vsplit.addWidget(w)

        self.init_straditizercontrol(straditizer_widgets, item)

        self.widgets2disable = [
            self.btn_select_names, self.btn_find, self.btn_load_image,
            self.btn_select_colpic
        ]

        self.btn_select_names.clicked.connect(self.toggle_dialog)
        self.btn_select_colpic.clicked.connect(self.toggle_colpic_selection)
        self.btn_cancel_colpic_selection.clicked.connect(
            self.cancel_colpic_selection)
        self.txt_rotate.textChanged.connect(self.rotate)
        self.cb_fliph.stateChanged.connect(self.mirror)
        self.cb_flipv.stateChanged.connect(self.flip)
        self.colnames_table.itemSelectionChanged.connect(
            self.highlight_selected_col)
        self.colnames_table.cellChanged.connect(self.colname_changed)
        self.main_canvas.mpl_connect('resize_event',
                                     self.adjust_lims_after_resize)
        self.btn_load_image.clicked.connect(self.load_image)
        self.btn_recognize.clicked.connect(self.read_colpic)
        self.btn_find.clicked.connect(self._find_colnames)
        self.cb_find_all_cols.stateChanged.connect(
            self.enable_or_disable_btn_find)
        self.cb_ignore_data_part.stateChanged.connect(
            self.change_ignore_data_part)

    def colname_changed(self, row, column):
        """Update the column name in the :attr:`colnames_reader`

        This method is called when a cell in the :attr:`colnames_table` has
        been changed and updates the corresponding name in the
        :attr:`colnames_reader`

        Parameters
        ----------
        row: int
            The row of the cell in the :attr:`colnames_table` that changed
        column: int
            The column of the cell in the :attr:`colnames_table` that changed
        """
        self.colnames_reader._column_names[row] = self.colnames_table.item(
            row, column).text()

    def read_colpic(self):
        """Recognize the text in the :attr:`colpic`

        See Also
        --------
        straditize.colnames.ColNamesReader.recognize_text"""
        text = self.colnames_reader.recognize_text(self.colpic)
        self.colnames_table.item(self.current_col, 0).setText(text)
        self.colnames_reader._column_names[self.current_col] = text
        return text

    def load_image(self):
        """Load a high resolution image"""
        if self.btn_load_image.isChecked():
            fname = QtWidgets.QFileDialog.getOpenFileName(
                self.straditizer_widgets, 'Straditizer project',
                self.straditizer_widgets.menu_actions._start_directory,
                'Projects and images '
                '(*.nc *.nc4 *.pkl *.jpeg *.jpg *.pdf *.png *.raw *.rgba *.tif'
                ' *.tiff);;'
                'NetCDF files (*.nc *.nc4);;'
                'Pickle files (*.pkl);;'
                'All images '
                '(*.jpeg *.jpg *.pdf *.png *.raw *.rgba *.tif *.tiff);;'
                'Joint Photographic Experts Group (*.jpeg *.jpg);;'
                'Portable Document Format (*.pdf);;'
                'Portable Network Graphics (*.png);;'
                'Raw RGBA bitmap (*.raw *.rbga);;'
                'Tagged Image File Format(*.tif *.tiff);;'
                'All files (*)')
            fname = fname[0]
            if fname:
                from PIL import Image
                with Image.open(fname) as _image:
                    image = Image.fromarray(np.array(_image.convert('RGBA')),
                                            'RGBA')
                self.colnames_reader.highres_image = image

        else:
            self.colnames_reader.highres_image = None
        self.refresh()

    def cancel_colpic_selection(self):
        """Stop the colpic selection in the :attr:`im_rotated`"""
        self.colnames_reader._colpics = self._colpics_save
        if self.current_col is not None:
            self.colpic = self.colnames_reader.colpics[self.current_col]
        self.btn_select_colpic.setChecked(False)
        self.toggle_colpic_selection()

    def toggle_colpic_selection(self):
        """Enable or disable the colpic selection"""
        if (not self.btn_select_colpic.isChecked()
                and self.selector is not None):
            self.remove_selector()
            self.btn_select_colpic.setText('Select column name')
            if self.current_col is not None:
                self.colnames_reader._colpics[self.current_col] = self.colpic
            if self.colpic is None and self.colpic_im is not None:
                self.colpic_im.remove()
                del self.colpic_im
                self.colpic_canvas.draw()
            self.btn_cancel_colpic_selection.setVisible(False)
            self.main_canvas.toolbar.pan()
            self._colpics_save.clear()
            self.info_label.setText(self.NAVIGATION_LABEL)
        else:
            self.create_selector()
            self.btn_select_colpic.setText('Cancel')
            self.info_label.setText(self.SELECT_LABEL)
            self.main_canvas.toolbar.pan()
            self._colpics_save = list(self.colnames_reader.colpics)
            self.cb_find_all_cols.setChecked(False)
        self.main_canvas.draw()

    def remove_selector(self):
        """Remove and disconnect the :attr:`selector`"""
        self.selector.disconnect_events()
        for a in self.selector.artists:
            try:
                a.remove()
            except ValueError:
                pass
        self.main_canvas.draw()
        del self.selector
        self.main_canvas.mpl_disconnect(self.key_press_cid)

    def reset_control(self):
        """Reset the dialog"""
        if self.is_shown:
            self.hide_plugin()
            self.btn_select_names.setChecked(False)
        self.remove_images()
        self.cb_find_all_cols.setChecked(False)
        self.btn_select_colpic.setChecked(False)
        self.btn_cancel_colpic_selection.setVisible(False)
        if self.selector is not None:
            self.remove_selector()
        self.cb_fliph.setChecked(False)
        self.cb_flipv.setChecked(False)
        self.txt_rotate.blockSignals(True)
        self.txt_rotate.setText('0')
        self.txt_rotate.blockSignals(False)

    def create_selector(self):
        """Create the :attr:`selector` to enable :attr:`colpic` selection"""
        self.selector = RectangleSelector(self.main_ax,
                                          self.update_image,
                                          interactive=True)
        if self.colpic_extents is not None:
            self.selector.extents = self.colpic_extents
        self.key_press_cid = self.main_canvas.mpl_connect(
            'key_press_event', self.update_image)

    def plot_colpic(self):
        """Plot the :attr:`colpic` in the :attr:`colpic_ax`"""
        try:
            self.colpic_im.remove()
        except (AttributeError, ValueError):
            pass
        self.colpic_im = self.colpic_ax.imshow(self.colpic)
        self.colpic_canvas.draw()

    def update_image(self, *args, **kwargs):
        """Update the :attr:`colpic` with the extents of the :attr:`selector`

        ``*args`` and ``**kwargs`` are ignored
        """
        self.colpic_extents = np.round(self.selector.extents).astype(int)
        x, y = self.colpic_extents.reshape((2, 2))
        x0, x1 = sorted(x)
        y0, y1 = sorted(y)
        self.colpic = self.colnames_reader._colpics[self.current_col] = \
            self.colnames_reader.get_colpic(x0, y0, x1, y1)
        self.plot_colpic()
        self.btn_select_colpic.setText('Apply')
        self.btn_cancel_colpic_selection.setVisible(True)
        self.btn_recognize.setEnabled(
            self.should_be_enabled(self.btn_recognize))

    def highlight_selected_col(self):
        """Highlight the column selected in the :attr:`colnames_tables`

        See Also
        --------
        straditize.colnames.ColNamesReader.highlight_column"""
        draw = False
        if self.rect is not None:
            self.rect.remove()
            draw = True
            del self.rect
        col = self.current_col
        if col is not None:
            reader = self.straditizer.colnames_reader
            self.rect = reader.highlight_column(col, self.main_ax)
            reader.navigate_to_col(col, self.main_ax)
            self.btn_select_colpic.setEnabled(True)
            if self.colpic_im is not None:
                self.colpic_im.remove()
                del self.colpic_im
            self.colpic = colpic = self.colnames_reader.colpics[col]
            if colpic is not None:
                self.colpic_im = self.colpic_ax.imshow(colpic)
            self.colpic_canvas.draw()
            self.btn_recognize.setEnabled(
                self.should_be_enabled(self.btn_recognize))
            draw = True
        else:
            self.btn_select_colpic.setEnabled(False)
        if draw:
            self.main_canvas.draw()
        self.enable_or_disable_btn_find()

    def enable_or_disable_btn_find(self, *args, **kwargs):
        self.btn_find.setEnabled(self.should_be_enabled(self.btn_find))

    def setup_children(self, item):
        child = QtWidgets.QTreeWidgetItem(0)
        item.addChild(child)
        self.straditizer_widgets.tree.setItemWidget(child, 0,
                                                    self.btn_select_names)

    def should_be_enabled(self, w):
        ret = self.straditizer is not None and getattr(
            self.straditizer.data_reader, '_column_starts', None) is not None
        if ret and w is self.btn_find:
            from straditize.colnames import tesserocr
            ret = tesserocr is not None and (self.cb_find_all_cols.isChecked()
                                             or self.current_col is not None)
        elif ret and w is self.btn_recognize:
            from straditize.colnames import tesserocr
            ret = tesserocr is not None and self.colpic is not None
        return ret

    def toggle_dialog(self):
        """Close the dialog when the :attr:`btn_select_names` button is clicked
        """
        from psyplot_gui.main import mainwindow
        if not self.refreshing:
            if not self.btn_select_names.isChecked() or (self.dock is not None
                                                         and self.is_shown):
                self.hide_plugin()
                if self.btn_select_colpic.isChecked():
                    self.btn_select_colpic.setChecked(False)
                    self.toggle_colpic_selection()
            elif self.btn_select_names.isEnabled():
                self.straditizer_widgets.tree.itemWidget(
                    self.straditizer_widgets.col_names_item, 1).show_docs()
                self.to_dock(mainwindow, 'Straditizer column names')
                self.info_label.setText(self.NAVIGATION_LABEL)
                self.show_plugin()
                self.dock.raise_()
                self.widget(0).layout().update()
            self.refresh()

    def _maybe_check_btn_select_names(self):
        if self.dock is None:
            return
        self.btn_select_names.blockSignals(True)
        self.btn_select_names.setChecked(
            self.dock.toggleViewAction().isChecked())
        self.btn_select_names.blockSignals(False)

    def refresh(self):
        with self.refreshing:
            super().refresh()
            self.btn_select_names.setChecked(self.btn_select_names.isEnabled()
                                             and self.dock is not None
                                             and self.is_shown)
        if self.btn_select_names.isEnabled():
            names = self.straditizer.colnames_reader.column_names
            self.colnames_table.setRowCount(len(names))
            for i, name in enumerate(names):
                self.colnames_table.setItem(i, 0,
                                            QtWidgets.QTableWidgetItem(name))
            self.colnames_table.setVerticalHeaderLabels(
                list(map(str, range(len(names)))))
            self.replot_figure()
            reader = self.colnames_reader
            self.txt_rotate.setText(str(reader.rotate))
            self.cb_fliph.setChecked(reader.mirror)
            self.cb_flipv.setChecked(reader.flip)
            self.cb_ignore_data_part.setChecked(reader.ignore_data_part)

            image = reader._highres_image
            if image is reader.image:
                image = None
            if image is not None:
                self.btn_load_image.setText('HR image with size {}'.format(
                    image.size))
                self.btn_load_image.setToolTip(
                    'Remove and ignore the high resolution image')
                checked = True
            else:
                self.btn_load_image.setText('Load HR image')
                self.btn_load_image.setToolTip(
                    'Select a version of this image with a higher resolution '
                    'to improve the text recognition')
                checked = False
            self.btn_load_image.blockSignals(True)
            self.btn_load_image.setChecked(checked)
            self.btn_load_image.blockSignals(False)
            self.btn_recognize.setEnabled(
                self.should_be_enabled(self.btn_recognize))
        else:
            self.colnames_table.setRowCount(0)
            self.remove_images()

    def remove_images(self):
        """Remove the :attr:`im_rotated` and the :attr:`colpic_im`"""
        try:
            self.im_rotated.remove()
        except (AttributeError, ValueError):
            pass
        try:
            self.colpic_im.remove()
        except (AttributeError, ValueError):
            pass
        self.im_rotated = self.colpic_im = self.xc = self.yc = None

    def set_xc_yc(self):
        """Set the x- and y-center before rotating or flipping"""
        xc = np.mean(self.main_ax.get_xlim())
        yc = np.mean(self.main_ax.get_ylim())
        self.xc, self.yc = self.colnames_reader.transform_point(xc, yc, True)

    def flip(self, checked):
        """TFlip the image"""
        self.set_xc_yc()
        self.colnames_reader.flip = checked == Qt.Checked
        self.replot_figure()

    def mirror(self, checked):
        """Mirror the image"""
        self.set_xc_yc()
        self.colnames_reader.mirror = checked == Qt.Checked
        self.replot_figure()

    def change_ignore_data_part(self, checked):
        """Change :attr:`straditize.colnames.ColNamesReader.ignore_data_part`
        """
        self.colnames_reader.ignore_data_part = checked == Qt.Checked

    def rotate(self, val):
        """Rotate the image

        Parameters
        ----------
        float
            The angle for the rotation"""
        if not str(val).strip():
            return
        try:
            val = float(val)
        except (ValueError, TypeError):
            val = 0
        self.set_xc_yc()
        self.colnames_reader.rotate = val
        self.replot_figure()

    def replot_figure(self):
        """Remove and replot the :attr:`im_rotated`"""
        adjust_lims = self.im_rotated is None
        ax = self.main_ax
        if not self.is_shown:
            return
        elif self.im_rotated:
            rotated = self.straditizer.colnames_reader.rotated_image
            if np.all(self.im_rotated.get_array() == np.asarray(rotated)):
                return
            else:
                try:
                    self.im_rotated.remove()
                except ValueError:
                    pass
        else:
            rotated = self.straditizer.colnames_reader.rotated_image
        self.im_rotated = ax.imshow(rotated)
        if self.xc is not None:
            dx = np.diff(ax.get_xlim()) / 2.
            dy = np.diff(ax.get_ylim()) / 2.
            xc, yc = self.colnames_reader.transform_point(self.xc, self.yc)
            ax.set_xlim(xc - dx, xc + dx)
            ax.set_ylim(yc - dy, yc + dy)
        self.highlight_selected_col()
        self.xc = self.yc = None
        if adjust_lims:
            self.adjust_lims()

    def adjust_lims(self):
        """Adjust the limits of the :attr:`main_ax` to fill the entire figure
        """
        size = xs, ys = np.array(self.im_rotated.get_size())
        ax = self.main_ax
        figw, figh = ax.figure.get_figwidth(), ax.figure.get_figheight()
        woh = figw / figh  # width over height
        how = figh / figw  # height over width
        limits = np.array([[xs, xs * how], [xs * woh, xs], [ys, ys * how],
                           [ys * woh, ys]])
        x, y = min(filter(lambda a: (a >= size).all(), limits),
                   key=lambda a: (a - size).max())
        ax.set_xlim(0, x)
        ax.set_ylim(y, 0)
        ax.axis('off')
        ax.margins(0)
        ax.set_position([0, 0, 1, 1])

    def to_dock(self, main, title=None, position=None, *args, **kwargs):
        if position is None:
            if main.centralWidget() is not main.help_explorer:
                position = main.dockWidgetArea(main.help_explorer.dock)
            else:
                position = Qt.RightDockWidgetArea
        connect = self.dock is None
        ret = super(ColumnNamesManager, self).to_dock(main, title, position,
                                                      *args, **kwargs)
        if connect:
            action = self.dock.toggleViewAction()
            action.triggered.connect(self.maybe_tabify)
            action.triggered.connect(self._maybe_check_btn_select_names)
        return ret

    def maybe_tabify(self):
        main = self.dock.parent()
        if self.is_shown and main.dockWidgetArea(
                main.help_explorer.dock) == main.dockWidgetArea(self.dock):
            main.tabifyDockWidget(main.help_explorer.dock, self.dock)

    def adjust_lims_after_resize(self, event):
        """Adjust the limits of the :attr:`main_ax` after resize of the figure
        """
        h = event.height
        w = event.width
        if self.fig_w is None:
            self.fig_w = w
            self.fig_h = h
            self.adjust_lims()
            return
        ax = self.main_ax
        dx = np.diff(ax.get_xlim())[0]
        dy = np.diff(ax.get_ylim())[0]
        new_dx = dx * w / self.fig_w
        new_dy = dy * h / self.fig_h
        xc = np.mean(ax.get_xlim())
        yc = np.mean(ax.get_ylim())
        ax.set_xlim(xc - new_dx / 2, xc + new_dx / 2)
        ax.set_ylim(yc - new_dy / 2, yc + new_dy / 2)
        self.fig_w = w
        self.fig_h = h

    def _find_colnames(self):
        return self.find_colnames()

    def find_colnames(self, warn=True, full_image=False, all_cols=None):
        """Find the column names automatically

        See Also
        --------
        straditize.colnames.ColNamesReader.find_colnames"""
        ys, xs = self.im_rotated.get_size()
        x0, x1 = self.main_ax.get_xlim() if not full_image else (0, xs)
        y0, y1 = sorted(self.main_ax.get_ylim()) if not full_image else (0, ys)
        x0 = max(x0, 0)
        y0 = max(y0, 0)
        x1 = min(x1, xs)
        y1 = min(y1, ys)
        reader = self.colnames_reader
        texts, images, boxes = reader.find_colnames([x0, y0, x1, y1])
        # make sure we have the exact length
        reader.column_names
        reader.colpics
        all_cols = all_cols or (all_cols is None
                                and self.cb_find_all_cols.isChecked())
        if not all_cols and self.current_col not in texts:
            if self.current_col is not None:
                msg = ("Could not find a column name of column %i in the "
                       "selected image!" % self.current_col)
                if warn:
                    QtWidgets.QMessageBox.warning(
                        self.straditizer_widgets, 'Could not find column name',
                        msg)
            return msg
        elif not texts:
            msg = "Could not find any column name in the selected image!"
            if warn:
                QtWidgets.QMessageBox.warning(self.straditizer_widgets,
                                              'Could not find column name',
                                              msg)
            return msg
        elif not all_cols:
            texts = {self.current_col: texts[self.current_col]}
        for col, text in texts.items():
            self.colnames_table.setItem(col, 0,
                                        QtWidgets.QTableWidgetItem(text))
            self.colnames_reader._colpics[col] = images[col]
        if self.current_col is not None:
            self.colpic = self.colnames_reader._colpics[self.current_col]
            if self.selector is not None:
                box = boxes[self.current_col]
                self.colpic_extents = np.round(box.extents).astype(int)
                self.remove_selector()
                self.create_selector()
                self.main_canvas.draw()
            self.plot_colpic()
Beispiel #2
0
class UrlBrowser(QFrame):
    """Very simple browser with session history and autocompletion based upon
    the :class:`PyQt5.QtWebEngineWidgets.QWebEngineView` class

    Warnings
    --------
    This class is known to crash under PyQt4 when new web page domains are
    loaded. Hence it should be handled with care"""

    completed = _temp_bool_prop(
        'completed',
        "Boolean whether the html page loading is completed.",
        default=True)

    url_like_re = re.compile('^\w+://')

    doc_urls = OrderedDict([
        ('startpage', 'https://startpage.com/'),
        ('psyplot', 'http://psyplot.readthedocs.org/en/latest/'),
        ('pyplot', 'http://matplotlib.org/api/pyplot_api.html'),
        ('seaborn', 'http://stanford.edu/~mwaskom/software/seaborn/api.html'),
        ('cartopy', 'http://scitools.org.uk/cartopy/docs/latest/index.html'),
        ('xarray', 'http://xarray.pydata.org/en/stable/'),
        ('pandas', 'http://pandas.pydata.org/pandas-docs/stable/'),
        ('numpy', 'https://docs.scipy.org/doc/numpy/reference/routines.html'),
    ])

    #: The initial url showed in the webview. If None, nothing will be
    #: displayed
    default_url = None

    #: adress line
    tb_url = None

    #: button to go to previous url
    bt_back = None

    #: button to go to next url
    bt_ahead = None

    #: refresh the current url
    bt_refresh = None

    #: button to go lock to the current url
    bt_lock = None

    #: button to disable browsing in www
    bt_url_lock = None

    #: The upper part of the browser containing all the buttons
    button_box = None

    #: The upper most layout aranging the button box and the html widget
    vbox = None

    def __init__(self, *args, **kwargs):
        super(UrlBrowser, self).__init__(*args, **kwargs)

        # ---------------------------------------------------------------------
        # ---------------------------- upper buttons --------------------------
        # ---------------------------------------------------------------------
        # adress line
        self.tb_url = UrlCombo(self)
        # button to go to previous url
        self.bt_back = QToolButton(self)
        # button to go to next url
        self.bt_ahead = QToolButton(self)
        # refresh the current url
        self.bt_refresh = QToolButton(self)
        # button to go lock to the current url
        self.bt_lock = QToolButton(self)
        # button to disable browsing in www
        self.bt_url_lock = QToolButton(self)

        # ---------------------------- buttons settings -----------------------
        self.bt_back.setIcon(QIcon(get_icon('previous.png')))
        self.bt_back.setToolTip('Go back one page')
        self.bt_ahead.setIcon(QIcon(get_icon('next.png')))
        self.bt_back.setToolTip('Go forward one page')

        self.bt_refresh.setIcon(QIcon(get_icon('refresh.png')))
        self.bt_refresh.setToolTip('Refresh the current page')

        self.bt_lock.setCheckable(True)
        self.bt_url_lock.setCheckable(True)

        if not with_qt5 and rcParams['help_explorer.online'] is None:
            # We now that the browser can crash with Qt4, therefore we disable
            # the browing in the internet
            self.bt_url_lock.click()
            rcParams['help_explorer.online'] = False
        elif rcParams['help_explorer.online'] is False:
            self.bt_url_lock.click()
        elif rcParams['help_explorer.online'] is None:
            rcParams['help_explorer.online'] = True
        rcParams.connect('help_explorer.online', self.update_url_lock_from_rc)

        self.bt_url_lock.clicked.connect(self.toogle_url_lock)
        self.bt_lock.clicked.connect(self.toogle_lock)

        # tooltip and icons of lock and url_lock are set in toogle_lock and
        # toogle_url_lock
        self.toogle_lock()
        self.toogle_url_lock()

        # ---------------------------------------------------------------------
        # --------- initialization and connection of the web view -------------
        # ---------------------------------------------------------------------

        #: The actual widget showing the html content
        self.html = QWebEngineView(parent=self)
        self.html.loadStarted.connect(self.completed)
        self.html.loadFinished.connect(self.completed)

        self.tb_url.currentIndexChanged[str].connect(self.browse)
        self.bt_back.clicked.connect(self.html.back)
        self.bt_ahead.clicked.connect(self.html.forward)
        self.bt_refresh.clicked.connect(self.html.reload)
        self.html.urlChanged.connect(self.url_changed)

        # ---------------------------------------------------------------------
        # ---------------------------- layouts --------------------------------
        # ---------------------------------------------------------------------

        # The upper part of the browser containing all the buttons
        self.button_box = button_box = QHBoxLayout()

        button_box.addWidget(self.bt_back)
        button_box.addWidget(self.bt_ahead)
        button_box.addWidget(self.tb_url)
        button_box.addWidget(self.bt_refresh)
        button_box.addWidget(self.bt_lock)
        button_box.addWidget(self.bt_url_lock)

        # The upper most layout aranging the button box and the html widget
        self.vbox = vbox = QVBoxLayout()
        self.vbox.setContentsMargins(0, 0, 0, 0)
        vbox.addLayout(button_box)

        vbox.addWidget(self.html)

        self.setLayout(vbox)

        if self.default_url is not None:
            self.tb_url.addItem(self.default_url)

    def browse(self, url):
        """Make a web browse on the given url and show the page on the Webview
        widget. """
        if self.bt_lock.isChecked():
            return
        if not self.url_like_re.match(url):
            url = 'https://' + url
        if self.bt_url_lock.isChecked() and url.startswith('http'):
            return
        if not self.completed:
            logger.debug('Stopping current load...')
            self.html.stop()
            self.completed = True
        logger.debug('Loading %s', url)
        # we use :meth:`PyQt5.QtWebEngineWidgets.QWebEngineView.setUrl` instead
        # of :meth:`PyQt5.QtWebEngineWidgets.QWebEngineView.load` because that
        # changes the url directly and is more useful for unittests
        self.html.setUrl(QtCore.QUrl(url))

    def url_changed(self, url):
        """Triggered when the url is changed to update the adress line"""
        try:
            url = url.toString()
        except AttributeError:
            pass
        logger.debug('url changed to %s', url)
        try:
            self.tb_url.setCurrentText(url)
        except AttributeError:  # Qt4
            self.tb_url.setEditText(url)
        self.tb_url.add_text_on_top(url, block=True)

    def update_url_lock_from_rc(self, online):
        if (online and self.bt_url_lock.isChecked()
                or not online and not self.bt_url_lock.isChecked()):
            self.bt_url_lock.click()

    def toogle_url_lock(self):
        """Disable (or enable) the loading of web pages in www"""
        bt = self.bt_url_lock
        offline = bt.isChecked()
        bt.setIcon(QIcon(
            get_icon('world_red.png' if offline else 'world.png')))
        online_message = "Go online"
        if not with_qt5:
            online_message += ("\nWARNING: This mode is unstable under Qt4 "
                               "and might result in a complete program crash!")
        bt.setToolTip(online_message if offline else "Offline mode")
        if rcParams['help_explorer.online'] is offline:
            rcParams['help_explorer.online'] = not offline

    def toogle_lock(self):
        """Disable (or enable) the changing of the current webpage"""
        bt = self.bt_lock
        bt.setIcon(
            QIcon(get_icon('lock.png' if bt.isChecked() else 'lock_open.png')))
        bt.setToolTip("Unlock" if bt.isChecked() else "Lock to current page")
Beispiel #3
0
        class Test(object):

            test = utils._temp_bool_prop('test')
Beispiel #4
0
class FormatoptionWidget(QWidget, DockMixin):
    """
    Widget to update the formatoptions of the current project

    This widget, mainly made out of a combobox for the formatoption group,
    a combobox for the formatoption, and a one-line text editor, is designed
    for updating the selected formatoptions for the current subproject.

    The widget is connected to the :attr:`psyplot.project.Project.oncpchange`
    signal and refills the comboboxes if the current subproject changes.

    The one-line text editor accepts python code that will be executed in side
    the given `shell`.
    """

    no_fmtos_update = _temp_bool_prop('no_fmtos_update',
                                      """update the fmto combo box or not""")

    #: The combobox for the formatoption groups
    group_combo = None

    #: The combobox for the formatoptions
    fmt_combo = None

    #: The help_explorer to display the documentation of the formatoptions
    help_explorer = None

    #: The shell to execute the update of the formatoptions in the current
    #: project
    shell = None

    def __init__(self, *args, **kwargs):
        """
        Parameters
        ----------
        help_explorer: psyplot_gui.help_explorer.HelpExplorer
            The help explorer to show the documentation of one formatoption
        shell: IPython.core.interactiveshell.InteractiveShell
            The shell that can be used to update the current subproject via::

                psy.gcp().update(**kwargs)

            where ``**kwargs`` is defined through the selected formatoption
            in the :attr:`fmt_combo` combobox and the value in the
            :attr:`line_edit` editor
        ``*args, **kwargs``
            Any other keyword for the QWidget class
        """
        help_explorer = kwargs.pop('help_explorer', None)
        shell = kwargs.pop('shell', None)
        super(FormatoptionWidget, self).__init__(*args, **kwargs)
        self.help_explorer = help_explorer
        self.shell = shell

        # ---------------------------------------------------------------------
        # -------------------------- Child widgets ----------------------------
        # ---------------------------------------------------------------------
        self.group_combo = QComboBox(parent=self)
        self.fmt_combo = QComboBox(parent=self)
        self.line_edit = QLineEdit(parent=self)
        self.run_button = QToolButton(parent=self)

        self.keys_button = QPushButton('Formatoption keys', parent=self)
        self.summaries_button = QPushButton('Summaries', parent=self)
        self.docs_button = QPushButton('Docs', parent=self)

        self.grouped_cb = QCheckBox('grouped', parent=self)
        self.all_groups_cb = QCheckBox('all groups', parent=self)
        self.include_links_cb = QCheckBox('include links', parent=self)

        # ---------------------------------------------------------------------
        # -------------------------- Descriptions -----------------------------
        # ---------------------------------------------------------------------

        self.group_combo.setToolTip('Select the formatoption group')
        self.fmt_combo.setToolTip('Select the formatoption to update')
        self.line_edit.setToolTip(
            'Insert the value which what you want to update the selected '
            'formatoption and hit right button. The code is executed in the '
            'main console.')
        self.run_button.setIcon(QIcon(get_icon('run_arrow.png')))
        self.run_button.setToolTip('Update the selected formatoption')
        self.keys_button.setToolTip(
            'Show the formatoption keys in this group (or in all '
            'groups) in the help explorer')
        self.summaries_button.setToolTip(
            'Show the formatoption summaries in this group (or in all '
            'groups) in the help explorer')
        self.docs_button.setToolTip(
            'Show the formatoption documentations in this group (or in all '
            'groups) in the help explorer')
        self.grouped_cb.setToolTip(
            'Group the formatoptions before displaying them in the help '
            'explorer')
        self.all_groups_cb.setToolTip('Use all groups when displaying the '
                                      'keys, docs or summaries')
        self.include_links_cb.setToolTip(
            'Include links to remote documentations when showing the '
            'keys, docs and summaries in the help explorer (requires '
            'intersphinx)')

        # ---------------------------------------------------------------------
        # -------------------------- Connections ------------------------------
        # ---------------------------------------------------------------------
        self.group_combo.currentIndexChanged[int].connect(self.fill_fmt_combo)
        self.fmt_combo.currentIndexChanged[int].connect(self.show_fmt_info)
        self.run_button.clicked.connect(self.run_code)
        self.line_edit.returnPressed.connect(self.run_button.click)
        self.keys_button.clicked.connect(
            partial(self.show_all_fmt_info, 'keys'))
        self.summaries_button.clicked.connect(
            partial(self.show_all_fmt_info, 'summaries'))
        self.docs_button.clicked.connect(
            partial(self.show_all_fmt_info, 'docs'))

        # ---------------------------------------------------------------------
        # ------------------------------ Layouts ------------------------------
        # ---------------------------------------------------------------------
        self.combos = QHBoxLayout()
        self.combos.addWidget(self.group_combo)
        self.combos.addWidget(self.fmt_combo)

        self.execs = QHBoxLayout()
        self.execs.addWidget(self.line_edit)
        self.execs.addWidget(self.run_button)

        self.info_box = QHBoxLayout()
        self.info_box.addStretch(0)
        for w in [
                self.keys_button, self.summaries_button, self.docs_button,
                self.all_groups_cb, self.grouped_cb, self.include_links_cb
        ]:
            self.info_box.addWidget(w)

        self.vbox = QVBoxLayout()
        self.vbox.addLayout(self.combos)
        self.vbox.addLayout(self.execs)
        self.vbox.addLayout(self.info_box)

        self.setLayout(self.vbox)

        # fill with content
        self.fill_combos_from_project(psy.gcp())
        psy.Project.oncpchange.connect(self.fill_combos_from_project)

    def fill_combos_from_project(self, project):
        """Fill :attr:`group_combo` and :attr:`fmt_combo` from a project

        Parameters
        ----------
        project: psyplot.project.Project
            The project to use"""
        current_text = self.group_combo.currentText()
        with self.no_fmtos_update:
            self.group_combo.clear()
            if project is None or project.is_main or not len(project.plotters):
                self.fmt_combo.clear()
                self.groups = []
                self.fmtos = []
                self.line_edit.setEnabled(False)
                return
            self.line_edit.setEnabled(True)
            # get dimensions
            coords = sorted(project.coords_intersect)
            coords_name = [COORDSGROUP] if coords else []
            coords_verbose = ['Dimensions'] if coords else []
            coords = [coords] if coords else []

            # get formatoptions and group them alphabetically
            grouped_fmts = defaultdict(list)
            for fmto in project._fmtos:
                grouped_fmts[fmto.group].append(fmto)
            for val in six.itervalues(grouped_fmts):
                val.sort(key=self.get_name)
            grouped_fmts = OrderedDict(
                sorted(six.iteritems(grouped_fmts),
                       key=lambda t: psyp.groups.get(t[0], t[0])))
            fmt_groups = list(grouped_fmts.keys())
            # save original names
            self.groups = coords_name + [ALLGROUP] + fmt_groups
            # save verbose group names (which are used in the combo box)
            self.groupnames = coords_verbose + ['All formatoptions'] + list(
                map(lambda s: psyp.groups.get(s, s), fmt_groups))
            # save formatoptions
            fmtos = list(grouped_fmts.values())
            self.fmtos = coords + [sorted(chain(*fmtos), key=self.get_name)
                                   ] + fmtos
            self.group_combo.addItems(self.groupnames)
            ind = self.group_combo.findText(current_text)
            self.group_combo.setCurrentIndex(ind if ind >= 0 else 0)
        self.fill_fmt_combo(self.group_combo.currentIndex())

    def get_name(self, fmto):
        """Get the name of a :class:`psyplot.plotter.Formatoption` instance"""
        if isinstance(fmto, six.string_types):
            return fmto
        return '%s (%s)' % (fmto.name, fmto.key) if fmto.name else fmto.key

    def fill_fmt_combo(self, i):
        """Fill the :attr:`fmt_combo` combobox based on the current group name
        """
        if not self.no_fmtos_update:
            with self.no_fmtos_update:
                current_text = self.fmt_combo.currentText()
                self.fmt_combo.clear()
                self.fmt_combo.addItems(list(map(self.get_name,
                                                 self.fmtos[i])))
                ind = self.fmt_combo.findText(current_text)
                self.fmt_combo.setCurrentIndex(ind if ind >= 0 else 0)
            self.show_fmt_info(self.fmt_combo.currentIndex())

    def show_fmt_info(self, i):
        """Show the documentation of the formatoption in the help explorer
        """
        group_ind = self.group_combo.currentIndex()
        if (not self.no_fmtos_update
                and self.groups[group_ind] != COORDSGROUP):
            fmto = self.fmtos[self.group_combo.currentIndex()][i]
            fmto.plotter.show_docs(
                fmto.key, include_links=self.include_links_cb.isChecked())

    def run_code(self):
        """Run the update of the project inside the :attr:`shell`"""
        text = str(self.line_edit.text())
        if not text or not self.fmtos:
            return
        group_ind = self.group_combo.currentIndex()
        if self.groups[group_ind] == COORDSGROUP:
            key = self.fmtos[group_ind][self.fmt_combo.currentIndex()]
            param = 'dims'
        else:
            key = self.fmtos[group_ind][self.fmt_combo.currentIndex()].key
            param = 'fmt'
        e = ExecutionResult()
        self.shell.run_code(
            "psy.gcp().update(%s={'%s': %s})" % (param, key, text), e)
        e.raise_error()

    def show_all_fmt_info(self, what):
        """Show the keys, summaries or docs of the formatoptions

        Calling this function let's the help browser show the documentation
        etc. of all docs or only the selected group determined by the state of
        the :attr:`grouped_cb` and :attr:`all_groups_cb` checkboxes

        Parameters
        ----------
        what: {'keys', 'summaries', 'docs'}
            Determines what to show"""
        if not self.fmtos:
            return
        if self.all_groups_cb.isChecked():
            fmtos = list(
                chain(*(fmto_group for i, fmto_group in enumerate(self.fmtos)
                        if not self.groups[i] in [ALLGROUP, COORDSGROUP])))
        else:
            if self.groups[self.group_combo.currentIndex()] == COORDSGROUP:
                return
            fmtos = self.fmtos[self.group_combo.currentIndex()]
        plotter = fmtos[0].plotter
        getattr(plotter, 'show_' +
                what)([fmto.key for fmto in fmtos],
                      grouped=self.grouped_cb.isChecked(),
                      include_links=self.include_links_cb.isChecked())
Beispiel #5
0
class CrossMarks(object):
    """
    A set of draggable marks in a matplotlib axes
    """
    @property
    def fig(self):
        """The :class:`matplotlib.figure.Figure` that this mark plots on"""
        return self.ax.figure

    @property
    def y(self):
        """The y-position of the mark"""
        return self.ya[self._i_hline]

    @y.setter
    def y(self, value):
        """The y-position of the mark"""
        self.ya[self._i_hline] = value

    @property
    def x(self):
        """The x-position of the mark"""
        return self.xa[self._i_vline]

    @x.setter
    def x(self, value):
        """The x-position of the mark"""
        self.xa[self._i_vline] = value

    @property
    def hline(self):
        """The current horizontal line"""
        return self.hlines[self._i_hline]

    @property
    def vline(self):
        """The current vertical line"""
        return self.vlines[self._i_vline]

    @property
    def pos(self):
        """The position of the current line"""
        return np.array([self.xa[self._i_vline], self.ya[self._i_hline]])

    @pos.setter
    def pos(self, value):
        """The position of the current line"""
        self.xa[self._i_vline] = value[0] if np.ndim(value) else value
        self.ya[self._i_hline] = value[1] if np.ndim(value) else value

    @property
    def points(self):
        """The x-y-coordinates of the points as a (N, 2)-shaped array"""
        return np.array(list(product(self.xa, self.ya)))

    @property
    def line_connections(self):
        """The line connections to the current position"""
        return self._all_line_connections[self._i_hline][self._i_vline]

    @line_connections.setter
    def line_connections(self, value):
        """The line connections to the current position"""
        self._all_line_connections[self._i_hline][self._i_vline] = value

    @property
    def other_connections(self):
        """All other connections to the current position"""
        return self._all_other_connections[self._i_hline][self._i_vline]

    @other_connections.setter
    def other_connections(self, value):
        """All other connections to the current position"""
        self._all_other_connections[self._i_hline][self._i_vline] = value

    @property
    def idx_h(self):
        """The index for vertical lines"""
        return None if not self._idx_h else self._idx_h[self._i_vline]

    @idx_h.setter
    def idx_h(self, value):
        """The index for vertical lines"""
        if self._idx_h is None:
            self._idx_h = [None] * len(self.xa)
        self._idx_h[self._i_vline] = value

    @property
    def idx_v(self):
        """The index for horizontal lines"""
        return None if not self._idx_v else self._idx_v[self._i_hline]

    @idx_v.setter
    def idx_v(self, value):
        """The index for horizontal lines"""
        if self._idx_v is None:
            self._idx_v = [None] * len(self.ya)
        self._idx_v[self._i_hline] = value

    #: Boolean to control whether the vertical lines should be hidden
    hide_vertical = False

    #: Boolean to control whether the horizontal lines should be hidden
    hide_horizontal = False

    #: A signal that is emitted when the mark is moved. Connected function are
    #: expected to accept two arguments. One tuple with the old position and
    #: the CrossMarks instance itself
    moved = Signal('_moved')

    block_signals = _temp_bool_prop(
        'block_signals', "Block the emitting of signals of this instance")

    #: The index of the selected hline
    _i_hline = 0

    #: The index of the selected vline
    _i_vline = 0

    #: Boolean that is True, if the animated property of the lines should be
    #: used
    _animated = True

    #: The matplotlib axes to plot on
    ax = None

    #: The x-limits of the :attr:`hlines`
    xlim = None

    #: The x-limits of the :attr:`vlines`
    ylim = None

    #: Class attribute that is set to a :class:`CrossMark` instance to lock the
    #: selection of marks
    lock = None

    #: A boolean to control whether the connected artists should be shown
    #: at all
    show_connected_artists = True

    #: a list of :class:`matplotlib.artist.Artist` whose colors are changed
    #: when this mark is selected
    connected_artists = []

    #: The default properties of the unselected mark, complementing the
    #: :attr:`_select_props`
    _unselect_props = {}

    #: the list of horizontal lines
    hlines = []

    #: the list of vertical lines
    vlines = []

    @docstrings.get_sectionsf('CrossMarks')
    @docstrings.dedent
    def __init__(self,
                 pos=(0, 0),
                 ax=None,
                 selectable=['h', 'v'],
                 draggable=['h', 'v'],
                 idx_h=None,
                 idx_v=None,
                 xlim=None,
                 ylim=None,
                 select_props={'c': 'r'},
                 auto_hide=False,
                 connected_artists=[],
                 lock=True,
                 draw_lines=True,
                 hide_vertical=None,
                 hide_horizontal=None,
                 **kwargs):
        """
        Parameters
        ----------
        pos: tuple of 2 arrays
            The initial positions of the crosses. The first item marks the
            x-coordinates of the points, the second the y-coordinates
        ax: matplotlib.axes.Axes
            The axes object to draw to. If not specified and draw_lines is
            True, the current axes object is used
        selectable: list of {'x', 'y'}
            Determine whether only the x-, y-, or both lines should be
            selectable
        draggable: list of {'x', 'y'}
            Determine whether only the x-, y-, or both lines should be
            draggable
        idx_h: pandas.Index
            The index for the horizontal coordinates. If not provided, we
            use a continuous movement along x.
        idx_v: pandas.Index
            The index for the vertical coordinates. If not provided, we
            use a continuous movement along y.
        xlim: tuple of floats (xmin, xmax)
            The minimum and maximum x value for the lines
        ylim: tuple for floats (ymin, ymax)
            The minimum and maximum y value for the lines
        select_props: color
            The line properties for selected marks
        auto_hide: bool
            If True, the lines are hidden if they are not selected.
        connected_artists: list of artists
            List of artists whose properties should be changed to
            `select_props` when this marks is selected
        lock: bool
            If True, at most one mark can be selected at a time
        draw_lines: bool
            If True, the cross mark lines are drawn. Otherwise, you must call
            the `draw_lines` method explicitly
        hide_vertical: bool
            Boolean to control whether the vertical lines should be hidden. If
            None, the default class attribute is used
        hide_horizontal: bool
            Boolean to control whether the horizontal lines should be hidden.
            If None, the default class attribute is used
        ``**kwargs``
            Any other keyword argument that is passed to the
            :func:`matplotlib.pyplot.plot` function"""
        self.xa = np.asarray([pos[0]] if not np.ndim(pos[0]) else pos[0],
                             dtype=float)
        self.ya = np.asarray([pos[1]] if not np.ndim(pos[1]) else pos[1],
                             dtype=float)
        self._xa0 = self.xa.copy()
        self._ya0 = self.ya.copy()
        self._constant_dist_x = []
        self._constant_dist_x_marks = []
        self._constant_dist_y = []
        self._constant_dist_y_marks = []
        self.selectable = list(selectable)
        self.draggable = list(draggable)
        if hide_horizontal is not None:
            self.hide_horizontal = hide_horizontal
        if hide_vertical is not None:
            self.hide_vertical = hide_vertical
        self._select_props = select_props.copy()
        self.press = None
        if idx_h is not None:
            try:
                idx_h[0][0]
            except IndexError:
                idx_h = [idx_h] * len(self.xa)
        if idx_v is not None and np.ndim(idx_v) != 2:
            try:
                idx_v[0][0]
            except IndexError:
                idx_v = [idx_v] * len(self.ya)
        self._idx_h = idx_h
        self._idx_v = idx_v
        self.xlim = xlim
        self.ylim = ylim
        self.other_marks = []
        self._connection_visible = []
        self._all_line_connections = [[[] for _ in range(len(self.xa))]
                                      for _ in range(len(self.ya))]
        self._all_other_connections = [[[] for _ in range(len(self.xa))]
                                       for _ in range(len(self.ya))]
        self._lock_mark = lock
        kwargs.setdefault('marker', '+')
        self.auto_hide = auto_hide
        self._line_kwargs = kwargs
        self.set_connected_artists(list(connected_artists))
        if draw_lines:
            self.ax = ax
            self.draw_lines()
            self.connect()
        elif ax is not None:
            self.ax = ax

    def set_connected_artists(self, artists):
        """Set the connected artists

        Parameters
        ----------
        artists: matplotlib.artist.Artist
            The artists (e.g. other lines) that should be connected and
            highlighted if this mark is selected"""
        self.connected_artists = artists
        self._connected_artists_props = [{
            key: getattr(a, 'get_' + key)()
            for key in self._select_props
        } for a in artists]

    def draw_lines(self, **kwargs):
        """Draw the vertical and horizontal lines

        Parameters
        ----------
        ``**kwargs``
            An keyword that is passed to the :func:`matplotlib.pyplot.plot`
            function"""
        if kwargs:
            self._line_kwargs = kwargs
        else:
            kwargs = self._line_kwargs

        if self.ax is None:
            import matplotlib.pyplot as plt
            self.ax = plt.gca()

        if self.ylim is None:
            self.ylim = ylim = self.ax.get_ylim()
        else:
            ylim = self.ylim
        if self.xlim is None:
            self.xlim = xlim = self.ax.get_xlim()
        else:
            xlim = self.xlim

        xmin = min(xlim)
        xmax = max(xlim)
        ymin = min(ylim)
        ymax = max(ylim)
        xy = zip(repeat(self.xa), self.ya)
        x, y = next(xy)
        # we plot the first separate line to get the correct color
        line = self.ax.plot(np.r_[[xmin], x, [xmax]], [y] * (len(x) + 2),
                            markevery=slice(1,
                                            len(x) + 1),
                            label='cross_mark_hline',
                            visible=not self.hide_horizontal,
                            **kwargs)[0]
        if 'color' not in kwargs and 'c' not in kwargs:
            kwargs['c'] = line.get_c()
        # now the rest of the horizontal lines
        self.hlines = [line] + [
            self.ax.plot(np.r_[[xmin], x, [xmax]], [y] * (len(x) + 2),
                         markevery=slice(1,
                                         len(x) + 1),
                         label='cross_mark_hline',
                         visible=not self.hide_horizontal,
                         **kwargs)[0] for x, y in xy
        ]
        # and the vertical lines
        self.vlines = [
            self.ax.plot([x] * (len(y) + 2),
                         np.r_[[ymin], y, [ymax]],
                         markevery=slice(1,
                                         len(y) + 1),
                         label='cross_mark_vline',
                         visible=not self.hide_vertical,
                         **kwargs)[0] for x, y in zip(self.xa, repeat(self.ya))
        ]
        for h, v in zip(self.hlines, self.vlines):
            visible = v.get_visible()
            v.update_from(h)
            v.set_visible(visible)
        line = self.hlines[0]
        props = self._select_props
        if 'lw' not in props and 'linewidth' not in props:
            props.setdefault('lw', line.get_lw())
        # copy the current attributes from the lines
        self._unselect_props = {
            key: getattr(line, 'get_' + key)()
            for key in props
        }
        if self.auto_hide:
            for l in chain(self.hlines, self.vlines, self.line_connections):
                l.set_lw(0)

    def set_visible(self, b):
        """Set the visibility of the mark

        Parameters
        ----------
        b: bool
            If False, hide all horizontal and vertical lines, and the
            :attr:`connected_artists`"""
        for l in self.hlines:
            l.set_visible(b and not self.hide_horizontal)
        for l in self.vlines:
            l.set_visible(b and not self.hide_vertical)
        show_connected = self.show_connected_artists and b
        for l in self.connected_artists:
            l.set_visible(show_connected)

    def __reduce__(self):
        return (
            self.__class__,
            (
                (self.xa, self.ya),  # pos
                None,  # ax  --  do not make a plot
                self.selectable,  # selectable
                self.draggable,  # draggable
                self._idx_h,  # idx_h
                self._idx_v,  # idx_v
                self.xlim,  # xlim
                self.ylim,  # ylim
                self._select_props,  # select_props
                self.auto_hide,  # auto_hide
                [],  # connected_artists
                self._lock_mark,  # lock
                False,  # draw_lines  --  do not draw the lines
            ),
            {
                '_line_kwargs': self._line_kwargs,
                'hide_horizontal': self.hide_horizontal,
                'hide_vertical': self.hide_vertical,
                '_unselect_props': self._unselect_props,
                'xa': self.xa,
                'ya': self.ya
            })

    @staticmethod
    def maintain_y(marks):
        """Connect marks and maintain a constant vertical distance between them

        Parameters
        ----------
        marks: list of CrossMarks
            A list of marks. If one of the marks is moved vertically, the
            others are, too"""
        for mark in marks:
            mark._maintain_y([m for m in marks if m is not mark])

    def _maintain_y(self, marks):
        """Connect to marks and maintain a constant vertical distance

        Parameters
        ----------
        marks: list of CrossMarks
            A list of other marks. If this mark is moved vertically, the others
            are, too"""
        y = self.y
        self._constant_dist_y.extend(m.y - y for m in marks)
        self._constant_dist_y_marks.extend(marks)

    @staticmethod
    def maintain_x(marks):
        """Connect marks and maintain a constant horizontal distance

        Parameters
        ----------
        marks: list of CrossMarks
            A list of marks. If one of the marks is moved horizontally, the
            others are, too"""
        for mark in marks:
            mark._maintain_x([m for m in marks if m is not mark])

    def _maintain_x(self, marks):
        """Connect to marks and maintain a constant horizontal distance

        Parameters
        ----------
        marks: list of CrossMarks
            A list of other marks. If this mark is moved horizontally, the
            others are, too"""
        x = self.x
        self._constant_dist_x.extend(m.x - x for m in marks)
        self._constant_dist_x_marks.extend(marks)

    def connect_to_marks(self, marks, visible=False, append=True):
        """Append other marks that should be considered for aligning the lines

        Parameters
        ----------
        marks: list of CrossMarks
            A list of other marks
        visible: bool
            If True, the marks are connected through visible lines
        append: bool
            If True, the marks are appended. This is important if the mark
            will be moved by the `set_pos` method

        Notes
        -----
        This method can only be used to connect other marks with this mark.
        If you want to connect multiple marks within each other, use the
        :meth:`connect_marks` static method
        """
        if append:
            self.other_marks.extend(marks)
            self._connection_visible.extend([visible] * len(marks))
        if visible:
            ya = self.ya
            xa = self.xa
            ax = self.ax
            for m in marks:
                for i1, j1 in product(range(len(xa)), range(len(ya))):
                    self.set_current_point(i1, j1)
                    pos = self.pos
                    for i2, j2 in product(range(len(m.xa)), range(len(m.ya))):
                        m.set_current_point(i2, j2)
                        line = ax.plot([pos[0], m.pos[0]], [pos[1], m.pos[1]],
                                       label='cross_mark_connection',
                                       **self._unselect_props)[0]
                        if self.auto_hide:
                            line.set_lw(0)
                        self.line_connections.append(line)
                        m.other_connections.append(line)

    @staticmethod
    def connect_marks(marks, visible=False):
        """Connect multiple marks to each other

        Parameters
        ----------
        marks: list of CrossMarks
            A list of marks
        visible: bool
            If True, the marks are connected through visible lines

        Notes
        -----
        Different from the :meth:`connect_to_marks` method, this static
        function connects each of the marks to the others.
        """
        for mark in marks:
            mark.connect_to_marks([m for m in marks if m is not mark], visible)

    def connect(self):
        """Connect the marks matplotlib events"""
        fig = self.fig
        self.cidpress = fig.canvas.mpl_connect('button_press_event',
                                               self.on_press)
        self.cidrelease = fig.canvas.mpl_connect('button_release_event',
                                                 self.on_release)
        self.cidmotion = fig.canvas.mpl_connect('motion_notify_event',
                                                self.on_motion)

    def is_selected_by(self, event, buttons=[1]):
        """Test if the given `event` selects the mark

        Parameters
        ----------
        event: matplotlib.backend_bases.MouseEvent
            The matplotlib event
        button: list of int
            Possible buttons to select this mark

        Returns
        -------
        bool
            True, if it is selected"""
        return not (self.lock is not None or event.inaxes != self.ax
                    or event.button not in buttons
                    or self.fig.canvas.manager.toolbar.mode != ''
                    or not self.contains(event))

    def set_current_point(self, x, y, nearest=False):
        """Set the current point that is selected

        Parameters
        ----------
        x: int
            The index of the x-value in the :attr:`xa` attribute
        y: int
            The index of the y-value in the :attr:`ya` attribute
        nearest: bool
            If not None, `x` and `y` are interpreted as x- and y-values and
            we select the closest one
        """
        if nearest:
            x = np.abs(self.xa - x).argmin()
            y = np.abs(self.ya - y).argmin()
        self._i_vline = x
        self._i_hline = y

    def on_press(self, event, force=False, connected=True):
        """Select the mark

        Parameters
        ----------
        event: matplotlib.backend_bases.MouseEvent
            The mouseevent that selects the mark
        force: bool
            If True, the mark is selected although it does not contain
            the `event`
        connected: bool
            If True, connected marks that should maintain a constant x- and
            y-distance are selected, too"""
        if not force and not self.is_selected_by(event):
            return
        self.set_current_point(event.xdata, event.ydata, True)
        # use only the upper most CrossMarks
        if self._lock_mark and connected:
            CrossMarks.lock = self
        if self._animated:
            self.hline.set_animated(True)
            self.vline.set_animated(True)
            self.background = self.fig.canvas.copy_from_bbox(self.ax.bbox)
        self.hline.update(self._select_props)
        self.vline.update(self._select_props)

        # toggle line connections
        artist_props = self._select_props.copy()
        for a in chain(self.line_connections, self.other_connections):
            a.update(artist_props)

        # toggle connected artists
        artist_props['visible'] = (self.show_connected_artists
                                   and artist_props.get('visible', True))
        for a in self.connected_artists:
            a.update(artist_props)
        self.press = self.pos[0], self.pos[1], event.xdata, event.ydata
        # select the connected marks that should maintain the distance
        if connected:
            for m in set(
                    chain(self._constant_dist_y_marks,
                          self._constant_dist_x_marks)):
                m._i_vline = self._i_vline
                m._i_hline = self._i_hline
                event.xdata, event.ydata = m.pos
                m.on_press(event, True, False)
            event.xdata, event.ydata = self.press[2:]

        for l in chain(self.other_connections, self.line_connections,
                       self.connected_artists):
            self.ax.draw_artist(l)

        self.ax.draw_artist(self.hline)
        self.ax.draw_artist(self.vline)
        if self._animated:
            self.fig.canvas.blit(self.ax.bbox)

    def contains(self, event):
        """Test if the mark is selected by the given `event`

        Parameters
        ----------
        event: ButtonPressEvent
            The ButtonPressEvent that has been triggered"""
        contains = None
        if 'h' in self.selectable:
            contains = any(l.contains(event)[0] for l in self.hlines)
        if not contains and 'v' in self.selectable:
            contains = any(l.contains(event)[0] for l in self.vlines)
        return contains

    def on_motion(self, event, force=False, move_connected=True, restore=True):
        """Move the lines of this mark

        Parameters
        ----------
        event: matplotlib.backend_bases.MouseEvent
            The mouseevent that moves the mark
        force: bool
            If True, the mark is moved although it does not contain
            the `event`
        move_connected: bool
            If True, connected marks that should maintain a constant x- and
            y-distance are moved, too
        restore: bool
            If True, the axes background is restored"""
        if self.press is None or (not force and self._lock_mark
                                  and self.lock is not self):
            return
        if not force and event.inaxes != self.ax:
            return
        x0, y0, xpress, ypress = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        canvas = self.fig.canvas

        if dy and 'h' in self.draggable:
            y1 = y0 + dy
            one_percent = np.abs(0.01 * np.diff(self.ax.get_ylim())[0])
            for mark in filter(lambda m: m.ax is self.ax, self.other_marks):
                if np.abs(mark.pos[1] - y1) < one_percent:
                    y1 = mark.pos[1]
                    break
            if self.idx_v is not None:
                y1 = self.idx_v[self.idx_v.get_loc(y1, method='nearest')]
            self.hline.set_ydata([y1] * len(self.hline.get_ydata()))
            self.y = y1
            # first we move the horizontal line that is associated with this
            # mark
            ydata = self.vline.get_ydata()[:]
            ydata[self._i_hline + 1] = y1
            for l in self.vlines:
                l.set_ydata(ydata)
            # now we move all connections that are connected to this horizontal
            # layer
            for l in chain.from_iterable(
                    self._all_line_connections[self._i_hline]):
                l.set_ydata([y1, l.get_ydata()[1]])
            for l in chain.from_iterable(
                    self._all_other_connections[self._i_hline]):
                l.set_ydata([l.get_ydata()[0], y1])
        if dx and 'v' in self.draggable:
            x1 = x0 + dx
            one_percent = np.abs(0.01 * np.diff(self.ax.get_xlim())[0])
            for mark in filter(lambda m: m.ax is self.ax, self.other_marks):
                if np.abs(mark.pos[0] - x1) < one_percent:
                    x1 = mark.pos[0]
                    break
            if self.idx_h is not None:
                x1 = self.idx_h[self.idx_h.get_loc(x1, method='nearest')]
            self.vline.set_xdata([x1] * len(self.vline.get_xdata()))
            self.x = x1
            # first we move the vertical line that is associated with this mark
            xdata = self.hline.get_xdata()[:]
            xdata[self._i_vline + 1] = x1
            for l in self.hlines:
                l.set_xdata(xdata)
            # now we move all connections that are connected to this vertical
            # layer
            for l in chain.from_iterable(l[self._i_vline]
                                         for l in self._all_line_connections):
                l.set_xdata([x1, l.get_xdata()[1]])
            for l in chain.from_iterable(l[self._i_vline]
                                         for l in self._all_other_connections):
                l.set_xdata([l.get_xdata()[0], x1])
        if restore and self._animated:
            canvas.restore_region(self.background)
        for l in chain(self.other_connections, self.line_connections,
                       self.connected_artists):
            self.ax.draw_artist(l)
        if restore and self._animated:
            self.ax.draw_artist(self.hline)
            self.ax.draw_artist(self.vline)
            canvas.blit(self.ax.bbox)
        else:
            self.ax.figure.canvas.draw_idle()

        # move the marks that should maintain a constant distance
        orig_xy = (event.xdata, event.ydata)
        if move_connected and dy and 'h' in self.draggable:
            for dist, m in zip(self._constant_dist_y,
                               self._constant_dist_y_marks):
                event.xdata = m.press[-2]
                event.ydata = y1 + dist
                m.on_motion(event, True, False, m.ax is not self.ax)
        if move_connected and dx and 'v' in self.draggable:
            for dist, m in zip(self._constant_dist_x,
                               self._constant_dist_x_marks):
                event.xdata = x1 + dist
                event.ydata = m.press[-1]
                m.on_motion(event, True, False, m.ax is not self.ax)
        event.xdata, event.ydata = orig_xy

    def set_connected_artists_visible(self, visible):
        """Set the visibility of the connected artists

        Parameters
        ----------
        visible: bool
            True, show the connected artists, else don't"""
        self.show_connected_artists = visible
        for a in self.connected_artists:
            a.set_visible(visible)
        for d in self._connected_artists_props:
            d['visible'] = visible

    def on_release(self,
                   event,
                   force=False,
                   connected=True,
                   draw=True,
                   *args,
                   **kwargs):
        """Release the mark and unselect it

        Parameters
        ----------
        event: matplotlib.backend_bases.MouseEvent
            The mouseevent that releases the mark
        force: bool
            If True, the mark is released although it does not contain
            the `event`
        connected: bool
            If True, connected marks that should maintain a constant x- and
            y-distance are released, too
        draw: bool
            If True, the figure is drawn
        ``*args, **kwargs``
            Any other parameter that is passed to the connected lines"""
        if (not force and self._lock_mark and self.lock is not self
                or self.press is None):
            return
        self.hline.update(self._unselect_props)
        self.vline.update(self._unselect_props)
        for d, a in zip_longest(self._connected_artists_props,
                                self.connected_artists,
                                fillvalue=self._unselect_props):
            a.update(d)
        for l in chain(self.line_connections, self.other_connections):
            l.update(self._unselect_props)
        if self.auto_hide:
            self.hline.set_lw(0)
            self.vline.set_lw(0)
            for l in chain(self.line_connections, self.other_connections):
                l.set_lw(0)

        self.xa[self._i_vline] = self.pos[0]
        self.ya[self._i_hline] = self.pos[1]

        pos0 = self.press[:2]

        self.press = None
        if self._animated:
            self.hline.set_animated(False)
            self.vline.set_animated(False)
            self.background = None
        if connected:
            for m in set(
                    chain(self._constant_dist_y_marks,
                          self._constant_dist_x_marks)):
                m.on_release(event, True, False, m.fig is not self.fig, *args,
                             **kwargs)
        if self._lock_mark and self.lock is self:
            CrossMarks.lock = None
        if draw:
            self.fig.canvas.draw_idle()
        self.moved.emit(pos0, self)

    def disconnect(self):
        """Disconnect all the stored connection ids"""
        fig = self.fig
        fig.canvas.mpl_disconnect(self.cidpress)
        fig.canvas.mpl_disconnect(self.cidrelease)
        fig.canvas.mpl_disconnect(self.cidmotion)

    def remove(self, artists=True):
        """Remove all lines and disconnect the mark

        Parameters
        ----------
        artists: bool
            If True, the :attr:`connected_artists` list is cleared and the
            corresponding artists are removed as well"""
        for l in chain(
                self.hlines, self.vlines,
                self.connected_artists if artists else [],
                chain.from_iterable(
                    chain.from_iterable(self._all_other_connections)),
                chain.from_iterable(
                    chain.from_iterable(self._all_line_connections))):
            try:
                l.remove()
            except ValueError:
                pass
        self.hlines.clear()
        self.vlines.clear()
        if artists:
            self.connected_artists.clear()

        # Remove the line connections
        visible_connections = [
            m for m, v in zip(self.other_marks, self._connection_visible) if v
        ]
        for m in visible_connections:
            for l in chain.from_iterable(
                    chain.from_iterable(self._all_line_connections)):
                for i, j in product(range(len(m.ya)), range(len(m.xa))):
                    if l in m._all_other_connections[i][j]:
                        m._all_other_connections[i][j].remove(l)
                        break
            for l in chain.from_iterable(
                    chain.from_iterable(self._all_other_connections)):
                for i, j in product(range(len(m.ya)), range(len(m.xa))):
                    if l in m._all_line_connections[i][j]:
                        m._all_line_connections[i][j].remove(l)
                        break

        self._all_line_connections = [[[] for _ in range(len(self.xa))]
                                      for _ in range(len(self.ya))]
        self._all_other_connections = [[[] for _ in range(len(self.xa))]
                                       for _ in range(len(self.ya))]

        self.disconnect()

    def set_pos(self, pos):
        """Move the point(s) to another position

        Parameters
        ----------
        pos: tuple of 2 arrays
            The positions of the crosses. The first item marks the
            x-coordinates of the points, the second the y-coordinates"""
        self.remove(artists=False)
        self.xa[:] = pos[0]
        self.ya[:] = pos[1]
        self.draw_lines(**self._line_kwargs)
        self.connect()
        visible_connections = [
            m for m, v in zip(self.other_marks, self._connection_visible) if v
        ]
        if visible_connections:
            self.connect_to_marks(visible_connections, True, append=False)
Beispiel #6
0
class ImageRescaler(StraditizerControlBase, QPushButton):
    """A button to rescale the straditize image"""

    rescaling = _temp_bool_prop(
        'rescaling', "Boolean that is true if one of the axes is rescaling")

    #: A :class:`matplotlib.widgets.Slider` for specifying the size of the
    #: rescaled image
    slider = None

    #: The matplotlib image for the rescaled diagram
    im_rescale = None

    #: The matplotlib image for the original diagram
    im_orig = None

    #: The matplotlib axes for the :attr:`im_orig`
    ax_orig = None

    #: The matplotlib axes for the :attr:`im_rescale`
    ax_rescale = None

    #: The matplotlib figure for the rescaling
    fig = None

    def __init__(self, straditizer_widgets, item, *args, **kwargs):
        super(ImageRescaler, self).__init__('Rescale image')

        self.init_straditizercontrol(straditizer_widgets, item)

        self.widgets2disable = [self]

        self.clicked.connect(self.start_rescaling)

    def start_rescaling(self):
        """Create the rescaling figure"""
        self._create_rescale_figure()

    def _create_rescale_figure(self):
        import matplotlib.pyplot as plt
        from matplotlib.widgets import Slider
        import matplotlib.colorbar as mcbar
        self.fig, (self.ax_orig, self.ax_rescale) = plt.subplots(
            2, 1, figsize=(8, 12), gridspec_kw=dict(top=1.0, bottom=0.0))
        slider_ax, kw = mcbar.make_axes_gridspec(
            self.ax_rescale, orientation='horizontal', location='bottom')
        slider_ax.set_aspect('auto')
        slider_ax._hold = True
        self.slider = Slider(slider_ax, 'Fraction', 0, 100, valfmt='%1.3g %%')
        self.slider.set_val(100)
        self.slider.on_changed(self.rescale_plot)

        self.im_orig = self.ax_orig.imshow(self.straditizer.image)
        self.im_rescale = self.ax_rescale.imshow(self.straditizer.image)

        # connect limits
        self.ax_orig.callbacks.connect('xlim_changed',
                                       self.adjust_rescaled_limits)
        self.ax_orig.callbacks.connect('ylim_changed',
                                       self.adjust_rescaled_limits)
        self.ax_rescale.callbacks.connect('xlim_changed',
                                          self.adjust_orig_limits)
        self.ax_rescale.callbacks.connect('ylim_changed',
                                          self.adjust_orig_limits)
        self.fig.canvas.mpl_connect('resize_event', self.equalize_axes)

        self.connect2apply(self.rescale, self.close_figs)
        self.connect2cancel(self.close_figs)
        self.raise_figure()
        self.equalize_axes()

    def resize_stradi_image(self, percentage):
        """Resize the straditizer image

        Parameters
        ----------
        percentage: float
            A float between 0 and 100 specifying the target size of the
            :attr:`straditize.straditizer.Straditizer.image`

        Returns
        -------
        PIL.Image.Image
            The resized :attr:`~straditize.straditizer.Straditizer.image`
            of the current straditizer"""
        w, h = self.straditizer.image.size
        new_size = (int(round(w * percentage / 100.)),
                    int(round(h * percentage / 100.)))
        return self.straditizer.image.resize(new_size)

    def raise_figure(self):
        """Raise the figure for rescaling"""
        from psyplot_gui.main import mainwindow
        if mainwindow.figures:
            dock = self.fig.canvas.manager.window
            dock.widget().show_plugin()
            dock.raise_()

    def rescale_plot(self, percentage):
        """Replot :attr:`im_rescale` after adjustments of the :attr:`slider`"""
        self.im_rescale.remove()
        self.im_rescale = self.ax_rescale.imshow(
            self.resize_stradi_image(percentage))
        self.adjust_rescaled_limits()

    def adjust_rescaled_limits(self, *args, **kwargs):
        """Readjust :attr:`ax_rescale` after changes in :attr:`ax_orig`"""
        if self.rescaling:
            return
        with self.rescaling:
            x0, x1 = self.ax_orig.get_xlim()
            y0, y1 = self.ax_orig.get_ylim()
            fraction = self.slider.val / 100.
            self.ax_rescale.set_xlim(x0 * fraction, x1 * fraction)
            self.ax_rescale.set_ylim(y0 * fraction, y1 * fraction)
            self.draw_figure()

    def adjust_orig_limits(self, *args, **kwargs):
        """Readjust :attr:`ax_orig` after changes in :attr:`ax_rescale`"""
        if self.rescaling:
            return
        with self.rescaling:
            x0, x1 = self.ax_rescale.get_xlim()
            y0, y1 = self.ax_rescale.get_ylim()
            fraction = self.slider.val / 100.
            self.ax_orig.set_xlim(x0 / fraction, x1 / fraction)
            self.ax_orig.set_ylim(y0 / fraction, y1 / fraction)
            self.draw_figure()

    def equalize_axes(self, event=None):
        """Set both axes to the same size"""
        rescale_pos = self.ax_rescale.get_position()
        self.ax_orig.set_position((
            rescale_pos.x0, 0.55, rescale_pos.width,
            rescale_pos.height))

    def draw_figure(self):
        self.fig.canvas.draw()

    def rescale(self, ask=None):
        """Rescale and start a new straditizer

        Parameters
        ----------
        ask: bool
            Whether to ask with a QMessageBox. If None, it defaults to the
            :attr:`straditize.widgets.StraditizerWidgers.always_yes`"""
        if ask is None:
            ask = not self.straditizer_widgets.always_yes
        answer = QMessageBox.Yes if not ask else QMessageBox.question(
            self, 'Restart project?',
            'This will close the straditizer and create new figures. '
            'Are you sure, you want to continue?')
        if answer == QMessageBox.Yes:
            image = self.resize_stradi_image(self.slider.val)
            attrs = self.straditizer.attrs
            self.straditizer_widgets.close_straditizer()
            self.straditizer_widgets.menu_actions.open_straditizer(
                image, attrs=attrs)

    def close_figs(self):
        """Close the :attr:`fig`"""
        import matplotlib.pyplot as plt
        plt.close(self.fig.number)
        del self.fig, self.ax_orig, self.ax_rescale, self.im_rescale, \
            self.im_orig, self.slider

    def should_be_enabled(self, w):
        return self.straditizer is not None
Beispiel #7
0
class FormatoptionWidget(QWidget, DockMixin):
    """
    Widget to update the formatoptions of the current project

    This widget, mainly made out of a combobox for the formatoption group,
    a combobox for the formatoption, and a text editor, is designed
    for updating the selected formatoptions for the current subproject.

    The widget is connected to the :attr:`psyplot.project.Project.oncpchange`
    signal and refills the comboboxes if the current subproject changes.

    The text editor either accepts python code that will be executed by the
    given `console`, or yaml code.
    """

    no_fmtos_update = _temp_bool_prop('no_fmtos_update',
                                      """update the fmto combo box or not""")

    #: The combobox for the formatoption groups
    group_combo = None

    #: The combobox for the formatoptions
    fmt_combo = None

    #: The help_explorer to display the documentation of the formatoptions
    help_explorer = None

    #: The formatoption specific widget that is loaded from the formatoption
    fmt_widget = None

    #: A line edit for updating the formatoptions
    line_edit = None

    #: A multiline text editor for updating the formatoptions
    text_edit = None

    #: A button to switch between :attr:`line_edit` and :attr:`text_edit`
    multiline_button = None

    @property
    def shell(self):
        """The shell to execute the update of the formatoptions in the current
        project"""
        return self.console.kernel_manager.kernel.shell

    def __init__(self, *args, **kwargs):
        """
        Parameters
        ----------
        help_explorer: psyplot_gui.help_explorer.HelpExplorer
            The help explorer to show the documentation of one formatoption
        console: psyplot_gui.console.ConsoleWidget
            The console that can be used to update the current subproject via::

                psy.gcp().update(**kwargs)

            where ``**kwargs`` is defined through the selected formatoption
            in the :attr:`fmt_combo` combobox and the value in the
            :attr:`line_edit` editor
        ``*args, **kwargs``
            Any other keyword for the QWidget class
        """
        help_explorer = kwargs.pop('help_explorer', None)
        console = kwargs.pop('console', None)
        super(FormatoptionWidget, self).__init__(*args, **kwargs)
        self.help_explorer = help_explorer
        self.console = console
        self.error_msg = PyErrorMessage(self)

        # ---------------------------------------------------------------------
        # -------------------------- Child widgets ----------------------------
        # ---------------------------------------------------------------------
        self.group_combo = QComboBox(parent=self)
        self.fmt_combo = QComboBox(parent=self)
        self.line_edit = QLineEdit(parent=self)
        self.text_edit = QTextEdit(parent=self)
        self.run_button = QToolButton(parent=self)

        # completer for the fmto widget
        self.fmt_combo.setEditable(True)
        self.fmt_combo.setInsertPolicy(QComboBox.NoInsert)
        self.fmto_completer = completer = QCompleter(
            ['time', 'lat', 'lon', 'lev'])
        completer.setCompletionMode(QCompleter.PopupCompletion)
        completer.activated[str].connect(self.set_fmto)
        if with_qt5:
            completer.setFilterMode(Qt.MatchContains)
        completer.setModel(QStandardItemModel())
        self.fmt_combo.setCompleter(completer)

        self.dim_widget = DimensionsWidget(parent=self)
        self.dim_widget.setVisible(False)

        self.multiline_button = QPushButton('Multiline', parent=self)
        self.multiline_button.setCheckable(True)

        self.yaml_cb = QCheckBox('Yaml syntax')
        self.yaml_cb.setChecked(True)

        self.keys_button = QPushButton('Keys', parent=self)
        self.summaries_button = QPushButton('Summaries', parent=self)
        self.docs_button = QPushButton('Docs', parent=self)

        self.grouped_cb = QCheckBox('grouped', parent=self)
        self.all_groups_cb = QCheckBox('all groups', parent=self)
        self.include_links_cb = QCheckBox('include links', parent=self)

        self.text_edit.setVisible(False)

        # ---------------------------------------------------------------------
        # -------------------------- Descriptions -----------------------------
        # ---------------------------------------------------------------------

        self.group_combo.setToolTip('Select the formatoption group')
        self.fmt_combo.setToolTip('Select the formatoption to update')
        self.line_edit.setToolTip(
            'Insert the value which what you want to update the selected '
            'formatoption and hit right button. The code is executed in the '
            'main console.')
        self.yaml_cb.setToolTip(
            "Use the yaml syntax for the values inserted in the above cell. "
            "Otherwise the content there is evaluated as a python expression "
            "in the terminal")
        self.text_edit.setToolTip(self.line_edit.toolTip())
        self.run_button.setIcon(QIcon(get_icon('run_arrow.png')))
        self.run_button.setToolTip('Update the selected formatoption')
        self.multiline_button.setToolTip(
            'Allow linebreaks in the text editor line above.')
        self.keys_button.setToolTip(
            'Show the formatoption keys in this group (or in all '
            'groups) in the help explorer')
        self.summaries_button.setToolTip(
            'Show the formatoption summaries in this group (or in all '
            'groups) in the help explorer')
        self.docs_button.setToolTip(
            'Show the formatoption documentations in this group (or in all '
            'groups) in the help explorer')
        self.grouped_cb.setToolTip(
            'Group the formatoptions before displaying them in the help '
            'explorer')
        self.all_groups_cb.setToolTip('Use all groups when displaying the '
                                      'keys, docs or summaries')
        self.include_links_cb.setToolTip(
            'Include links to remote documentations when showing the '
            'keys, docs and summaries in the help explorer (requires '
            'intersphinx)')

        # ---------------------------------------------------------------------
        # -------------------------- Connections ------------------------------
        # ---------------------------------------------------------------------
        self.group_combo.currentIndexChanged[int].connect(self.fill_fmt_combo)
        self.fmt_combo.currentIndexChanged[int].connect(self.show_fmt_info)
        self.fmt_combo.currentIndexChanged[int].connect(self.load_fmt_widget)
        self.fmt_combo.currentIndexChanged[int].connect(
            self.set_current_fmt_value)
        self.run_button.clicked.connect(self.run_code)
        self.line_edit.returnPressed.connect(self.run_button.click)
        self.multiline_button.clicked.connect(self.toggle_line_edit)
        self.keys_button.clicked.connect(
            partial(self.show_all_fmt_info, 'keys'))
        self.summaries_button.clicked.connect(
            partial(self.show_all_fmt_info, 'summaries'))
        self.docs_button.clicked.connect(
            partial(self.show_all_fmt_info, 'docs'))

        # ---------------------------------------------------------------------
        # ------------------------------ Layouts ------------------------------
        # ---------------------------------------------------------------------
        self.combos = QHBoxLayout()
        self.combos.addWidget(self.group_combo)
        self.combos.addWidget(self.fmt_combo)

        self.execs = QHBoxLayout()
        self.execs.addWidget(self.line_edit)
        self.execs.addWidget(self.text_edit)
        self.execs.addWidget(self.run_button)

        self.info_box = QHBoxLayout()
        self.info_box.addWidget(self.multiline_button)
        self.info_box.addWidget(self.yaml_cb)
        self.info_box.addStretch(0)
        for w in [
                self.keys_button, self.summaries_button, self.docs_button,
                self.all_groups_cb, self.grouped_cb, self.include_links_cb
        ]:
            self.info_box.addWidget(w)

        self.vbox = QVBoxLayout()
        self.vbox.addLayout(self.combos)
        self.vbox.addWidget(self.dim_widget)
        self.vbox.addLayout(self.execs)
        self.vbox.addLayout(self.info_box)

        self.vbox.setSpacing(0)

        self.setLayout(self.vbox)

        # fill with content
        self.fill_combos_from_project(psy.gcp())
        psy.Project.oncpchange.connect(self.fill_combos_from_project)
        rcParams.connect('fmt.sort_by_key', self.refill_from_rc)

    def refill_from_rc(self, sort_by_key):
        from psyplot.project import gcp
        self.fill_combos_from_project(gcp())

    def fill_combos_from_project(self, project):
        """Fill :attr:`group_combo` and :attr:`fmt_combo` from a project

        Parameters
        ----------
        project: psyplot.project.Project
            The project to use"""
        if rcParams['fmt.sort_by_key']:

            def sorter(fmto):
                return fmto.key
        else:
            sorter = self.get_name

        current_text = self.group_combo.currentText()
        with self.no_fmtos_update:
            self.group_combo.clear()
            if project is None or project.is_main or not len(project):
                self.fmt_combo.clear()
                self.groups = []
                self.fmtos = []
                self.line_edit.setEnabled(False)
                return
            self.line_edit.setEnabled(True)
            # get dimensions
            it_vars = chain.from_iterable(arr.psy.iter_base_variables
                                          for arr in project.arrays)
            dims = next(it_vars).dims
            sdims = set(dims)
            for var in it_vars:
                sdims.intersection_update(var.dims)
            coords = [d for d in dims if d in sdims]
            coords_name = [COORDSGROUP] if coords else []
            coords_verbose = ['Dimensions'] if coords else []
            coords = [coords] if coords else []

            if len(project.plotters):
                # get formatoptions and group them alphabetically
                grouped_fmts = defaultdict(list)
                for fmto in project._fmtos:
                    grouped_fmts[fmto.group].append(fmto)
                for val in six.itervalues(grouped_fmts):
                    val.sort(key=sorter)
                grouped_fmts = OrderedDict(
                    sorted(six.iteritems(grouped_fmts),
                           key=lambda t: psyp.groups.get(t[0], t[0])))
                fmt_groups = list(grouped_fmts.keys())
                # save original names
                self.groups = coords_name + [ALLGROUP] + fmt_groups
                # save verbose group names (which are used in the combo box)
                self.groupnames = (
                    coords_verbose + ['All formatoptions'] +
                    list(map(lambda s: psyp.groups.get(s, s), fmt_groups)))
                # save formatoptions
                fmtos = list(grouped_fmts.values())
                self.fmtos = coords + [sorted(chain(*fmtos), key=sorter)
                                       ] + fmtos
            else:
                self.groups = coords_name
                self.groupnames = coords_verbose
                self.fmtos = coords
            self.group_combo.addItems(self.groupnames)
            ind = self.group_combo.findText(current_text)
            self.group_combo.setCurrentIndex(ind if ind >= 0 else 0)
        self.fill_fmt_combo(self.group_combo.currentIndex())

    def get_name(self, fmto):
        """Get the name of a :class:`psyplot.plotter.Formatoption` instance"""
        if isinstance(fmto, six.string_types):
            return fmto
        return '%s (%s)' % (fmto.name, fmto.key) if fmto.name else fmto.key

    @property
    def fmto(self):
        return self.fmtos[self.group_combo.currentIndex()][
            self.fmt_combo.currentIndex()]

    @fmto.setter
    def fmto(self, value):
        name = self.get_name(value)
        for i, fmtos in enumerate(self.fmtos):
            if i == 1:  # all formatoptions
                continue
            if name in map(self.get_name, fmtos):
                with self.no_fmtos_update:
                    self.group_combo.setCurrentIndex(i)
                self.fill_fmt_combo(i, name)
                return

    def toggle_line_edit(self):
        """Switch between the :attr:`line_edit` and :attr:`text_edit`

        This method is called when the :attr:`multiline_button` is clicked
        and switches between the single line :attr:``line_edit` and the
        multiline :attr:`text_edit`
        """
        # switch to multiline text edit
        if (self.multiline_button.isChecked()
                and not self.text_edit.isVisible()):
            self.line_edit.setVisible(False)
            self.text_edit.setVisible(True)
            self.text_edit.setPlainText(self.line_edit.text())
        elif (not self.multiline_button.isChecked()
              and not self.line_edit.isVisible()):
            self.line_edit.setVisible(True)
            self.text_edit.setVisible(False)
            self.line_edit.setText(self.text_edit.toPlainText())

    def fill_fmt_combo(self, i, current_text=None):
        """Fill the :attr:`fmt_combo` combobox based on the current group name
        """
        if not self.no_fmtos_update:
            with self.no_fmtos_update:
                if current_text is None:
                    current_text = self.fmt_combo.currentText()
                self.fmt_combo.clear()
                self.fmt_combo.addItems(list(map(self.get_name,
                                                 self.fmtos[i])))
                ind = self.fmt_combo.findText(current_text)
                self.fmt_combo.setCurrentIndex(ind if ind >= 0 else 0)
                # update completer model
                self.setup_fmt_completion_model()
            idx = self.fmt_combo.currentIndex()
            self.show_fmt_info(idx)
            self.load_fmt_widget(idx)
            self.set_current_fmt_value(idx)

    def set_fmto(self, name):
        self.fmto = name

    def setup_fmt_completion_model(self):
        fmtos = list(
            unique_everseen(map(self.get_name,
                                chain.from_iterable(self.fmtos))))
        model = self.fmto_completer.model()
        model.setRowCount(len(fmtos))
        for i, name in enumerate(fmtos):
            model.setItem(i, QStandardItem(name))

    def load_fmt_widget(self, i):
        """Load the formatoption specific widget

        This method loads the formatoption specific widget from the
        :meth:`psyplot.plotter.Formatoption.get_fmt_widget` method and
        displays it above the :attr:`line_edit`

        Parameters
        ----------
        i: int
            The index of the current formatoption"""
        self.remove_fmt_widget()
        group_ind = self.group_combo.currentIndex()
        if not self.no_fmtos_update:
            from psyplot.project import gcp
            if self.groups[group_ind] == COORDSGROUP:
                dim = self.fmtos[group_ind][i]
                self.fmt_widget = self.dim_widget
                self.dim_widget.set_dim(dim)
                self.dim_widget.set_single_selection(dim not in gcp()[0].dims)
                self.dim_widget.setVisible(True)
            else:
                fmto = self.fmtos[group_ind][i]
                self.fmt_widget = fmto.get_fmt_widget(self, gcp())
                if self.fmt_widget is not None:
                    self.vbox.insertWidget(2, self.fmt_widget)

    def reset_fmt_widget(self):
        idx = self.fmt_combo.currentIndex()
        self.load_fmt_widget(idx)
        self.set_current_fmt_value(idx)

    def remove_fmt_widget(self):
        if self.fmt_widget is not None:
            self.fmt_widget.hide()
            if self.fmt_widget is self.dim_widget:
                self.fmt_widget.reset_combobox()
            else:
                self.vbox.removeWidget(self.fmt_widget)
                self.fmt_widget.close()
            del self.fmt_widget

    def set_current_fmt_value(self, i):
        """Add the value of the current formatoption to the line text"""
        group_ind = self.group_combo.currentIndex()
        if not self.no_fmtos_update:
            if self.groups[group_ind] == COORDSGROUP:
                from psyplot.project import gcp
                dim = self.fmtos[group_ind][i]
                self.set_obj(gcp().arrays[0].psy.idims[dim])
            else:
                fmto = self.fmtos[group_ind][i]
                self.set_obj(fmto.value)

    def show_fmt_info(self, i):
        """Show the documentation of the formatoption in the help explorer
        """
        group_ind = self.group_combo.currentIndex()
        if (not self.no_fmtos_update
                and self.groups[group_ind] != COORDSGROUP):
            fmto = self.fmtos[self.group_combo.currentIndex()][i]
            fmto.plotter.show_docs(
                fmto.key, include_links=self.include_links_cb.isChecked())

    def run_code(self):
        """Run the update of the project inside the :attr:`shell`"""
        if self.line_edit.isVisible():
            text = str(self.line_edit.text())
        else:
            text = str(self.text_edit.toPlainText())
        if not text or not self.fmtos:
            return
        group_ind = self.group_combo.currentIndex()
        if self.groups[group_ind] == COORDSGROUP:
            key = self.fmtos[group_ind][self.fmt_combo.currentIndex()]
            param = 'dims'
        else:
            key = self.fmtos[group_ind][self.fmt_combo.currentIndex()].key
            param = 'fmt'
        if self.yaml_cb.isChecked():
            import psyplot.project as psy
            psy.gcp().update(**{key: yaml.load(text, Loader=yaml.Loader)})
        else:
            code = "psy.gcp().update(%s={'%s': %s})" % (param, key, text)
            if ExecutionInfo is not None:
                info = ExecutionInfo(raw_cell=code,
                                     store_history=False,
                                     silent=True,
                                     shell_futures=False)
                e = ExecutionResult(info)
            else:
                e = ExecutionResult()
            self.console.run_command_in_shell(code, e)
            try:
                e.raise_error()
            except Exception:  # reset the console and clear the error message
                raise
            finally:
                self.console.reset()

    def get_text(self):
        """Get the current update text"""
        if self.line_edit.isVisible():
            return self.line_edit.text()
        else:
            return self.text_edit.toPlainText()

    def get_obj(self):
        """Get the current update text"""
        if self.line_edit.isVisible():
            txt = self.line_edit.text()
        else:
            txt = self.text_edit.toPlainText()
        try:
            obj = yaml.load(txt, Loader=yaml.Loader)
        except Exception:
            self.error_msg.showTraceback("Could not load %s" % txt)
        else:
            return obj

    def insert_obj(self, obj):
        """Add a string to the formatoption widget"""
        current = self.get_text()
        use_yaml = self.yaml_cb.isChecked()
        use_line_edit = self.line_edit.isVisible()
        # strings are treated separately such that we consider quotation marks
        # at the borders
        if isstring(obj) and current:
            if use_line_edit:
                pos = self.line_edit.cursorPosition()
            else:
                pos = self.text_edit.textCursor().position()
            if pos not in [0, len(current)]:
                s = obj
            else:
                if current[0] in ['"', "'"]:
                    current = current[1:-1]
                self.clear_text()
                if pos == 0:
                    s = '"' + obj + current + '"'
                else:
                    s = '"' + current + obj + '"'
                current = ''
        elif isstring(obj):  # add quotation marks
            s = '"' + obj + '"'
        elif not use_yaml:
            s = repr(obj)
        else:
            s = yaml.dump(obj, default_flow_style=True).strip()
            if s.endswith('\n...'):
                s = s[:-4]
        if use_line_edit:
            self.line_edit.insert(s)
        else:
            self.text_edit.insertPlainText(s)

    def clear_text(self):
        if self.line_edit.isVisible():
            self.line_edit.clear()
        else:
            self.text_edit.clear()

    def set_obj(self, obj):
        self.clear_text()
        self.insert_obj(obj)

    def show_all_fmt_info(self, what):
        """Show the keys, summaries or docs of the formatoptions

        Calling this function let's the help browser show the documentation
        etc. of all docs or only the selected group determined by the state of
        the :attr:`grouped_cb` and :attr:`all_groups_cb` checkboxes

        Parameters
        ----------
        what: {'keys', 'summaries', 'docs'}
            Determines what to show"""
        if not self.fmtos:
            return
        if (self.all_groups_cb.isChecked()
                or self.group_combo.currentIndex() < 2):
            fmtos = list(
                chain.from_iterable(
                    fmto_group for i, fmto_group in enumerate(self.fmtos)
                    if self.groups[i] not in [ALLGROUP, COORDSGROUP]))
        else:
            fmtos = self.fmtos[self.group_combo.currentIndex()]
        plotter = fmtos[0].plotter
        getattr(plotter, 'show_' +
                what)([fmto.key for fmto in fmtos],
                      grouped=self.grouped_cb.isChecked(),
                      include_links=self.include_links_cb.isChecked())