Example #1
0
    def __init__(self, parent, dim=None):
        super(DimensionsWidget, self).__init__(parent)
        self.coord_combo = CoordComboBox(self.get_ds, dim)
        self.cb_use_coord = QCheckBox('show coordinates')
        self.cb_close_popups = QCheckBox('close dropdowns')
        self.cb_close_popups.setChecked(True)
        self.toggle_close_popup()
        self._single_selection = False

        self.dim = dim
        hbox = QHBoxLayout()
        hbox.addWidget(self.cb_close_popups)
        hbox.addWidget(self.cb_use_coord)
        hbox.addWidget(self.coord_combo)
        self.setLayout(hbox)
        self.cb_use_coord.stateChanged.connect(self.reset_combobox)
        self.cb_close_popups.stateChanged.connect(self.toggle_close_popup)
        self.coord_combo.leftclick.connect(self.insert_from_combo)
Example #2
0
class ResultsPlot(StraditizerControlBase):
    """A widget for plotting the final results

    This widgets contains a QPushButton :attr:`btn_plot` to plot the results
    using the :meth:`straditize.binary.DataReader.plot_results` method"""

    #: The QPushButton to call the :meth:`plot_results` method
    btn_plot = None

    #: A QCheckBox whether x- and y-axis should be translated from pixel to
    #: data units
    cb_transformed = None

    #: A QCheckBox whether the samples or the full digitized data shall be
    #: plotted
    cb_final = None

    def __init__(self, straditizer_widgets):
        self.init_straditizercontrol(straditizer_widgets)

        self.btn_plot = QPushButton("Plot results")

        self.cb_final = QCheckBox("Samples")
        self.cb_final.setToolTip(
            "Create the diagram based on the samples only, not on the full "
            "digized data")
        self.cb_final.setChecked(True)
        self.cb_final.setEnabled(False)

        self.cb_transformed = QCheckBox("Translated")
        self.cb_transformed.setToolTip("Use the x-axis and y-axis translation")
        self.cb_transformed.setChecked(True)
        self.cb_transformed.setEnabled(False)

        self.btn_plot.clicked.connect(self.plot_results)

    def setup_children(self, item):
        tree = self.straditizer_widgets.tree
        tree.setItemWidget(item, 0, self.btn_plot)

        child = QTreeWidgetItem(0)
        item.addChild(child)
        widget = QWidget()
        vbox = QVBoxLayout()
        vbox.addWidget(self.cb_final)
        vbox.addWidget(self.cb_transformed)
        widget.setLayout(vbox)

        tree.setItemWidget(child, 0, widget)

    def refresh(self):
        try:
            self.straditizer.yaxis_px
            self.straditizer.data_reader.xaxis_px
        except (AttributeError, ValueError):
            self.cb_transformed.setEnabled(False)
        else:
            self.cb_transformed.setEnabled(True)
        try:
            assert (self.straditizer.data_reader._sample_locs is not None
                    and len(self.straditizer.data_reader._sample_locs))
        except (AssertionError, AttributeError):
            self.cb_final.setEnabled(False)
        else:
            self.cb_final.setEnabled(True)
        try:
            self.btn_plot.setEnabled(
                self.straditizer.data_reader._full_df is not None)
        except AttributeError:
            self.btn_plot.setEnabled(False)

    def plot_results(self):
        """Plot the results

        What is plotted depends on the :attr:`cb_transformed` and the
        :attr:`cb_final`

        :attr:`cb_transformed` and :attr:`cb_final` are checked
            Plot the :attr:`straditize.straditizer.Straditizer.final_df`
        :attr:`cb_transformed` is checked but not :attr:`cb_final`
            Plot the :attr:`straditize.straditizer.Straditizer.full_df`
        :attr:`cb_transformed` is not checked but :attr:`cb_final`
            Plot the :attr:`straditize.binary.DataReader.sample_locs`
        :attr:`cb_transformed` and :attr:`cb_final` are both not checked
            Plot the :attr:`straditize.binary.DataReader.full_df`"""
        transformed = self.cb_transformed.isEnabled() and \
            self.cb_transformed.isChecked()
        if self.cb_final.isEnabled() and self.cb_final.isChecked():
            df = self.straditizer.final_df if transformed else \
                self.straditizer.data_reader.sample_locs
        else:
            df = self.straditizer.full_df if transformed else \
                self.straditizer.data_reader._full_df
        return self.straditizer.data_reader.plot_results(
            df, transformed=transformed)
Example #3
0
    def add_item(self,
                 what,
                 get_artists,
                 plot_func=None,
                 remove_func=None,
                 can_be_plotted=None):
        """Add a plot object to the table

        Parameters
        ----------
        what: str
            The description of the plot object
        get_artists: function
            A function that takes no arguments and returns the artists
        plot_func: function, optional
            A function that takes no arguments and makes the plot.
        remove_func: function, optional
            A function that takes no arguments and removes the plot.
        can_be_plotted: function, optional
            A function that takes no argument and returns True if the plot can
            be made.
        """
        def hide_or_show(checked):
            checked = checked is True or checked == Qt.Checked
            artist = None
            for artist in get_artists():
                artist.set_visible(checked)
            if artist is not None:
                self.draw_figs(get_artists())

        def trigger_plot_btn():
            a = next(iter(get_artists()), None)
            if a is None:
                if can_be_plotted is None or can_be_plotted():
                    plot_func()
                    cb.setChecked(True)
                    btn.setIcon(QIcon(get_icon('invalid.png')))
                    btn.setToolTip('Remove ' + what)
                    self.draw_figs(get_artists())
                    cb.setEnabled(True)
            else:
                fig = a.axes.figure
                figs = {a.axes.figure for a in get_artists()}
                remove_func()
                btn.setIcon(QIcon(get_icon('valid.png')))
                btn.setToolTip('Show ' + what)
                for fig in figs:
                    fig.canvas.draw_idle()
                cb.setEnabled(False)

        self.get_artists_funcs[what] = get_artists

        a = next(iter(get_artists()), None)

        cb = QCheckBox()
        cb.label = what
        self.hide_funcs[what] = hide_or_show
        cb.setChecked(
            Qt.Checked if a is not None and a.get_visible() else Qt.Unchecked)
        cb.stateChanged.connect(hide_or_show)

        row = self.rowCount()
        self.setRowCount(row + 1)
        self.setVerticalHeaderLabels(list(self.get_artists_funcs))

        self.setCellWidget(row, 0, cb)

        if plot_func is not None:
            btn = QToolButton()
            btn.setIcon(
                QIcon(get_icon(('in' if a is None else '') + 'valid.png')))
            btn.clicked.connect(trigger_plot_btn)
            btn.setEnabled(can_be_plotted is None or can_be_plotted())
            btn.setToolTip(('Remove ' if a else 'Show ') + what)
            self.can_be_plotted_funcs[what] = can_be_plotted
            btn.label = what

            self.setCellWidget(row, 1, btn)
Example #4
0
    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)
Example #5
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())
Example #6
0
class SelectionToolbar(QToolBar, StraditizerControlBase):
    """A toolbar for selecting features in the straditizer and data image

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

    _idPress = None

    _idRelease = None

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

    set_cursor_id = None

    reset_cursor_id = None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    _pattern_selection = None

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

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

        select_group = QActionGroup(self)

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

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

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

        a.setMenu(select_menu)

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

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

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

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

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

        a.setMenu(tool_menu)

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

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

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

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

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

        self.set_label_wand_mode()

        self.addSeparator()
        type_group = QActionGroup(self)

        self._type_actions = {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        a.setMenu(pattern_menu)

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

        self.refresh()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        select_group = QActionGroup(self)

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

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

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

        a.setMenu(select_menu)

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

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

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

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

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

        a.setMenu(tool_menu)

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

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

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

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

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

        self.set_label_wand_mode()

        self.addSeparator()
        type_group = QActionGroup(self)

        self._type_actions = {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        a.setMenu(pattern_menu)

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

        self.refresh()
Example #8
0
    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 __init__(self, *args, **kwargs):
        super(DataFrameEditor, self).__init__(*args, **kwargs)
        self.error_msg = PyErrorMessage(self)

        # Label for displaying the DataFrame size
        self.lbl_size = QLabel()

        # A Checkbox for enabling and disabling the editability of the index
        self.cb_index_editable = QCheckBox('Index editable')

        # A checkbox for enabling and disabling the change of data types
        self.cb_dtypes_changeable = QCheckBox('Datatypes changeable')

        # A checkbox for enabling and disabling sorting
        self.cb_enable_sort = QCheckBox('Enable sorting')

        # A button to open a dataframe from the file
        self.btn_open_df = QToolButton(parent=self)
        self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png')))
        self.btn_open_df.setToolTip('Open a DataFrame from your disk')

        self.btn_from_console = LoadFromConsoleButton(pd.DataFrame)
        self.btn_from_console.setToolTip('Show a DataFrame from the console')

        # The table to display the DataFrame
        self.table = DataFrameView(pd.DataFrame(), self)

        # format line edit
        self.format_editor = QLineEdit()
        self.format_editor.setText(self.table.model()._format)

        # format update button
        self.btn_change_format = QPushButton('Update')
        self.btn_change_format.setEnabled(False)

        # table clearing button
        self.btn_clear = QPushButton('Clear')
        self.btn_clear.setToolTip(
            'Clear the table and disconnect from the DataFrame')

        # refresh button
        self.btn_refresh = QToolButton()
        self.btn_refresh.setIcon(QIcon(get_icon('refresh.png')))
        self.btn_refresh.setToolTip('Refresh the table')

        # close button
        self.btn_close = QPushButton('Close')
        self.btn_close.setToolTip('Close this widget permanentely')

        # ---------------------------------------------------------------------
        # ------------------------ layout --------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()
        self.top_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.cb_index_editable)
        hbox.addWidget(self.cb_dtypes_changeable)
        hbox.addWidget(self.cb_enable_sort)
        hbox.addWidget(self.lbl_size)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_open_df)
        hbox.addWidget(self.btn_from_console)
        vbox.addLayout(hbox)
        vbox.addWidget(self.table)
        self.bottom_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.format_editor)
        hbox.addWidget(self.btn_change_format)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_clear)
        hbox.addWidget(self.btn_close)
        hbox.addWidget(self.btn_refresh)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # ------------------------ Connections --------------------------------
        # ---------------------------------------------------------------------
        self.cb_dtypes_changeable.stateChanged.connect(
            self.set_dtypes_changeable)
        self.cb_index_editable.stateChanged.connect(self.set_index_editable)
        self.btn_from_console.object_loaded.connect(self._open_ds_from_console)
        self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text())
        self.format_editor.textChanged.connect(self.toggle_fmt_button)
        self.btn_change_format.clicked.connect(self.update_format)
        self.btn_clear.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(lambda: self.close())
        self.btn_refresh.clicked.connect(self.table.reset_model)
        self.btn_open_df.clicked.connect(self._open_dataframe)
        self.table.set_index_action.triggered.connect(
            self.update_index_editable)
        self.table.append_index_action.triggered.connect(
            self.update_index_editable)
        self.cb_enable_sort.stateChanged.connect(self.table.setSortingEnabled)
class DataFrameEditor(DockMixin, QWidget):
    """An editor for data frames"""

    dock_cls = DataFrameDock

    #: A signal that is emitted, if the table is cleared
    cleared = QtCore.pyqtSignal()

    #: A signal that is emitted when a cell has been changed. The argument
    #: is a tuple of two integers and one float:
    #: the row index, the column index and the new value
    cell_edited = QtCore.pyqtSignal(int, int, object, object)

    #: A signal that is emitted, if rows have been inserted into the dataframe.
    #: The first value is the integer of the (original) position of the row,
    #: the second one is the number of rows
    rows_inserted = QtCore.pyqtSignal(int, int)

    @property
    def hidden(self):
        return not self.table.filled

    def __init__(self, *args, **kwargs):
        super(DataFrameEditor, self).__init__(*args, **kwargs)
        self.error_msg = PyErrorMessage(self)

        # Label for displaying the DataFrame size
        self.lbl_size = QLabel()

        # A Checkbox for enabling and disabling the editability of the index
        self.cb_index_editable = QCheckBox('Index editable')

        # A checkbox for enabling and disabling the change of data types
        self.cb_dtypes_changeable = QCheckBox('Datatypes changeable')

        # A checkbox for enabling and disabling sorting
        self.cb_enable_sort = QCheckBox('Enable sorting')

        # A button to open a dataframe from the file
        self.btn_open_df = QToolButton(parent=self)
        self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png')))
        self.btn_open_df.setToolTip('Open a DataFrame from your disk')

        self.btn_from_console = LoadFromConsoleButton(pd.DataFrame)
        self.btn_from_console.setToolTip('Show a DataFrame from the console')

        # The table to display the DataFrame
        self.table = DataFrameView(pd.DataFrame(), self)

        # format line edit
        self.format_editor = QLineEdit()
        self.format_editor.setText(self.table.model()._format)

        # format update button
        self.btn_change_format = QPushButton('Update')
        self.btn_change_format.setEnabled(False)

        # table clearing button
        self.btn_clear = QPushButton('Clear')
        self.btn_clear.setToolTip(
            'Clear the table and disconnect from the DataFrame')

        # refresh button
        self.btn_refresh = QToolButton()
        self.btn_refresh.setIcon(QIcon(get_icon('refresh.png')))
        self.btn_refresh.setToolTip('Refresh the table')

        # close button
        self.btn_close = QPushButton('Close')
        self.btn_close.setToolTip('Close this widget permanentely')

        # ---------------------------------------------------------------------
        # ------------------------ layout --------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()
        self.top_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.cb_index_editable)
        hbox.addWidget(self.cb_dtypes_changeable)
        hbox.addWidget(self.cb_enable_sort)
        hbox.addWidget(self.lbl_size)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_open_df)
        hbox.addWidget(self.btn_from_console)
        vbox.addLayout(hbox)
        vbox.addWidget(self.table)
        self.bottom_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.format_editor)
        hbox.addWidget(self.btn_change_format)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_clear)
        hbox.addWidget(self.btn_close)
        hbox.addWidget(self.btn_refresh)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # ------------------------ Connections --------------------------------
        # ---------------------------------------------------------------------
        self.cb_dtypes_changeable.stateChanged.connect(
            self.set_dtypes_changeable)
        self.cb_index_editable.stateChanged.connect(self.set_index_editable)
        self.btn_from_console.object_loaded.connect(self._open_ds_from_console)
        self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text())
        self.format_editor.textChanged.connect(self.toggle_fmt_button)
        self.btn_change_format.clicked.connect(self.update_format)
        self.btn_clear.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(lambda: self.close())
        self.btn_refresh.clicked.connect(self.table.reset_model)
        self.btn_open_df.clicked.connect(self._open_dataframe)
        self.table.set_index_action.triggered.connect(
            self.update_index_editable)
        self.table.append_index_action.triggered.connect(
            self.update_index_editable)
        self.cb_enable_sort.stateChanged.connect(self.table.setSortingEnabled)

    def update_index_editable(self):
        model = self.table.model()
        if len(model.df.index.names) > 1:
            model.index_editable = False
            self.cb_index_editable.setEnabled(False)
        self.cb_index_editable.setChecked(model.index_editable)

    def set_lbl_size_text(self, nrows=None, ncols=None):
        """Set the text of the :attr:`lbl_size` label to display the size"""
        model = self.table.model()
        nrows = nrows if nrows is not None else model.rowCount()
        ncols = ncols if ncols is not None else model.columnCount()
        if not nrows and not ncols:
            self.lbl_size.setText('')
        else:
            self.lbl_size.setText('Rows: %i, Columns: %i' % (nrows, ncols))

    def clear_table(self):
        """Clear the table and emit the :attr:`cleared` signal"""
        df = pd.DataFrame()
        self.set_df(df, show=False)

    def _open_ds_from_console(self, oname, df):
        self.set_df(df)

    @docstrings.dedent
    def set_df(self, df, *args, **kwargs):
        """
        Fill the table from a :class:`~pandas.DataFrame`

        Parameters
        ----------
        %(DataFrameModel.parameters.no_parent)s
        show: bool
            If True (default), show and raise_ the editor
        """
        show = kwargs.pop('show', True)
        self.table.set_df(df, *args, **kwargs)
        self.set_lbl_size_text(*df.shape)
        model = self.table.model()
        self.cb_dtypes_changeable.setChecked(model.dtypes_changeable)

        if len(model.df.index.names) > 1:
            model.index_editable = False
            self.cb_index_editable.setEnabled(False)
        else:
            self.cb_index_editable.setEnabled(True)
        self.cb_index_editable.setChecked(model.index_editable)
        self.cleared.emit()
        if show:
            self.show_plugin()
            self.dock.raise_()

    def set_index_editable(self, state):
        """Set the :attr:`DataFrameModel.index_editable` attribute"""
        self.table.model().index_editable = state == Qt.Checked

    def set_dtypes_changeable(self, state):
        """Set the :attr:`DataFrameModel.dtypes_changeable` attribute"""
        self.table.model().dtypes_changeable = state == Qt.Checked

    def toggle_fmt_button(self, text):
        try:
            text % 1.1
        except (TypeError, ValueError):
            self.btn_change_format.setEnabled(False)
        else:
            self.btn_change_format.setEnabled(
                text.strip() != self.table.model()._format)

    def update_format(self):
        """Update the format of the table"""
        self.table.model().set_format(self.format_editor.text().strip())

    def to_dock(self, main, *args, **kwargs):
        connect = self.dock is None
        super(DataFrameEditor, self).to_dock(main, *args, **kwargs)
        if connect:
            self.dock.toggleViewAction().triggered.connect(self.maybe_tabify)

    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 _open_dataframe(self):
        self.open_dataframe()

    def open_dataframe(self, fname=None, *args, **kwargs):
        """Opens a file dialog and the dataset that has been inserted"""
        if fname is None:
            fname = QFileDialog.getOpenFileName(
                self, 'Open dataset', os.getcwd(),
                'Comma separated files (*.csv);;'
                'Excel files (*.xls *.xlsx);;'
                'JSON files (*.json);;'
                'All files (*)')
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
        if isinstance(fname, pd.DataFrame):
            self.set_df(fname)
        elif not fname:
            return
        else:
            ext = osp.splitext(fname)[1]
            open_funcs = {
                '.xls': pd.read_excel,
                '.xlsx': pd.read_excel,
                '.json': pd.read_json,
                '.tab': partial(pd.read_csv, delimiter='\t'),
                '.dat': partial(pd.read_csv, delim_whitespace=True),
            }
            open_func = open_funcs.get(ext, pd.read_csv)
            try:
                df = open_func(fname)
            except Exception:
                self.error_msg.showTraceback(
                    '<b>Could not open DataFrame %s with %s</b>' %
                    (fname, open_func))
                return
            self.set_df(df)

    def close(self, *args, **kwargs):
        if self.dock is not None:
            self.dock.close(*args, **kwargs)  # removes the dock window
            del self.dock
        return super(DataFrameEditor, self).close(*args, **kwargs)
Example #11
0
class DimensionsWidget(QWidget):
    """A widget for updating the dimensions"""

    def __init__(self, parent, dim=None):
        super(DimensionsWidget, self).__init__(parent)
        self.coord_combo = CoordComboBox(self.get_ds, dim)
        self.cb_use_coord = QCheckBox('show coordinates')
        self.cb_close_popups = QCheckBox('close dropdowns')
        self.cb_close_popups.setChecked(True)
        self.toggle_close_popup()
        self._single_selection = False

        self.dim = dim
        hbox = QHBoxLayout()
        hbox.addWidget(self.cb_close_popups)
        hbox.addWidget(self.cb_use_coord)
        hbox.addWidget(self.coord_combo)
        self.setLayout(hbox)
        self.cb_use_coord.stateChanged.connect(self.reset_combobox)
        self.cb_close_popups.stateChanged.connect(self.toggle_close_popup)
        self.coord_combo.leftclick.connect(self.insert_from_combo)

    def set_dim(self, dim):
        self.dim = self.coord_combo.dim = dim

    def slice2list(self, sl):
        if not isinstance(sl, slice):
            return sl
        return list(range(*sl.indices(self.coord_combo.count() - 1)))

    def reset_combobox(self):
        """Clear all comboboxes"""
        self.coord_combo.use_coords = self.cb_use_coord.isChecked()
        self.coord_combo.clear()
        self.coord_combo._is_empty = True

    def toggle_close_popup(self):
        self.coord_combo.close_popups = self.cb_close_popups.isChecked()

    def insert_from_combo(self):
        cb = self.coord_combo
        inserts = list(
            ind.row() - 1
            for ind in cb.view().selectionModel().selectedIndexes()
            if ind.row() > 0)
        if not inserts:
            return
        elif not self._single_selection:
            try:
                current = yaml.load(self.parent().get_text())
            except Exception:
                pass
            else:
                if current:
                    current = self.slice2list(current)
                    inserts = sorted(set(chain(inserts, safe_list(current))))
        else:
            inserts = inserts[0]

        self.parent().set_obj(inserts)

    def get_ds(self):
        import psyplot.project as psy
        project = psy.gcp()
        datasets = project.datasets
        dim = self.dim
        dims = {ds.coords[dim].shape[0]: ds for ds in datasets.values()}
        if len(dims) > 1:
            warn("Datasets have differing dimensions lengths for the "
                 "%s dimension!" % dim)
        return min(dims.items())[1]

    def set_single_selection(self, yes=True):
        self._single_selection = yes
        if yes:
            self.coord_combo.view().setSelectionMode(
                QListView.SingleSelection)
        else:
            self.coord_combo.view().setSelectionMode(
                QListView.ExtendedSelection)
Example #12
0
    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)
Example #13
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)})
        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)
        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).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())
Example #14
0
class DimensionsWidget(QWidget):
    """A widget for updating the dimensions"""
    def __init__(self, parent, dim=None):
        super(DimensionsWidget, self).__init__(parent)
        self.coord_combo = CoordComboBox(self.get_ds, dim)
        self.cb_use_coord = QCheckBox('show coordinates')
        self.cb_close_popups = QCheckBox('close dropdowns')
        self.cb_close_popups.setChecked(True)
        self.toggle_close_popup()
        self._single_selection = False

        self.dim = dim
        hbox = QHBoxLayout()
        hbox.addWidget(self.cb_close_popups)
        hbox.addWidget(self.cb_use_coord)
        hbox.addWidget(self.coord_combo)
        self.setLayout(hbox)
        self.cb_use_coord.stateChanged.connect(self.reset_combobox)
        self.cb_close_popups.stateChanged.connect(self.toggle_close_popup)
        self.coord_combo.leftclick.connect(self.insert_from_combo)

    def set_dim(self, dim):
        self.dim = self.coord_combo.dim = dim

    def slice2list(self, sl):
        if not isinstance(sl, slice):
            return sl
        return list(range(*sl.indices(self.coord_combo.count() - 1)))

    def reset_combobox(self):
        """Clear all comboboxes"""
        self.coord_combo.use_coords = self.cb_use_coord.isChecked()
        self.coord_combo.clear()
        self.coord_combo._is_empty = True

    def toggle_close_popup(self):
        self.coord_combo.close_popups = self.cb_close_popups.isChecked()

    def insert_from_combo(self):
        cb = self.coord_combo
        inserts = list(ind.row() - 1
                       for ind in cb.view().selectionModel().selectedIndexes()
                       if ind.row() > 0)
        if not inserts:
            return
        elif not self._single_selection:
            try:
                current = yaml.load(self.parent().get_text(),
                                    Loader=yaml.Loader)
            except Exception:
                pass
            else:
                if current:
                    current = self.slice2list(current)
                    inserts = sorted(set(chain(inserts, safe_list(current))))
        else:
            inserts = inserts[0]

        self.parent().set_obj(inserts)

    def get_ds(self):
        import psyplot.project as psy
        project = psy.gcp()
        datasets = project.datasets
        dim = self.dim
        dims = {ds.coords[dim].shape[0]: ds for ds in datasets.values()}
        if len(dims) > 1:
            warn("Datasets have differing dimensions lengths for the "
                 "%s dimension!" % dim)
        return min(dims.items())[1]

    def set_single_selection(self, yes=True):
        self._single_selection = yes
        if yes:
            self.coord_combo.view().setSelectionMode(QListView.SingleSelection)
        else:
            self.coord_combo.view().setSelectionMode(
                QListView.ExtendedSelection)
Example #15
0
class DataFrameEditor(DockMixin, QWidget):
    """An editor for data frames"""

    dock_cls = DataFrameDock

    #: A signal that is emitted, if the table is cleared
    cleared = QtCore.pyqtSignal()

    #: A signal that is emitted when a cell has been changed. The argument
    #: is a tuple of two integers and one float:
    #: the row index, the column index and the new value
    cell_edited = QtCore.pyqtSignal(int, int, object, object)

    #: A signal that is emitted, if rows have been inserted into the dataframe.
    #: The first value is the integer of the (original) position of the row,
    #: the second one is the number of rows
    rows_inserted = QtCore.pyqtSignal(int, int)

    @property
    def hidden(self):
        return not self.table.filled

    def __init__(self, *args, **kwargs):
        super(DataFrameEditor, self).__init__(*args, **kwargs)
        self.error_msg = PyErrorMessage(self)

        # Label for displaying the DataFrame size
        self.lbl_size = QLabel()

        # A Checkbox for enabling and disabling the editability of the index
        self.cb_index_editable = QCheckBox('Index editable')

        # A checkbox for enabling and disabling the change of data types
        self.cb_dtypes_changeable = QCheckBox('Datatypes changeable')

        # A checkbox for enabling and disabling sorting
        self.cb_enable_sort = QCheckBox('Enable sorting')

        # A button to open a dataframe from the file
        self.btn_open_df = QToolButton(parent=self)
        self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png')))
        self.btn_open_df.setToolTip('Open a DataFrame from your disk')

        self.btn_from_console = LoadFromConsoleButton(pd.DataFrame)
        self.btn_from_console.setToolTip('Show a DataFrame from the console')

        # The table to display the DataFrame
        self.table = DataFrameView(pd.DataFrame(), self)

        # format line edit
        self.format_editor = QLineEdit()
        self.format_editor.setText(self.table.model()._format)

        # format update button
        self.btn_change_format = QPushButton('Update')
        self.btn_change_format.setEnabled(False)

        # table clearing button
        self.btn_clear = QPushButton('Clear')
        self.btn_clear.setToolTip(
            'Clear the table and disconnect from the DataFrame')

        # refresh button
        self.btn_refresh = QToolButton()
        self.btn_refresh.setIcon(QIcon(get_icon('refresh.png')))
        self.btn_refresh.setToolTip('Refresh the table')

        # close button
        self.btn_close = QPushButton('Close')
        self.btn_close.setToolTip('Close this widget permanentely')

        # ---------------------------------------------------------------------
        # ------------------------ layout --------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()
        self.top_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.cb_index_editable)
        hbox.addWidget(self.cb_dtypes_changeable)
        hbox.addWidget(self.cb_enable_sort)
        hbox.addWidget(self.lbl_size)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_open_df)
        hbox.addWidget(self.btn_from_console)
        vbox.addLayout(hbox)
        vbox.addWidget(self.table)
        self.bottom_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.format_editor)
        hbox.addWidget(self.btn_change_format)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_clear)
        hbox.addWidget(self.btn_close)
        hbox.addWidget(self.btn_refresh)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # ------------------------ Connections --------------------------------
        # ---------------------------------------------------------------------
        self.cb_dtypes_changeable.stateChanged.connect(
            self.set_dtypes_changeable)
        self.cb_index_editable.stateChanged.connect(self.set_index_editable)
        self.btn_from_console.object_loaded.connect(self._open_ds_from_console)
        self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text())
        self.format_editor.textChanged.connect(self.toggle_fmt_button)
        self.btn_change_format.clicked.connect(self.update_format)
        self.btn_clear.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(lambda: self.close())
        self.btn_refresh.clicked.connect(self.table.reset_model)
        self.btn_open_df.clicked.connect(self._open_dataframe)
        self.table.set_index_action.triggered.connect(
            self.update_index_editable)
        self.table.append_index_action.triggered.connect(
            self.update_index_editable)
        self.cb_enable_sort.stateChanged.connect(
            self.table.setSortingEnabled)

    def update_index_editable(self):
        model = self.table.model()
        if len(model.df.index.names) > 1:
            model.index_editable = False
            self.cb_index_editable.setEnabled(False)
        self.cb_index_editable.setChecked(model.index_editable)

    def set_lbl_size_text(self, nrows=None, ncols=None):
        """Set the text of the :attr:`lbl_size` label to display the size"""
        model = self.table.model()
        nrows = nrows if nrows is not None else model.rowCount()
        ncols = ncols if ncols is not None else model.columnCount()
        if not nrows and not ncols:
            self.lbl_size.setText('')
        else:
            self.lbl_size.setText('Rows: %i, Columns: %i' % (nrows, ncols))

    def clear_table(self):
        """Clear the table and emit the :attr:`cleared` signal"""
        df = pd.DataFrame()
        self.set_df(df, show=False)

    def _open_ds_from_console(self, oname, df):
        self.set_df(df)

    @docstrings.dedent
    def set_df(self, df, *args, **kwargs):
        """
        Fill the table from a :class:`~pandas.DataFrame`

        Parameters
        ----------
        %(DataFrameModel.parameters.no_parent)s
        show: bool
            If True (default), show and raise_ the editor
        """
        show = kwargs.pop('show', True)
        self.table.set_df(df, *args, **kwargs)
        self.set_lbl_size_text(*df.shape)
        model = self.table.model()
        self.cb_dtypes_changeable.setChecked(model.dtypes_changeable)

        if len(model.df.index.names) > 1:
            model.index_editable = False
            self.cb_index_editable.setEnabled(False)
        else:
            self.cb_index_editable.setEnabled(True)
        self.cb_index_editable.setChecked(model.index_editable)
        self.cleared.emit()
        if show:
            self.show_plugin()
            self.dock.raise_()

    def set_index_editable(self, state):
        """Set the :attr:`DataFrameModel.index_editable` attribute"""
        self.table.model().index_editable = state == Qt.Checked

    def set_dtypes_changeable(self, state):
        """Set the :attr:`DataFrameModel.dtypes_changeable` attribute"""
        self.table.model().dtypes_changeable = state == Qt.Checked

    def toggle_fmt_button(self, text):
        try:
            text % 1.1
        except (TypeError, ValueError):
            self.btn_change_format.setEnabled(False)
        else:
            self.btn_change_format.setEnabled(
                text.strip() != self.table.model()._format)

    def update_format(self):
        """Update the format of the table"""
        self.table.model().set_format(self.format_editor.text().strip())

    def to_dock(self, main, *args, **kwargs):
        connect = self.dock is None
        super(DataFrameEditor, self).to_dock(main, *args, **kwargs)
        if connect:
            self.dock.toggleViewAction().triggered.connect(self.maybe_tabify)

    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 _open_dataframe(self):
        self.open_dataframe()

    def open_dataframe(self, fname=None, *args, **kwargs):
        """Opens a file dialog and the dataset that has been inserted"""
        if fname is None:
            fname = QFileDialog.getOpenFileName(
                self, 'Open dataset', os.getcwd(),
                'Comma separated files (*.csv);;'
                'Excel files (*.xls *.xlsx);;'
                'JSON files (*.json);;'
                'All files (*)'
                )
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
        if isinstance(fname, pd.DataFrame):
            self.set_df(fname)
        elif not fname:
            return
        else:
            ext = osp.splitext(fname)[1]
            open_funcs = {
                '.xls': pd.read_excel, '.xlsx': pd.read_excel,
                '.json': pd.read_json,
                '.tab': partial(pd.read_csv, delimiter='\t'),
                '.dat': partial(pd.read_csv, delim_whitespace=True),
                }
            open_func = open_funcs.get(ext, pd.read_csv)
            try:
                df = open_func(fname)
            except Exception:
                self.error_msg.showTraceback(
                    '<b>Could not open DataFrame %s with %s</b>' % (
                        fname, open_func))
                return
            self.set_df(df)

    def close(self, *args, **kwargs):
        if self.dock is not None:
            self.dock.close(*args, **kwargs)  # removes the dock window
            del self.dock
        return super(DataFrameEditor, self).close(*args, **kwargs)
Example #16
0
class ExportDfDialog(QDialog):
    """A QDialog to export a :class:`pandas.DataFrame` to Excel or CSV"""
    @docstrings.get_sectionsf('ExportDfDialog')
    def __init__(self, df, straditizer, fname=None, *args, **kwargs):
        """
        Parameters
        ----------
        df: pandas.DataFrame
            The DataFrame to be exported
        straditizer: straditize.straditizer.Straditizer
            The source straditizer
        fname: str
            The file name to export to
        """
        super().__init__(*args, **kwargs)
        self.df = df
        self.stradi = straditizer
        self.txt_fname = QLineEdit()
        self.bt_open_file = QToolButton()
        self.bt_open_file.setIcon(QIcon(get_icon('run_arrow.png')))
        self.bt_open_file.setToolTip('Select the export file on your drive')

        self.cb_include_meta = QCheckBox('Include meta data')
        self.cb_include_meta.setChecked(True)

        self.bbox = bbox = QDialogButtonBox(QDialogButtonBox.Ok
                                            | QDialogButtonBox.Cancel)

        # ---------------------------------------------------------------------
        # --------------------------- Layouts ---------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()

        hbox = QHBoxLayout()
        hbox.addWidget(QLabel('Export to:'))
        hbox.addWidget(self.txt_fname)
        hbox.addWidget(self.bt_open_file)
        vbox.addLayout(hbox)

        vbox.addWidget(self.cb_include_meta)

        vbox.addWidget(bbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # --------------------------- Connections -----------------------------
        # ---------------------------------------------------------------------
        bbox.accepted.connect(self._export)
        bbox.rejected.connect(self.reject)
        self.bt_open_file.clicked.connect(self.get_open_file_name)

        if fname is not None:
            self.txt_fname.setText(fname)
            self._export()

    def get_open_file_name(self):
        """Ask the user for a filename for saving the data frame"""
        def check_current():
            dirname = osp.dirname(current)
            if osp.exists(dirname) and osp.isdir(dirname):
                return dirname

        current = self.txt_fname.text().strip()
        start = None
        if current:
            start = check_current()
        if start is None:
            for attr in 'project_file', 'image_file':
                try:
                    current = self.stradi.get_attr(attr)
                except KeyError:
                    pass
                else:
                    start = check_current()
                    if start is not None:
                        break
        if start is None:
            start = os.getcwd()
        fname = QFileDialog.getSaveFileName(
            self, 'DataFrame file destination', start,
            'Excel files (*.xlsx *.xls);;'
            'csv files (*.csv);;'
            'All files (*)')
        if with_qt5:  # the filter is passed as well
            fname = fname[0]
        if not fname:
            return
        self.txt_fname.setText(fname)

    def _export(self):
        fname = self.txt_fname.text()
        ending = osp.splitext(fname)[1]
        self.stradi.set_attr('exported', str(dt.datetime.now()))
        meta = self.stradi.valid_attrs
        if ending in ['.xls', '.xlsx']:
            with pd.ExcelWriter(fname) as writer:
                self.df.to_excel(writer, 'Data')
                if self.cb_include_meta.isChecked() and len(meta):
                    meta.to_excel(writer, 'Metadata', header=False)
        else:
            with open(fname, 'w') as f:
                if self.cb_include_meta.isChecked():
                    for t in meta.iloc[:, 0].items():
                        f.write('# %s: %s\n' % t)
            self.df.to_csv(fname, mode='a')
        self.accept()

    def cancel(self):
        del self.stradi, self.df
        super().cancel()

    def accept(self):
        del self.stradi, self.df
        super().accept()

    @classmethod
    @docstrings.dedent
    def export_df(cls, parent, df, straditizer, fname=None, exec_=True):
        """Open a dialog for exporting a DataFrame

        Parameters
        ----------
        parent: QWidget
            The parent widget
        %(ExportDfDialog.parameters)s"""
        dialog = cls(df, straditizer, fname, parent=parent)
        if fname is None:
            available_width = QDesktopWidget().availableGeometry().width() / 3.
            width = dialog.sizeHint().width()
            height = dialog.sizeHint().height()
            # The plot creator window should cover at least one third of the
            # screen
            dialog.resize(max(available_width, width), height)
            if exec_:
                dialog.exec_()
            else:
                return dialog
Example #17
0
    def __init__(self, *args, **kwargs):
        super(DataFrameEditor, self).__init__(*args, **kwargs)
        self.error_msg = PyErrorMessage(self)

        # Label for displaying the DataFrame size
        self.lbl_size = QLabel()

        # A Checkbox for enabling and disabling the editability of the index
        self.cb_index_editable = QCheckBox('Index editable')

        # A checkbox for enabling and disabling the change of data types
        self.cb_dtypes_changeable = QCheckBox('Datatypes changeable')

        # A checkbox for enabling and disabling sorting
        self.cb_enable_sort = QCheckBox('Enable sorting')

        # A button to open a dataframe from the file
        self.btn_open_df = QToolButton(parent=self)
        self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png')))
        self.btn_open_df.setToolTip('Open a DataFrame from your disk')

        self.btn_from_console = LoadFromConsoleButton(pd.DataFrame)
        self.btn_from_console.setToolTip('Show a DataFrame from the console')

        # The table to display the DataFrame
        self.table = DataFrameView(pd.DataFrame(), self)

        # format line edit
        self.format_editor = QLineEdit()
        self.format_editor.setText(self.table.model()._format)

        # format update button
        self.btn_change_format = QPushButton('Update')
        self.btn_change_format.setEnabled(False)

        # table clearing button
        self.btn_clear = QPushButton('Clear')
        self.btn_clear.setToolTip(
            'Clear the table and disconnect from the DataFrame')

        # refresh button
        self.btn_refresh = QToolButton()
        self.btn_refresh.setIcon(QIcon(get_icon('refresh.png')))
        self.btn_refresh.setToolTip('Refresh the table')

        # close button
        self.btn_close = QPushButton('Close')
        self.btn_close.setToolTip('Close this widget permanentely')

        # ---------------------------------------------------------------------
        # ------------------------ layout --------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()
        self.top_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.cb_index_editable)
        hbox.addWidget(self.cb_dtypes_changeable)
        hbox.addWidget(self.cb_enable_sort)
        hbox.addWidget(self.lbl_size)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_open_df)
        hbox.addWidget(self.btn_from_console)
        vbox.addLayout(hbox)
        vbox.addWidget(self.table)
        self.bottom_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.format_editor)
        hbox.addWidget(self.btn_change_format)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_clear)
        hbox.addWidget(self.btn_close)
        hbox.addWidget(self.btn_refresh)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # ------------------------ Connections --------------------------------
        # ---------------------------------------------------------------------
        self.cb_dtypes_changeable.stateChanged.connect(
            self.set_dtypes_changeable)
        self.cb_index_editable.stateChanged.connect(self.set_index_editable)
        self.btn_from_console.object_loaded.connect(self._open_ds_from_console)
        self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text())
        self.format_editor.textChanged.connect(self.toggle_fmt_button)
        self.btn_change_format.clicked.connect(self.update_format)
        self.btn_clear.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(lambda: self.close())
        self.btn_refresh.clicked.connect(self.table.reset_model)
        self.btn_open_df.clicked.connect(self._open_dataframe)
        self.table.set_index_action.triggered.connect(
            self.update_index_editable)
        self.table.append_index_action.triggered.connect(
            self.update_index_editable)
        self.cb_enable_sort.stateChanged.connect(
            self.table.setSortingEnabled)
Example #18
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())