Exemple #1
0
    def test_drag_drop(self):
        """Test the drag and drop of the
        :class:`psyplot_gui.plot_creator.ArrayTable`"""
        self.pc.show()
        # XXX Try to use directly the dropEvent method by setting the source of
        # the event!
        point = QtCore.QPoint(0, 0)
        data = QtCore.QMimeData()
        event = QtGui.QDropEvent(point, Qt.MoveAction, data, Qt.LeftButton,
                                 Qt.NoModifier, QtCore.QEvent.Drop)

        # open dataset
        fname = self.get_file('test-t2m-u-v.nc')
        ds = psy.open_dataset(fname)
        self.pc.bt_get_ds.get_from_shell(ds)

        # add data arrays
        QTest.mouseClick(self.pc.bt_add_all, Qt.LeftButton)

        # move rows
        atab = self.pc.array_table
        old = list(atab.arr_names_dict.items())
        atab.selectRow(2)
        atab.dropOn(event)
        resorted = [old[i] for i in [2, 0, 1] + list(range(3, len(old)))]
        self.assertEqual(list(atab.arr_names_dict.items()), resorted,
                         msg="Rows not moved correctly!")
        ds.close()
Exemple #2
0
 def fetch_more(self, rows=False, columns=False):
     if self.can_fetch_more(rows=rows):
         reminder = self.total_rows - self.rows_loaded
         items_to_fetch = min(reminder, self.ROWS_TO_LOAD)
         self.beginInsertRows(QtCore.QModelIndex(), self.rows_loaded,
                              self.rows_loaded + items_to_fetch - 1)
         self.rows_loaded += items_to_fetch
         self.endInsertRows()
     if self.can_fetch_more(columns=columns):
         reminder = self.total_cols - self.cols_loaded
         items_to_fetch = min(reminder, self.COLS_TO_LOAD)
         self.beginInsertColumns(QtCore.QModelIndex(), self.cols_loaded,
                                 self.cols_loaded + items_to_fetch - 1)
         self.cols_loaded += items_to_fetch
         self.endInsertColumns()
Exemple #3
0
    def __init__(self, names=[], N=10, *args, **kwargs):
        """
        Parameters
        ----------
        %(ColormapModel.parameters)s

        Other Parameters
        ----------------
        ``*args, **kwargs``
            Anything else that is passed to the ColormapDialog
        """
        super(QDialog, self).__init__(*args, **kwargs)
        vbox = QVBoxLayout()
        self.table = ColormapTable(names=names, N=N)
        vbox.addWidget(self.table)
        self.setLayout(vbox)
        col_width = self.table.columnWidth(0)
        header_width = self.table.verticalHeader().width()
        row_height = self.table.rowHeight(0)
        available = QDesktopWidget().availableGeometry()
        height = int(min(row_height * (self.table.rowCount() + 1),
                         2. * available.height() / 3.))
        width = int(min(header_width + col_width * N + 0.5 * col_width,
                        2. * available.width() / 3.))
        self.resize(QtCore.QSize(width, height))
Exemple #4
0
 def load_new_marks(self, mark):
     new = [mark.y, mark]
     self.marks.append(new)
     self.sort_marks()
     idx = self.marks.index(new)
     self.beginInsertRows(QtCore.QModelIndex(), idx, idx)
     self.endInsertRows()
     mark.moved.connect(self.update_after_move)
     self.update_lines()
Exemple #5
0
 def columnCount(self, index=QtCore.QModelIndex()):
     """DataFrame column number"""
     # This is done to implement series
     if len(self.df.shape) == 1:
         return 2
     elif self.total_cols <= self.cols_loaded:
         return self.total_cols + 1
     else:
         return self.cols_loaded + 1
Exemple #6
0
 def __init__(self, valid, sep=',', *args, **kwargs):
     """
     Parameters
     ----------
     valid: list of str
         The possible choices
     sep: str, optional
         The separation pattern
     ``*args,**kwargs``
         Determined by PyQt5.QtGui.QValidator
     """
     patt = QtCore.QRegExp('^((%s)(;;)?)+$' % '|'.join(valid))
     super(QRegExpValidator, self).__init__(patt, *args, **kwargs)
Exemple #7
0
class EnableButton(QPushButton):
    """A `QPushButton` that emits a signal when enabled"""

    #: A signal that is emitted with a boolean whether if the button is
    #: enabled or disabled
    enabled = QtCore.pyqtSignal(bool)

    def setEnabled(self, b):
        """Reimplemented to emit the :attr:`enabled` signal"""
        if b is self.isEnabled():
            return
        super(EnableButton, self).setEnabled(b)
        self.enabled.emit(b)
Exemple #8
0
 def delRow(self, irow):
     mark = self.marks[irow][1]
     try:
         mark.moved.disconnect(self.update_after_move)
     except ValueError:
         pass
     try:
         self._remove_mark(mark)
     except ValueError:
         pass
     del self.marks[irow]
     self.beginRemoveRows(QtCore.QModelIndex(), irow, irow)
     self.endRemoveRows()
     self.update_lines()
class ConfigPage(object):
    """An abstract base class for configuration pages"""

    #: A signal that shall be emitted if the validation state changes
    validChanged = QtCore.pyqtSignal(bool)

    #: A signal that is emitted if changes are propsed. The signal should be
    #: emitted with the instance of the page itself
    propose_changes = QtCore.pyqtSignal(object)

    #: The title for the config page
    title = None

    #: The icon of the page
    icon = None

    #: :class:`bool` that is True, if the changes in this ConfigPage are set
    #: immediately
    auto_updates = False

    @property
    def is_valid(self):
        """Check whether the page is valid"""
        raise NotImplementedError

    @property
    def changed(self):
        """Check whether the preferences will change"""
        raise NotImplementedError

    def initialize(self):
        """Initialize the page"""
        raise NotImplementedError

    def apply_changes(self):
        """Apply the planned changes"""
        raise NotImplementedError
Exemple #10
0
 def remove_mark(self, mark):
     found = False
     for i, (y, m) in enumerate(self.marks):
         if m is mark:
             found = True
             break
     if found:
         try:
             mark.moved.disconnect(self.update_after_move)
         except ValueError:
             pass
         del self.marks[i]
         self.beginRemoveRows(QtCore.QModelIndex(), i, i)
         self.endRemoveRows()
         self.update_lines()
Exemple #11
0
    def insertRows(self, irow, nrows=1):
        """Insert a row into the :attr:`df`

        Parameters
        ----------
        irow: int
            The row index. If `irow` is equal to the length of the
            :attr:`df`, the rows will be appended.
        nrows: int
            The number of rows to insert"""
        df = self.df
        if not irow:
            if not len(df):
                idx = 0
            else:
                idx = df.index.values[0]
        else:
            try:
                idx = df.index.values[irow-1:irow+1].mean()
            except TypeError:
                idx = df.index.values[min(irow, len(df) - 1)]
            else:
                idx = df.index.values[min(irow, len(df) - 1)].__class__(idx)
        # reset the index to sort it correctly
        idx_name = df.index.name
        dtype = df.index.dtype
        df.reset_index(inplace=True)
        new_idx_name = df.columns[0]
        current_len = len(df)
        for i in range(nrows):
            df.loc[current_len + i, new_idx_name] = idx
        df[new_idx_name] = df[new_idx_name].astype(dtype)
        if irow < current_len:
            changed = df.index.values.astype(float)
            changed[current_len:] = irow - 0.5
            df.index = changed
            df.sort_index(inplace=True)
        df.set_index(new_idx_name, inplace=True, drop=True)
        df.index.name = idx_name
        self.update_df_index()
        self.beginInsertRows(QtCore.QModelIndex(), self.rows_loaded,
                             self.rows_loaded + nrows - 1)
        self.total_rows += nrows
        self.rows_loaded += nrows
        self.endInsertRows()
        self._parent.rows_inserted.emit(irow, nrows)
Exemple #12
0
    def __init__(self, versions, *args, **kwargs):
        """
        Parameters
        ----------
        %(DependenciesTree.parameters)s
        """
        super(DependenciesDialog, self).__init__(*args, **kwargs)
        self.setWindowTitle('Dependencies')
        self.versions = versions
        self.vbox = layout = QVBoxLayout()

        self.label = QLabel("""
            psyplot and the plugins depend on several python libraries. The
            tree widget below lists the versions of the plugins and the
            requirements. You can select the items in the tree and copy them to
            clipboard.""", parent=self)

        layout.addWidget(self.label)

        self.tree = DependenciesTree(versions, parent=self)
        self.tree.setSelectionMode(QAbstractItemView.MultiSelection)
        layout.addWidget(self.tree)

        # copy button
        self.bt_copy = QPushButton('Copy selection to clipboard')
        self.bt_copy.setToolTip(
            'Copy the selected packages in the above table to the clipboard.')
        self.bt_copy.clicked.connect(lambda: self.copy_selected())

        self.bbox = QDialogButtonBox(QDialogButtonBox.Ok)
        self.bbox.accepted.connect(self.accept)

        hbox = QHBoxLayout()
        hbox.addWidget(self.bt_copy)
        hbox.addStretch(1)
        hbox.addWidget(self.bbox)
        layout.addLayout(hbox)

        #: A label for simple status update
        self.info_label = QLabel('', self)
        layout.addWidget(self.info_label)
        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.clear_label)

        self.setLayout(layout)
Exemple #13
0
    def load_new_marks(self, mark):
        """Add a new mark into the table after they have been added by the user

        Parameters
        ----------
        mark: straditize.cross_mark.CrossMarks
            The added mark"""
        self._new_marks.append(mark)
        mark.moved.connect(self.update_after_move)
        if len(self._new_marks) == self.columnCount() - 1:
            new = (self._new_marks[0].y, self._new_marks)
            self.marks.append(new)
            self._new_marks = []
            self.sort_marks()
            idx = self.marks.index(new)
            self.beginInsertRows(QtCore.QModelIndex(), idx, idx)
            self.endInsertRows()
            self.update_lines()
Exemple #14
0
 def browse(self, url):
     """Make a web browse on the given url and show the page on the Webview
     widget. """
     if self.bt_lock.isChecked():
         return
     if not self.url_like_re.match(url):
         url = 'https://' + url
     if self.bt_url_lock.isChecked() and url.startswith('http'):
         return
     if not self.completed:
         logger.debug('Stopping current load...')
         self.html.stop()
         self.completed = True
     logger.debug('Loading %s', url)
     # we use :meth:`PyQt5.QtWebEngineWidgets.QWebEngineView.setUrl` instead
     # of :meth:`PyQt5.QtWebEngineWidgets.QWebEngineView.load` because that
     # changes the url directly and is more useful for unittests
     self.html.setUrl(QtCore.QUrl(url))
Exemple #15
0
    def remove_mark(self, mark):
        """Remove a mark from the table after it has been removed by the user

        Parameters
        ----------
        mark: straditize.cross_mark.CrossMarks
            The removed mark"""
        found = False
        for i, (y, marks) in enumerate(self.marks):
            if mark in marks:
                found = True
                break
        if found:
            for m in self.marks[i][1]:
                try:
                    m.moved.disconnect(self.update_after_move)
                except ValueError:
                    pass
            del self.marks[i]
            self.beginRemoveRows(QtCore.QModelIndex(), i, i)
            self.endRemoveRows()
            self.update_lines()
Exemple #16
0
    def insertRow(self, irow, xa=None, ya=None):
        """Insert a row into the table

        Parameters
        ----------
        irow: int
            The row index. If `irow` is equal to the length of the
            :attr:`marks`, the rows will be appended"""
        if xa is None or ya is None:
            mark = self.marks[min(irow, len(self.marks) - 1)][1]
            new = self._new_mark(mark.xa[0], mark.ya[0])[0]
        else:
            new = self._new_mark(xa + self._bounds[:, 0], ya + self._y0)[0]
            new.set_pos((xa + self._bounds[:, 0], ya + self._y0))
        y = new.y
        new.moved.connect(self.update_after_move)
        if irow == len(self.marks):
            self.marks.append((y, new))
        else:
            self.marks.insert(irow, (y, new))
        self.beginInsertRows(QtCore.QModelIndex(), irow, irow)
        self.endInsertRows()
        self.update_lines()
    def add_page(self, widget):
        """Add a new page to the preferences dialog

        Parameters
        ----------
        widget: ConfigPage
            The page to add"""
        widget.validChanged.connect(self.bt_apply.setEnabled)
        widget.validChanged.connect(
            self.bbox.button(QDialogButtonBox.Ok).setEnabled)
        scrollarea = QScrollArea(self)
        scrollarea.setWidgetResizable(True)
        scrollarea.setWidget(widget)
        self.pages_widget.addWidget(scrollarea)
        item = QListWidgetItem(self.contents_widget)
        try:
            item.setIcon(widget.icon)
        except TypeError:
            pass
        item.setText(widget.title)
        item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        item.setSizeHint(QtCore.QSize(0, 25))
        widget.propose_changes.connect(self.check_changes)
Exemple #18
0
class SphinxThread(QtCore.QThread):
    """A thread to render sphinx documentation in a separate process"""

    #: A signal to be emitted when the rendering finished. The url is the
    #: file location
    html_ready = QtCore.pyqtSignal(str)
    html_error = QtCore.pyqtSignal(str)

    def __init__(self, outdir, html_text_no_doc=''):
        super(SphinxThread, self).__init__()
        self.doc = None
        self.name = None
        self.html_text_no_doc = html_text_no_doc
        self.outdir = outdir
        self.index_file = osp.join(self.outdir, 'psyplot.rst')
        self.confdir = osp.join(get_module_path(__name__), 'sphinx_supp')
        shutil.copyfile(osp.join(self.confdir, 'psyplot.rst'),
                        osp.join(self.outdir, 'psyplot.rst'))
        self.build_dir = osp.join(self.outdir, '_build', 'html')

    def render(self, doc, name):
        """Render the given rst string and save the file as ``name + '.rst'``

        Parameters
        ----------
        doc: str
            The rst docstring
        name: str
            the name to use for the file"""
        if self.wait():
            self.doc = doc
            self.name = name
            # start rendering in separate process
            if rcParams['help_explorer.render_docs_parallel']:
                self.start()
            else:
                self.run()

    def run(self):
        """Create the html file. When called the first time, it may take a
        while because the :class:`sphinx.application.Sphinx` app is build,
        potentially with intersphinx

        When finished, the html_ready signal is emitted"""
        if not hasattr(self, 'app'):
            from IPython.core.history import HistoryAccessor
            # to avoid history access conflicts between different threads,
            # we disable the ipython history
            HistoryAccessor.enabled.default_value = False
            self.app = Sphinx(self.outdir,
                              self.confdir,
                              self.build_dir,
                              osp.join(self.outdir, '_build', 'doctrees'),
                              'html',
                              status=StreamToLogger(logger, logging.DEBUG),
                              warning=StreamToLogger(logger, logging.DEBUG))
        if self.name is not None:
            docfile = osp.abspath(osp.join(self.outdir, self.name + '.rst'))
            if docfile == self.index_file:
                self.name += '1'
                docfile = osp.abspath(osp.join(self.outdir,
                                               self.name + '.rst'))
            html_file = osp.abspath(
                osp.join(self.outdir, '_build', 'html', self.name + '.html'))
            if not osp.exists(docfile):
                with open(self.index_file, 'a') as f:
                    f.write('\n    ' + self.name)
            with open(docfile, 'w') as f:
                f.write(self.doc)
        else:
            html_file = osp.abspath(
                osp.join(self.outdir, '_build', 'html', 'psyplot.html'))
        try:
            self.app.build(None, [])
        except Exception:
            msg = 'Error while building sphinx document %s' % (self.name)
            self.html_error.emit('<b>' + msg + '</b>')
            logger.debug(msg)
        else:
            self.html_ready.emit(file2html(html_file))
class PlotterList(QListWidget):
    """QListWidget showing multiple ArrayItems of one Plotter class"""

    #: str. The name of the attribute of the :class:`psyplot.project.Project`
    #: class
    project_attribute = None

    #: boolean. True if the current project does not contain any arrays in the
    #: attribute identified by the :attr:`project_attribute`
    is_empty = True

    _no_project_update = _TempBool()

    updated_from_project = QtCore.pyqtSignal(QListWidget)

    # Determine whether the plotter could be loaded
    can_import_plotter = True

    @property
    def arrays(self):
        """List of The InteractiveBase instances in this list"""
        return ArrayList([
            getattr(item.arr(), 'arr', item.arr()) for item in self.array_items
        ])

    @property
    def array_items(self):
        """Iterable of :class:`ArrayItem` items in this list"""
        return filter(lambda i: i is not None,
                      map(self.item, range(self.count())))

    def __init__(self, plotter_type=None, *args, **kwargs):
        """
        Parameters
        ----------
        plotter_type: str or None
            If str, it mus be an attribute name of the
            :class:`psyplot.project.Project` class. Otherwise the full project
            is used
        ``*args,**kwargs``
            Are determined by the parent class

        Notes
        -----
        When initialized, the content of the list is determined by
        ``gcp(True)`` and ``gcp()``"""
        super(PlotterList, self).__init__(*args, **kwargs)
        self.project_attribute = plotter_type
        self.setSelectionMode(QAbstractItemView.MultiSelection)
        self.itemSelectionChanged.connect(self.update_cp)
        self.update_from_project(gcp(True))
        self.update_from_project(gcp())

    def update_from_project(self, project):
        """Update the content from the given Project

        Parameters
        ----------
        project: psyplot.project.Project
            If the project is a main project, new items will be added.
            Otherwise only the current selection changes"""
        if self._no_project_update:
            return
        if not self.can_import_plotter:
            # remove the current items
            self.disconnect_items()
            return
        attr = self.project_attribute
        # stop if the module of the plotter has not yet been imported
        if attr and Project._registered_plotters[attr][0] not in sys.modules:
            return
        try:
            arrays = project if not attr else getattr(project, attr)
            mp = gcp(True) if project is None else project.main
            main_arrays = mp if not attr else getattr(mp, attr)
        except ImportError:  # plotter could not be loaded
            self.is_empty = True
            self.can_import_plotter = False
            return
        self.is_empty = not bool(main_arrays)
        with self._no_project_update:
            if project is None:
                for item in self.array_items:
                    item.setSelected(False)
            elif project.is_main:
                old_arrays = self.arrays
                # remove outdated items
                i = 0
                for arr in old_arrays:
                    if arr not in arrays:
                        item = self.takeItem(i)
                        item.disconnect_from_array()
                    else:
                        i += 1
                # add new items
                for arr in arrays:
                    if arr not in old_arrays:
                        item = ArrayItem(weakref.ref(arr.psy), parent=self)
                        self.addItem(item)
                # resort to match the project
                for arr in reversed(main_arrays):
                    for i, item in enumerate(self.array_items):
                        if item.arr() is arr.psy:
                            self.insertItem(0, self.takeItem(i))
            cp = gcp()
            for item in self.array_items:
                item.setSelected(getattr(item.arr(), 'arr', item.arr()) in cp)
        self.updated_from_project.emit(self)

    def update_cp(self, *args, **kwargs):
        """Update the current project from what is selected in this list"""
        if not self._no_project_update:
            mp = gcp(True)
            sp = gcp()
            selected = [item.arr().arr_name for item in self.selectedItems()]
            arrays = self.arrays
            other_selected = [
                arr.psy.arr_name for arr in sp if arr not in arrays
            ]
            with self._no_project_update:
                scp(mp(arr_name=selected + other_selected))

    def disconnect_items(self):
        """Disconnect the items in this list from the arrays"""
        for item in list(self.array_items):
            item.disconnect_from_array()
            self.takeItem(self.indexFromItem(item).row())
        self.is_empty = True
Exemple #20
0
 def rowCount(self, index=QtCore.QModelIndex()):
     """DataFrame row number"""
     if self.total_rows <= self.rows_loaded:
         return self.total_rows
     else:
         return self.rows_loaded
Exemple #21
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)
Exemple #22
0
 def columnCount(self, index=QtCore.QModelIndex()):
     return len(self._column_names) + 1
Exemple #23
0
 def columnCount(self, index=QtCore.QModelIndex()):
     """The number of rows in the table"""
     return len(self.axes) + 1
Exemple #24
0
 def rowCount(self, index=QtCore.QModelIndex()):
     """The number of rows in the table"""
     return len(self.marks)
Exemple #25
0
class LoadFromConsoleButton(QToolButton):
    """A toolbutton to load an object from the console"""

    #: The signal that is emitted when an object has been loaded. The first
    #: argument is the object name, the second the object itself
    object_loaded = QtCore.pyqtSignal(str, object)

    @property
    def instances2check_str(self):
        return ', '.join('%s.%s' % (cls.__module__, cls.__name__)
                         for cls in self._instances2check)

    @property
    def potential_object_names(self):
        from ipykernel.inprocess.ipkernel import InProcessInteractiveShell
        shell = InProcessInteractiveShell.instance()
        return sorted(name for name, obj in shell.user_global_ns.items()
                      if not name.startswith('_') and self.check(obj))

    def __init__(self, instances=None, *args, **kwargs):
        """
        Parameters
        ----------
        instances: class or tuple of classes
            The classes that should be used for an instance check
        """
        super(LoadFromConsoleButton, self).__init__(*args, **kwargs)
        self.setIcon(QIcon(get_icon('console-go.png')))
        if instances is not None and inspect.isclass(instances):
            instances = (instances, )
        self._instances2check = instances
        self.error_msg = PyErrorMessage(self)
        self.clicked.connect(partial(self.get_from_shell, None))

    def check(self, obj):
        return True if not self._instances2check else isinstance(
            obj, self._instances2check)

    def get_from_shell(self, oname=None):
        """Open an input dialog, receive an object and emit the
        :attr:`object_loaded` signal"""
        if oname is None:
            oname, ok = QInputDialog.getItem(
                self, 'Select variable',
                'Select a variable to import from the console',
                self.potential_object_names)
            if not ok:
                return
        if self.check(oname) and (self._instances2check
                                  or not isinstance(oname, six.string_types)):
            obj = oname
            oname = 'object'
        else:
            found, obj = self.get_obj(oname.strip())
            if found:
                if not self.check(obj):
                    self.error_msg.showMessage(
                        'Object must be an instance of %r, not %r' %
                        (self.instances2check_str, '%s.%s' %
                         (type(obj).__module__, type(obj).__name__)))
                    return
            else:
                if not oname.strip():
                    msg = 'The variable name must not be empty!'
                else:
                    msg = 'Could not find object ' + oname
                self.error_msg.showMessage(msg)
                return
        self.object_loaded.emit(oname, obj)

    def get_obj(self, oname):
        """Load an object from the current shell"""
        from psyplot_gui.main import mainwindow
        return mainwindow.console.get_obj(oname)
Exemple #26
0
class StraditizerWidgets(QWidget, DockMixin):
    """A widget that contains widgets to control the straditization in a GUI

    This widget is the basis of the straditize GUI and implemented as a
    plugin into the psyplot gui. The open straditizers are handled in the
    :attr:`_straditizer` attribute.

    The central parts of this widget are

    - The combobox to manage the open straditizers
    - The QTreeWidget in the :attr:`tree` attribute that contains all the
      controls to interface the straditizer
    - the tutorial area
    - the :guilabel:`Apply` and :guilabel:`Cancel` button"""

    #: Boolean that is True if all dialogs should be answered with `Yes`
    always_yes = False

    #: The QTreeWidget that contains the different widgets for the digitization
    tree = None

    #: The apply button
    apply_button = None

    #: The cancel button
    cancel_button = None

    #: The button to edit the straditizer attributes
    attrs_button = None

    #: The button to start a tutorial
    tutorial_button = None

    #: An :class:`InfoButton` to display the docs
    info_button = None

    #: A QComboBox to select the current straditizer
    stradi_combo = None

    #: A button to open a new straditizer
    btn_open_stradi = None

    #: A button to close the current straditizer
    btn_close_stradi = None

    #: A button to reload the last autosaved state
    btn_reload_autosaved = None

    #: The :class:`straditize.widgets.progress_widget.ProgressWidget` to
    #: display the progress of the straditization
    progress_widget = None

    #: The :class:`straditize.widgets.data.DigitizingControl` to interface
    #: the :straditize.straditizer.Straditizer.data_reader`
    digitizer = None

    #: The :class:`straditize.widgets.colnames.ColumnNamesManager` to interface
    #: the :straditize.straditizer.Straditizer.colnames_reader`
    colnames_manager = None

    #: The :class:`straditize.widgets.axes_translations.AxesTranslations` to
    #: handle the y- and x-axis conversions
    axes_translations = None

    #: The :class:`straditize.widgets.image_correction.ImageRescaler` class to
    #: rescale the image
    image_rescaler = None

    #: The :class:`straditize.widgets.image_correction.ImageRotator` class to
    #: rotate the image
    image_rotator = None

    #: The :class:`straditize.widgets.plots.PlotControl` to display additional
    #: information on the diagram
    plot_control = None

    #: The :class:`straditize.widgets.marker_control.MarkerControl` to modify
    #: the appearance of the :class:`~straditize.straditizer.Straditizer.marks`
    #: of the current straditizer
    marker_control = None

    #: The :class:`straditize.widgets.selection_toolbar.SelectionToolbar` to
    #: select features in the stratigraphic diagram
    selection_toolbar = None

    #: The :class:`straditize.straditizer.Straditizer` instance
    straditizer = None

    #: open straditizers
    _straditizers = []

    #: The :class:`straditize.widgets.tutorial.Tutorial` class
    tutorial = None

    dock_position = Qt.LeftDockWidgetArea

    #: Auto-saved straditizers
    autosaved = []

    hidden = True

    title = 'Stratigraphic diagram digitization'

    window_layout_action = None

    open_external = QtCore.pyqtSignal(list)

    def __init__(self, *args, **kwargs):
        from straditize.widgets.menu_actions import StraditizerMenuActions
        from straditize.widgets.progress_widget import ProgressWidget
        from straditize.widgets.data import DigitizingControl
        from straditize.widgets.selection_toolbar import SelectionToolbar
        from straditize.widgets.marker_control import MarkerControl
        from straditize.widgets.plots import PlotControl
        from straditize.widgets.axes_translations import AxesTranslations
        from straditize.widgets.image_correction import (ImageRotator,
                                                         ImageRescaler)
        from straditize.widgets.colnames import ColumnNamesManager
        self._straditizers = []
        super(StraditizerWidgets, self).__init__(*args, **kwargs)
        self.tree = QTreeWidget(parent=self)
        self.tree.setSelectionMode(QTreeWidget.NoSelection)
        self.refresh_button = QToolButton(self)
        self.refresh_button.setIcon(QIcon(get_psy_icon('refresh.png')))
        self.refresh_button.setToolTip('Refresh from the straditizer')
        self.apply_button = EnableButton('Apply', parent=self)
        self.cancel_button = EnableButton('Cancel', parent=self)
        self.attrs_button = QPushButton('Attributes', parent=self)
        self.tutorial_button = QPushButton('Tutorial', parent=self)
        self.tutorial_button.setCheckable(True)
        self.error_msg = PyErrorMessage(self)
        self.stradi_combo = QComboBox()
        self.btn_open_stradi = QToolButton()
        self.btn_open_stradi.setIcon(QIcon(get_psy_icon('run_arrow.png')))
        self.btn_close_stradi = QToolButton()
        self.btn_close_stradi.setIcon(QIcon(get_psy_icon('invalid.png')))
        self.btn_reload_autosaved = QPushButton("Reload")
        self.btn_reload_autosaved.setToolTip(
            "Close the straditizer and reload the last autosaved project")

        # ---------------------------------------------------------------------
        # --------------------------- Tree widgets ----------------------------
        # ---------------------------------------------------------------------
        self.tree.setHeaderLabels(['', ''])
        self.tree.setColumnCount(2)

        self.progress_item = QTreeWidgetItem(0)
        self.progress_item.setText(0, 'ToDo list')
        self.progress_widget = ProgressWidget(self, self.progress_item)

        self.menu_actions_item = QTreeWidgetItem(0)
        self.menu_actions_item.setText(0, 'Images import/export')
        self.tree.addTopLevelItem(self.menu_actions_item)
        self.menu_actions = StraditizerMenuActions(self)

        self.digitizer_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Digitization control')
        self.digitizer = DigitizingControl(self, item)

        self.col_names_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Column names')
        self.colnames_manager = ColumnNamesManager(self, item)
        self.add_info_button(item, 'column_names.rst')

        self.axes_translations_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Axes translations')
        self.axes_translations = AxesTranslations(self, item)

        self.image_transform_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Transform source image')

        self.image_rescaler = ImageRescaler(self, item)

        self.image_rotator_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Rotate image')
        self.image_rotator = ImageRotator(self)
        self.image_transform_item.addChild(item)
        self.image_rotator.setup_children(item)

        self.plot_control_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Plot control')
        self.plot_control = PlotControl(self, item)
        self.add_info_button(item, 'plot_control.rst')

        self.marker_control_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Marker control')
        self.marker_control = MarkerControl(self, item)
        self.add_info_button(item, 'marker_control.rst')

        # ---------------------------------------------------------------------
        # ----------------------------- Toolbars ------------------------------
        # ---------------------------------------------------------------------
        self.selection_toolbar = SelectionToolbar(self, 'Selection toolbar')

        # ---------------------------------------------------------------------
        # ----------------------------- InfoButton ----------------------------
        # ---------------------------------------------------------------------
        self.info_button = InfoButton(self, get_doc_file('straditize.rst'))

        # ---------------------------------------------------------------------
        # --------------------------- Layouts ---------------------------------
        # ---------------------------------------------------------------------

        stradi_box = QHBoxLayout()
        stradi_box.addWidget(self.stradi_combo, 1)
        stradi_box.addWidget(self.btn_open_stradi)
        stradi_box.addWidget(self.btn_close_stradi)

        attrs_box = QHBoxLayout()
        attrs_box.addWidget(self.attrs_button)
        attrs_box.addStretch(0)
        attrs_box.addWidget(self.tutorial_button)

        btn_box = QHBoxLayout()
        btn_box.addWidget(self.refresh_button)
        btn_box.addWidget(self.info_button)
        btn_box.addStretch(0)
        btn_box.addWidget(self.apply_button)
        btn_box.addWidget(self.cancel_button)

        reload_box = QHBoxLayout()
        reload_box.addWidget(self.btn_reload_autosaved)
        reload_box.addStretch(0)

        vbox = QVBoxLayout()
        vbox.addLayout(stradi_box)
        vbox.addWidget(self.tree)
        vbox.addLayout(attrs_box)
        vbox.addLayout(btn_box)
        vbox.addLayout(reload_box)

        self.setLayout(vbox)

        self.apply_button.setEnabled(False)
        self.cancel_button.setEnabled(False)
        self.tree.expandItem(self.progress_item)
        self.tree.expandItem(self.digitizer_item)

        # ---------------------------------------------------------------------
        # --------------------------- Connections -----------------------------
        # ---------------------------------------------------------------------
        self.stradi_combo.currentIndexChanged.connect(self.set_current_stradi)
        self.refresh_button.clicked.connect(self.refresh)
        self.attrs_button.clicked.connect(self.edit_attrs)
        self.tutorial_button.clicked.connect(self.start_tutorial)
        self.open_external.connect(self._create_straditizer_from_args)
        self.btn_open_stradi.clicked.connect(
            self.menu_actions.open_straditizer)
        self.btn_close_stradi.clicked.connect(self.close_straditizer)
        self.btn_reload_autosaved.clicked.connect(self.reload_autosaved)

        self.refresh()
        header = self.tree.header()
        header.setStretchLastSection(False)
        header.setSectionResizeMode(0, QHeaderView.Stretch)

    def disable_apply_button(self):
        """Method that is called when the :attr:`cancel_button` is clicked"""
        for w in [self.apply_button, self.cancel_button]:
            try:
                w.clicked.disconnect()
            except TypeError:
                pass
            w.setEnabled(False)
        self.apply_button.setText('Apply')
        self.cancel_button.setText('Cancel')
        self.refresh_button.setEnabled(True)

    def switch_to_straditizer_layout(self):
        """Switch to the straditizer layout

        This method makes this widget visible and stacks it with the psyplot
        content widget"""
        mainwindow = self.dock.parent()
        mainwindow.figures_tree.hide_plugin()
        mainwindow.ds_tree.hide_plugin()
        mainwindow.fmt_widget.hide_plugin()
        self.show_plugin()
        mainwindow.tabifyDockWidget(mainwindow.project_content.dock, self.dock)
        hsize = self.marker_control.sizeHint().width() + 50
        self.menu_actions.setup_shortcuts(mainwindow)
        if with_qt5:
            mainwindow.resizeDocks([self.dock], [hsize], Qt.Horizontal)
            self.tree.resizeColumnToContents(0)
            self.tree.resizeColumnToContents(1)
        self.info_button.click()

    def to_dock(self, main, *args, **kwargs):
        ret = super(StraditizerWidgets, self).to_dock(main, *args, **kwargs)
        if self.menu_actions.window_layout_action is None:
            main.window_layouts_menu.addAction(self.window_layout_action)
            main.callbacks['straditize'] = self.open_external.emit
            main.addToolBar(self.selection_toolbar)
            self.dock.toggleViewAction().triggered.connect(
                self.show_or_hide_toolbar)
            self.menu_actions.setup_menu_actions(main)
            self.menu_actions.setup_children(self.menu_actions_item)
            try:
                main.open_file_options['Straditize project'] = \
                    self.create_straditizer_from_args
            except AttributeError:  # psyplot-gui <= 1.1.0
                pass
        return ret

    def show_or_hide_toolbar(self):
        """Show or hide the toolbar depending on the visibility of this widget
        """
        self.selection_toolbar.setVisible(self.is_shown)

    def _create_straditizer_from_args(self, args):
        """A method that is called when the :attr:`psyplot_gui.main.mainwindow`
        receives a 'straditize' callback"""
        self.create_straditizer_from_args(*args)

    def create_straditizer_from_args(self,
                                     fnames,
                                     project=None,
                                     xlim=None,
                                     ylim=None,
                                     full=False,
                                     reader_type='area'):
        """Create a straditizer from the given file name

        This method is called when the :attr:`psyplot_gui.main.mainwindow`
        receives a 'straditize' callback"""
        fname = fnames[0]
        if fname is not None:
            self.menu_actions.open_straditizer(fname)
            stradi = self.straditizer
            if stradi is None:
                return
            if xlim is not None:
                stradi.data_xlim = xlim
            if ylim is not None:
                stradi.data_ylim = ylim
            if xlim is not None or ylim is not None or full:
                if stradi.data_xlim is None:
                    stradi.data_xlim = [0, np.shape(stradi.image)[1]]
                if stradi.data_ylim is None:
                    stradi.data_ylim = [0, np.shape(stradi.image)[0]]
                stradi.init_reader(reader_type)
                stradi.data_reader.digitize()
            self.refresh()
        if not self.is_shown:
            self.switch_to_straditizer_layout()
        return fname is not None

    def start_tutorial(self, state, tutorial_cls=None):
        """Start or stop the tutorial

        Parameters
        ----------
        state: bool
            If False, the tutorial is stopped. Otherwise it is started
        tutorial_cls: straditize.widgets.tutorial.beginner.Tutorial
            The tutorial class to use. If None, it will be asked in a
            QInputDialog"""
        if self.tutorial is not None or not state:
            self.tutorial.close()
            self.tutorial_button.setText('Tutorial')
        elif state:
            if tutorial_cls is None:
                tutorial_cls, ok = QInputDialog.getItem(
                    self,
                    'Start tutorial',
                    "Select the tutorial type",
                    ["Beginner", "Advanced (Hoya del Castillo)"],
                    editable=False)
                if not ok:
                    self.tutorial_button.blockSignals(True)
                    self.tutorial_button.setChecked(False)
                    self.tutorial_button.blockSignals(False)
                    return
                if tutorial_cls == 'Beginner':
                    from straditize.widgets.tutorial import Tutorial
                else:
                    from straditize.widgets.tutorial import (
                        HoyaDelCastilloTutorial as Tutorial)
            else:
                Tutorial = tutorial_cls
            self.tutorial = Tutorial(self)
            self.tutorial_button.setText('Stop tutorial')

    def edit_attrs(self):
        """Edit the attributes of the current straditizer

        This creates a new dataframe editor to edit the
        :attr:`straditize.straditizer.Straditizer.attrs` meta informations"""
        def add_attr(key):
            model = editor.table.model()
            n = len(attrs)
            model.insertRow(n)
            model.setData(model.index(n, 0), key)
            model.setData(model.index(n, 1), '', change_type=six.text_type)

        from psyplot_gui.main import mainwindow
        from straditize.straditizer import common_attributes
        attrs = self.straditizer.attrs
        editor = mainwindow.new_data_frame_editor(attrs,
                                                  'Straditizer attributes')
        editor.table.resizeColumnToContents(1)
        editor.table.horizontalHeader().setVisible(False)
        editor.table.frozen_table_view.horizontalHeader().setVisible(False)
        combo = QComboBox()
        combo.addItems([''] + common_attributes)
        combo.currentTextChanged.connect(add_attr)
        hbox = QHBoxLayout()
        hbox.addWidget(QLabel('Common attributes:'))
        hbox.addWidget(combo)
        hbox.addStretch(0)
        editor.layout().insertLayout(1, hbox)
        return editor, combo

    def refresh(self):
        """Refresh from the straditizer"""
        for i, stradi in enumerate(self._straditizers):
            self.stradi_combo.setItemText(
                i,
                self.get_attr(stradi, 'project_file')
                or self.get_attr(stradi, 'image_file') or '')
        # toggle visibility of close button and attributes button
        enable = self.straditizer is not None
        self.btn_close_stradi.setVisible(enable)
        self.attrs_button.setEnabled(enable)
        # refresh controls
        self.menu_actions.refresh()
        self.progress_widget.refresh()
        self.digitizer.refresh()
        self.selection_toolbar.refresh()
        self.plot_control.refresh()
        self.marker_control.refresh()
        self.axes_translations.refresh()
        if self.tutorial is not None:
            self.tutorial.refresh()
        self.image_rotator.refresh()
        self.image_rescaler.refresh()
        self.colnames_manager.refresh()
        self.btn_reload_autosaved.setEnabled(bool(self.autosaved))

    def get_attr(self, stradi, attr):
        try:
            return stradi.get_attr(attr)
        except KeyError:
            pass

    docstrings.delete_params('InfoButton.parameters', 'parent')

    @docstrings.get_sectionsf('StraditizerWidgets.add_info_button')
    @docstrings.with_indent(8)
    def add_info_button(self,
                        child,
                        fname=None,
                        rst=None,
                        name=None,
                        connections=[]):
        """Add an infobutton to the :attr:`tree` widget

        Parameters
        ----------
        child: QTreeWidgetItem
            The item to which to add the infobutton
        %(InfoButton.parameters.no_parent)s
        connections: list of QPushButtons
            Buttons that should be clicked when the info button is clicked"""
        button = InfoButton(self, fname=fname, rst=rst, name=name)
        self.tree.setItemWidget(child, 1, button)
        for btn in connections:
            btn.clicked.connect(button.click)
        return button

    def raise_figures(self):
        """Raise the figures of the current straditizer in the GUI"""
        from psyplot_gui.main import mainwindow
        if mainwindow.figures and self.straditizer:
            dock = self.straditizer.ax.figure.canvas.manager.window
            dock.widget().show_plugin()
            dock.raise_()
            if self.straditizer.magni is not None:
                dock = self.straditizer.magni.ax.figure.canvas.manager.window
                dock.widget().show_plugin()
                dock.raise_()

    def set_current_stradi(self, i):
        """Set the i-th straditizer to the current one"""
        if not self._straditizers:
            return
        self.straditizer = self._straditizers[i]
        self.menu_actions.set_stradi_in_console()
        block = self.stradi_combo.blockSignals(True)
        self.stradi_combo.setCurrentIndex(i)
        self.stradi_combo.blockSignals(block)
        self.raise_figures()
        self.refresh()
        self.autosaved.clear()

    def _close_stradi(self, stradi):
        """Close the given straditizer and all it's figures"""
        is_current = stradi is self.straditizer
        if is_current:
            self.selection_toolbar.disconnect()
        stradi.close()
        try:
            i = self._straditizers.index(stradi)
        except ValueError:
            pass
        else:
            del self._straditizers[i]
            self.stradi_combo.removeItem(i)
        if is_current and self._straditizers:
            self.stradi_combo.setCurrentIndex(0)
        elif not self._straditizers:
            self.straditizer = None
            self.refresh()
        self.digitizer.digitize_item.takeChildren()
        self.digitizer.btn_digitize.setChecked(False)
        self.digitizer.btn_digitize.setCheckable(False)
        self.digitizer.toggle_txt_tolerance('')

    def close_straditizer(self):
        """Close the current straditizer"""
        self._close_stradi(self.straditizer)

    def close_all_straditizers(self):
        """Close all straditizers"""
        self.selection_toolbar.disconnect()
        for stradi in self._straditizers:
            stradi.close()
        self._straditizers.clear()
        self.straditizer = None
        self.stradi_combo.clear()
        self.digitizer.digitize_item.takeChildren()
        self.digitizer.btn_digitize.setChecked(False)
        self.digitizer.btn_digitize.setCheckable(False)
        self.digitizer.toggle_txt_tolerance('')
        self.refresh()

    def add_straditizer(self, stradi):
        """Add a straditizer to the list of open straditizers"""
        if stradi and stradi not in self._straditizers:
            self._straditizers.append(stradi)
            self.stradi_combo.addItem(' ')
            self.set_current_stradi(len(self._straditizers) - 1)

    def reset_control(self):
        """Reset the GUI of straditize"""
        if getattr(self.selection_toolbar, '_pattern_selection', None):
            self.selection_toolbar._pattern_selection.remove_plugin()
            del self.selection_toolbar._pattern_selection
        if getattr(self.digitizer, '_samples_editor', None):
            self.digitizer._close_samples_fig()
        tb = self.selection_toolbar
        tb.set_label_wand_mode()
        tb.set_rect_select_mode()
        tb.new_select_action.setChecked(True)
        tb.select_action.setChecked(False)
        tb.wand_action.setChecked(False)
        self.disable_apply_button()
        self.close_all_straditizers()
        self.colnames_manager.reset_control()

    def autosave(self):
        """Autosave the current straditizer"""
        self.autosaved = [self.straditizer.to_dataset().copy(True)] + \
            self.autosaved[:4]

    def reload_autosaved(self):
        """Reload the autosaved straditizer and close the old one"""
        from straditize.straditizer import Straditizer
        if not self.autosaved:
            return
        answer = QMessageBox.question(
            self, 'Reload autosave',
            'Shall I reload the last autosaved stage? This will close the '
            'current figures.')
        if answer == QMessageBox.Yes:
            self.close_straditizer()
            stradi = Straditizer.from_dataset(self.autosaved.pop(0))
            self.menu_actions.finish_loading(stradi)
Exemple #27
0
 def sizeHint(self):
     """Reimplemented to use the rowHeight as height"""
     s = super(ColorLabel, self).sizeHint()
     return QtCore.QSize(s.width(), self.rowHeight(0) * self.rowCount())
Exemple #28
0
 def sizeHint(self):
     header = self.horizontalHeader().sizeHint().height()
     s = super(PlotControlTable, self).sizeHint()
     return QtCore.QSize(s.width(),
                         self.rowHeight(0) * self.rowCount() + header)
Exemple #29
0
class ColorLabel(QTableWidget):
    """A QTableWidget with one cell and no headers to just display a color"""

    #: a signal that is emitted with an rgba color if the chosen color changes
    color_changed = QtCore.pyqtSignal(QtGui.QColor)

    #: QtCore.QColor. The current color that is displayed
    color = None

    def __init__(self, color='w', *args, **kwargs):
        """The color to display

        Parameters
        ----------
        color: object
            Either a QtGui.QColor object or a color that can be converted
            to RGBA using the :func:`matplotlib.colors.to_rgba` function"""
        super(ColorLabel, self).__init__(*args, **kwargs)
        self.setColumnCount(1)
        self.setRowCount(1)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.horizontalHeader().setHidden(True)
        self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.verticalHeader().setHidden(True)
        self.verticalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.setEditTriggers(QTableWidget.NoEditTriggers)
        self.setSelectionMode(QTableWidget.NoSelection)
        self.itemClicked.connect(self.select_color)
        self.color_item = QTableWidgetItem()
        self.setItem(0, 0, self.color_item)
        self.adjust_height()
        self.set_color(color)

    def select_color(self, *args):
        """Select a color using :meth:`PyQt5.QtWidgets.QColorDialog.getColor`
        """
        self.set_color(
            QColorDialog.getColor(self.color_item.background().color()))

    def set_color(self, color):
        """Set the color of the label

        This method sets the given `color` as background color for the cell
        and emits the :attr:`color_changed` signal

        Parameters
        ----------
        color: object
            Either a QtGui.QColor object or a color that can be converted
            to RGBA using the :func:`matplotlib.colors.to_rgba` function"""
        color = self._set_color(color)
        self.color_changed.emit(color)

    def _set_color(self, color):
        if not isinstance(color, QtGui.QColor):
            color = QtGui.QColor(*map(int,
                                      np.round(mcol.to_rgba(color)) * 255))
        self.color_item.setBackground(color)
        self.color = color
        return color

    def adjust_height(self):
        """Adjust the height to match the row height"""
        h = self.rowHeight(0) * self.rowCount()
        self.setMaximumHeight(h)
        self.setMinimumHeight(h)

    def sizeHint(self):
        """Reimplemented to use the rowHeight as height"""
        s = super(ColorLabel, self).sizeHint()
        return QtCore.QSize(s.width(), self.rowHeight(0) * self.rowCount())
class RcParamsTree(QTreeWidget):
    """A QTreeWidget that can be used to display a RcParams instance

    This widget is populated by a :class:`psyplot.config.rcsetup.RcParams`
    instance and displays whether the values are valid or not"""

    #: A signal that shall be emitted if the validation state changes
    validChanged = QtCore.pyqtSignal(bool)

    #: A signal that is emitted if changes are propsed. It is either emitted
    #: with the parent of this instance (if this is not None) or with the
    #: instance itself
    propose_changes = QtCore.pyqtSignal(object)

    #: The :class:`~psyplot.config.rcsetup.RcParams` to display
    rc = None

    #: list of :class:`bool`. A boolean for each rcParams key that states
    #: whether the proposed value is valid or not
    valid = []

    value_col = 2

    def __init__(self, rcParams, validators, descriptions, *args, **kwargs):
        """
        Parameters
        ----------
        rcParams: dict
            The dictionary that contains the rcParams
        validators: dict
            A mapping from the `rcParams` key to the validation function for
            the corresponding value
        descriptions: dict
            A mapping from the `rcParams` key to it's description

        See Also
        --------
        psyplot.config.rcsetup.RcParams
        psyplot.config.rcsetup.RcParams.validate
        psyplot.config.rcsetup.RcParams.descriptions
        """
        super(RcParamsTree, self).__init__(*args, **kwargs)
        self.rc = rcParams
        self.validators = validators
        self.descriptions = descriptions
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.open_menu)
        self.setColumnCount(self.value_col + 1)
        self.setHeaderLabels(['RcParams key', '', 'Value'])

    @property
    def is_valid(self):
        """True if all the proposed values in this tree are valid"""
        return all(self.valid)

    @property
    def top_level_items(self):
        """An iterator over the topLevelItems in this tree"""
        return map(self.topLevelItem, range(self.topLevelItemCount()))

    def initialize(self):
        """Fill the items of the :attr:`rc` into the tree"""
        rcParams = self.rc
        descriptions = self.descriptions
        self.valid = [True] * len(rcParams)
        validators = self.validators
        vcol = self.value_col
        for i, (key, val) in enumerate(sorted(rcParams.items())):
            item = QTreeWidgetItem(0)
            item.setText(0, key)
            item.setToolTip(0, key)
            item.setIcon(1, QIcon(get_icon('valid.png')))
            desc = descriptions.get(key)
            if desc:
                item.setText(vcol, desc)
                item.setToolTip(vcol, desc)
            child = QTreeWidgetItem(0)
            item.addChild(child)
            self.addTopLevelItem(item)
            editor = QTextEdit(self)
            # set maximal height of the editor to 3 rows
            editor.setMaximumHeight(4 *
                                    QtGui.QFontMetrics(editor.font()).height())
            editor.setPlainText(yaml.dump(val))
            self.setItemWidget(child, vcol, editor)
            editor.textChanged.connect(
                self.set_icon_func(i, item, validators[key]))
        self.resizeColumnToContents(0)
        self.resizeColumnToContents(1)

    def set_icon_func(self, i, item, validator):
        """Create a function to change the icon of one topLevelItem

        This method creates a function that can be called when the value of an
        item changes to display it's valid state. The returned function changes
        the icon of the given topLevelItem depending on
        whether the proposed changes are valid or not and it modifies the
        :attr:`valid` attribute accordingly

        Parameters
        ----------
        i: int
            The index of the topLevelItem
        item: QTreeWidgetItem
            The topLevelItem
        validator: func
            The validation function

        Returns
        -------
        function
            The function that can be called to set the correct icon"""
        def func():
            editor = self.itemWidget(item.child(0), self.value_col)
            s = asstring(editor.toPlainText())
            try:
                val = yaml.load(s, Loader=yaml.Loader)
            except Exception as e:
                item.setIcon(1, QIcon(get_icon('warning.png')))
                item.setToolTip(1, "Could not parse yaml code: %s" % e)
                self.set_valid(i, False)
                return
            try:
                validator(val)
            except Exception as e:
                item.setIcon(1, QIcon(get_icon('invalid.png')))
                item.setToolTip(1, "Wrong value: %s" % e)
                self.set_valid(i, False)
            else:
                item.setIcon(1, QIcon(get_icon('valid.png')))
                self.set_valid(i, True)
            self.propose_changes.emit(self.parent() or self)

        return func

    def set_valid(self, i, b):
        """Set the validation status

        If the validation status changed compared to the old one, the
        :attr:`validChanged` signal is emitted

        Parameters
        ----------
        i: int
            The index of the topLevelItem
        b: bool
            The valid state of the item
        """
        old = self.is_valid
        self.valid[i] = b
        new = self.is_valid
        if new is not old:
            self.validChanged.emit(new)

    def open_menu(self, position):
        """Open a menu to expand and collapse all items in the tree

        Parameters
        ----------
        position: QPosition
            The position where to open the menu"""
        menu = QMenu()
        expand_all_action = QAction('Expand all', self)
        expand_all_action.triggered.connect(self.expandAll)
        menu.addAction(expand_all_action)
        collapse_all_action = QAction('Collapse all', self)
        collapse_all_action.triggered.connect(self.collapseAll)
        menu.addAction(collapse_all_action)
        menu.exec_(self.viewport().mapToGlobal(position))

    def changed_rc(self, use_items=False):
        """Iterate over the changed rcParams

        Parameters
        ----------
        use_items: bool
            If True, the topLevelItems are used instead of the keys

        Yields
        ------
        QTreeWidgetItem or str
            The item identifier
        object
            The proposed value"""
        def equals(item, key, val, orig):
            return val != orig

        for t in self._get_rc(equals):
            yield t[0 if use_items else 1], t[2]

    def selected_rc(self, use_items=False):
        """Iterate over the selected rcParams

        Parameters
        ----------
        use_items: bool
            If True, the topLevelItems are used instead of the keys

        Yields
        ------
        QTreeWidgetItem or str
            The item identifier
        object
            The proposed value"""
        def is_selected(item, key, val, orig):
            return item.isSelected()

        for t in self._get_rc(is_selected):
            yield t[0 if use_items else 1], t[2]

    def _get_rc(self, filter_func=None):
        """Iterate over the rcParams

        This function applies the given `filter_func` to check whether the
        item should be included or not

        Parameters
        ----------
        filter_func: function
            A function that accepts the following arguments:

            item
                The QTreeWidgetItem
            key
                The rcParams key
            val
                The proposed value
            orig
                The current value

        Yields
        ------
        QTreeWidgetItem
            The corresponding topLevelItem
        str
            The rcParams key
        object
            The proposed value
        object
            The current value
        """
        def no_check(item, key, val, orig):
            return True

        rc = self.rc
        filter_func = filter_func or no_check
        for item in self.top_level_items:
            key = asstring(item.text(0))
            editor = self.itemWidget(item.child(0), self.value_col)
            val = yaml.load(asstring(editor.toPlainText()), Loader=yaml.Loader)
            try:
                val = rc.validate[key](val)
            except:
                pass
            try:
                include = filter_func(item, key, val, rc[key])
            except:
                warn('Could not check state for %s key' % key, RuntimeWarning)
            else:
                if include:
                    yield (item, key, val, rc[key])

    def apply_changes(self):
        """Update the :attr:`rc` with the proposed changes"""
        new = dict(self.changed_rc())
        if new != self.rc:
            self.rc.update(new)

    def select_changes(self):
        """Select all the items that changed comparing to the current rcParams
        """
        for item, val in self.changed_rc(True):
            item.setSelected(True)