Esempio n. 1
0
    def __init__(self, *args, **kwargs):
        self.sphinx_dir = kwargs.pop('sphinx_dir', mkdtemp())
        self.build_dir = osp.join(self.sphinx_dir, '_build', 'html')
        super(UrlHelp, self).__init__(*args, **kwargs)

        self.error_msg = PyErrorMessage(self)
        if with_sphinx:
            self.sphinx_thread = SphinxThread(self.sphinx_dir)
            self.sphinx_thread.html_ready[str].connect(self.browse)
            self.sphinx_thread.html_error[str].connect(
                self.error_msg.showTraceback)
            self.sphinx_thread.html_error[str].connect(logger.debug)
            rcParams.connect('help_explorer.render_docs_parallel',
                             self.reset_sphinx)
            rcParams.connect('help_explorer.use_intersphinx',
                             self.reset_sphinx)
            rcParams.connect('help_explorer.online', self.reset_sphinx)

        else:
            self.sphinx_thread = None

        self.bt_connect_console = QToolButton(self)
        self.bt_connect_console.setCheckable(True)
        if rcParams['console.connect_to_help']:
            self.bt_connect_console.setIcon(
                QIcon(get_icon('ipython_console.png')))
            self.bt_connect_console.click()
        else:
            self.bt_connect_console.setIcon(
                QIcon(get_icon('ipython_console_t.png')))
        self.bt_connect_console.clicked.connect(self.toogle_connect_console)
        rcParams.connect('console.connect_to_help',
                         self.update_connect_console)
        self.toogle_connect_console()

        # menu button with different urls
        self.bt_url_menus = QToolButton(self)
        self.bt_url_menus.setIcon(QIcon(get_icon('docu_button.png')))
        self.bt_url_menus.setToolTip('Browse documentations')
        self.bt_url_menus.setPopupMode(QToolButton.InstantPopup)

        docu_menu = QMenu(self)
        for name, url in six.iteritems(self.doc_urls):

            def to_url(b, url=url):
                self.browse(url)

            action = QAction(name, self)
            action.triggered.connect(to_url)
            docu_menu.addAction(action)
        self.bt_url_menus.setMenu(docu_menu)

        self.button_box.addWidget(self.bt_connect_console)
        self.button_box.addWidget(self.bt_url_menus)
        # toogle the lock again to set the bt_url_menus enabled state
        self.toogle_url_lock()
Esempio n. 2
0
    def __init__(self, straditizer, axes=None, *args, **kwargs):
        """
        Parameters
        ----------
        straditizer: weakref.ref
            The reference to the straditizer
        axes: matplotlib.axes.Axes
            The matplotlib axes corresponding to the marks
        """
        super(MultiCrossMarksEditor, self).__init__(*args, **kwargs)
        self.straditizer = straditizer
        straditizer = straditizer()
        self.error_msg = PyErrorMessage(self)

        #: Plot the reconstructed data
        self.cb_plot_lines = QCheckBox('Plot reconstruction')
        self.cb_plot_lines.setChecked(True)

        # A Checkbox to automatically zoom to the selection
        self.cb_zoom_to_selection = QCheckBox('Zoom to selection')

        # A Checkbox to automaticall hide the other marks
        self.cb_selection_only = QCheckBox('Selection only')

        # A Checkbox to automatically fit the selected cells to the selected
        # data
        self.cb_fit2selection = QCheckBox(
            'Fit selected cells to selected data')
        self.cb_fit2selection.setToolTip(
            'If checked, select cells from the table and click on one of the '
            'plots to update the table with the data at the selected position.'
        )

        # The table to display the DataFrame
        self.table = self.create_view(axes=axes)

        # 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)

        self.btn_save = QPushButton('Save')
        self.btn_save.setToolTip('Save the samples and continue editing')

        # ---------------------------------------------------------------------
        # ------------------------ layout --------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()
        self.top_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.cb_zoom_to_selection)
        hbox.addWidget(self.cb_selection_only)
        hbox.addWidget(self.cb_fit2selection)
        hbox.addWidget(self.cb_plot_lines)
        hbox.addStretch(0)
        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_save)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # ------------------------ Connections --------------------------------
        # ---------------------------------------------------------------------
        self.format_editor.textChanged.connect(self.toggle_fmt_button)
        self.btn_change_format.clicked.connect(self.update_format)
        self.btn_save.clicked.connect(self.save_samples)
        straditizer.mark_added.connect(self.table.model().load_new_marks)
        straditizer.mark_removed.connect(self.table.model().remove_mark)
        self.table.selectionModel().selectionChanged.connect(
            self.maybe_zoom_to_selection)
        self.table.frozen_table_view.selectionModel().selectionChanged.connect(
            self.maybe_zoom_to_selection)
        self.table.selectionModel().selectionChanged.connect(
            self.maybe_show_selection_only)
        self.table.frozen_table_view.selectionModel().selectionChanged.connect(
            self.maybe_show_selection_only)

        self.cb_zoom_to_selection.stateChanged.connect(
            self.toggle_cb_zoom_to_selection)
        self.cb_selection_only.stateChanged.connect(
            self.toggle_cb_selection_only)
        self.cb_fit2selection.stateChanged.connect(self.toggle_fit2selection)
        self.cb_plot_lines.stateChanged.connect(self.toggle_plot_lines)

        self.toggle_plot_lines()
Esempio n. 3
0
    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)
Esempio n. 4
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)
Esempio n. 5
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)
Esempio n. 6
0
class MainWindow(QMainWindow):

    #: A signal that is emmitted when the a signal is received through the
    #: open_files_server
    open_external = QtCore.pyqtSignal(list)

    #: The server to open external files
    open_files_server = None

    #: Inprocess console
    console = None

    #: tree widget displaying the open datasets
    ds_tree = None

    #: list of figures from the psyplot backend
    figures = []

    #: tree widget displaying the open figures
    figures_tree = None

    #: general formatoptions widget
    fmt_widget = None

    #: help explorer
    help_explorer = None

    #: the DataFrameEditor widgets, a widget to show and edit data frames
    dataframeeditors = None

    #: tab widget displaying the arrays in current main and sub project
    project_content = None

    #: The dockwidgets of this instance
    dockwidgets = []

    #: default widths of the dock widgets
    default_widths = {}

    _is_open = False

    #: The keyboard shortcuts of the default layout
    default_shortcuts = []

    #: The current keyboard shortcuts
    current_shortcuts = []

    #: The key for the central widget for the main window in the
    #: :attr:`plugins` dictionary
    central_widget_key = 'console'

    @property
    def logger(self):
        """The logger of this instance"""
        return logging.getLogger(
            '%s.%s' % (self.__class__.__module__, self.__class__.__name__))

    @docstrings.get_sections(base='MainWindow')
    @docstrings.dedent
    def __init__(self, show=True):
        """
        Parameters
        ----------
        show: bool
            If True, the created mainwindow is show
        """
        if sys.stdout is None:
            sys.stdout = StreamToLogger(self.logger)
        if sys.stderr is None:
            sys.stderr = StreamToLogger(self.logger)
        super(MainWindow, self).__init__()
        self.setWindowIcon(QIcon(get_icon('logo.png')))

        #: list of figures from the psyplot backend
        self.figures = []
        self.error_msg = PyErrorMessage(self)
        self.setDockOptions(QMainWindow.AnimatedDocks
                            | QMainWindow.AllowNestedDocks
                            | QMainWindow.AllowTabbedDocks)
        #: Inprocess console
        self.console = ConsoleWidget(self)
        self.project_actions = {}

        self.config_pages = []

        self.open_file_options = OrderedDict([
            ('new psyplot plot from dataset', self.open_external_files),
            ('new psyplot project', partial(self.open_external_files, [])),
        ])

        # ---------------------------------------------------------------------
        # ----------------------------- Menus ---------------------------------
        # ---------------------------------------------------------------------

        # ######################## File menu ##################################

        # --------------------------- New plot --------------------------------

        self.file_menu = QMenu('File', parent=self)
        self.new_plot_action = QAction('New plot', self)
        self.new_plot_action.setStatusTip(
            'Use an existing dataset (or open a new one) to create one or '
            'more plots')
        self.register_shortcut(self.new_plot_action, QKeySequence.New)
        self.new_plot_action.triggered.connect(lambda: self.new_plots(True))
        self.file_menu.addAction(self.new_plot_action)

        # --------------------------- Open project ----------------------------

        self.open_project_menu = QMenu('Open project', self)
        self.file_menu.addMenu(self.open_project_menu)

        self.open_mp_action = QAction('New main project', self)
        self.register_shortcut(self.open_mp_action, QKeySequence.Open)
        self.open_mp_action.setStatusTip('Open a new main project')
        self.open_mp_action.triggered.connect(self.open_mp)
        self.open_project_menu.addAction(self.open_mp_action)

        self.open_sp_action = QAction('Add to current', self)

        self.register_shortcut(
            self.open_sp_action,
            QKeySequence('Ctrl+Shift+O', QKeySequence.NativeText))
        self.open_sp_action.setStatusTip(
            'Load a project as a sub project and add it to the current main '
            'project')
        self.open_sp_action.triggered.connect(self.open_sp)
        self.open_project_menu.addAction(self.open_sp_action)

        # ---------------------- load preset menu -----------------------------

        self.load_preset_menu = QMenu('Load preset', parent=self)
        self.file_menu.addMenu(self.load_preset_menu)

        self.load_sp_preset_action = self.load_preset_menu.addAction(
            "For selection", self.load_sp_preset)
        self.load_sp_preset_action.setStatusTip(
            "Load a preset for the selected project")

        self.load_mp_preset_action = self.load_preset_menu.addAction(
            "For full project", self.load_mp_preset)
        self.load_sp_preset_action.setStatusTip(
            "Load a preset for the full project")

        # ----------------------- Save project --------------------------------

        self.save_project_menu = QMenu('Save', parent=self)
        self.file_menu.addMenu(self.save_project_menu)

        self.save_mp_action = QAction('Full psyplot project', self)
        self.save_mp_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.register_shortcut(self.save_mp_action, QKeySequence.Save)
        self.save_mp_action.triggered.connect(self.save_mp)
        self.save_project_menu.addAction(self.save_mp_action)

        self.save_sp_action = QAction('Selected psyplot project', self)
        self.save_sp_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_action.triggered.connect(self.save_sp)
        self.save_project_menu.addAction(self.save_sp_action)

        # ------------------------ Save project as ----------------------------

        self.save_project_as_menu = QMenu('Save as', parent=self)
        self.file_menu.addMenu(self.save_project_as_menu)

        self.save_mp_as_action = QAction('Full psyplot project', self)
        self.save_mp_as_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.register_shortcut(self.save_mp_as_action, QKeySequence.SaveAs)
        self.save_mp_as_action.triggered.connect(
            partial(self.save_mp, new_fname=True))
        self.save_project_as_menu.addAction(self.save_mp_as_action)

        self.save_sp_as_action = QAction('Selected psyplot project', self)
        self.save_sp_as_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_as_action.triggered.connect(
            partial(self.save_sp, new_fname=True))
        self.save_project_as_menu.addAction(self.save_sp_as_action)

        # ------------------------ Save preset --------------------------------

        self.save_preset_menu = QMenu('Save preset', parent=self)
        self.file_menu.addMenu(self.save_preset_menu)

        self.save_sp_preset_action = self.save_preset_menu.addAction(
            "Selection", self.save_sp_preset)
        self.save_sp_preset_action.setStatusTip(
            "Save the formatoptions of the selected project as a preset")

        self.save_mp_preset_action = self.save_preset_menu.addAction(
            "Full project", self.save_mp_preset)
        self.save_sp_preset_action.setStatusTip(
            "Save the formatoptions of the full project as a preset")

        # -------------------------- Pack project -----------------------------

        self.pack_project_menu = QMenu('Zip project files', parent=self)
        self.file_menu.addMenu(self.pack_project_menu)

        self.pack_mp_action = QAction('Full psyplot project', self)
        self.pack_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.pack_mp_action.triggered.connect(partial(self.save_mp, pack=True))
        self.pack_project_menu.addAction(self.pack_mp_action)

        self.pack_sp_action = QAction('Selected psyplot project', self)
        self.pack_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.pack_sp_action.triggered.connect(partial(self.save_sp, pack=True))
        self.pack_project_menu.addAction(self.pack_sp_action)

        # ------------------------ Export figures -----------------------------

        self.export_project_menu = QMenu('Export figures', parent=self)
        self.file_menu.addMenu(self.export_project_menu)

        self.export_mp_action = QAction('Full psyplot project', self)
        self.export_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.export_mp_action.triggered.connect(self.export_mp)
        self.register_shortcut(self.export_mp_action,
                               QKeySequence('Ctrl+E', QKeySequence.NativeText))
        self.export_project_menu.addAction(self.export_mp_action)

        self.export_sp_action = QAction('Selected psyplot project', self)
        self.export_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.register_shortcut(
            self.export_sp_action,
            QKeySequence('Ctrl+Shift+E', QKeySequence.NativeText))
        self.export_sp_action.triggered.connect(self.export_sp)
        self.export_project_menu.addAction(self.export_sp_action)

        # ------------------------ Close project ------------------------------

        self.file_menu.addSeparator()

        self.close_project_menu = QMenu('Close project', parent=self)
        self.file_menu.addMenu(self.close_project_menu)

        self.close_mp_action = QAction('Full psyplot project', self)
        self.register_shortcut(
            self.close_mp_action,
            QKeySequence('Ctrl+Shift+W', QKeySequence.NativeText))
        self.close_mp_action.setStatusTip(
            'Close the main project and delete all data and plots out of '
            'memory')
        self.close_mp_action.triggered.connect(
            lambda: psy.close(psy.gcp(True).num))
        self.close_project_menu.addAction(self.close_mp_action)

        self.close_sp_action = QAction('Selected psyplot project', self)
        self.close_sp_action.setStatusTip(
            'Close the selected arrays project and delete all data and plots '
            'out of memory')
        self.register_shortcut(self.close_sp_action, QKeySequence.Close)
        self.close_sp_action.triggered.connect(
            lambda: psy.gcp().close(True, True))
        self.close_project_menu.addAction(self.close_sp_action)

        # ----------------------------- Quit ----------------------------------

        if sys.platform != 'darwin':  # mac os makes this anyway
            self.quit_action = QAction('Quit', self)
            self.quit_action.triggered.connect(self.close)
            self.quit_action.triggered.connect(
                QtCore.QCoreApplication.instance().quit)
            self.register_shortcut(self.quit_action, QKeySequence.Quit)
            self.file_menu.addAction(self.quit_action)

        self.menuBar().addMenu(self.file_menu)

        # ######################## Console menu ###############################

        self.console_menu = QMenu('Console', self)
        self.console_menu.addActions(self.console.actions())
        self.menuBar().addMenu(self.console_menu)

        # ######################## Windows menu ###############################

        self.windows_menu = QMenu('Windows', self)
        self.menuBar().addMenu(self.windows_menu)

        # ############################ Help menu ##############################

        self.help_menu = QMenu('Help', parent=self)
        self.menuBar().addMenu(self.help_menu)

        # -------------------------- Preferences ------------------------------

        self.help_action = QAction('Preferences', self)
        self.help_action.triggered.connect(lambda: self.edit_preferences(True))
        self.register_shortcut(self.help_action, QKeySequence.Preferences)
        self.help_menu.addAction(self.help_action)

        # ---------------------------- About ----------------------------------

        self.about_action = QAction('About', self)
        self.about_action.triggered.connect(self.about)
        self.help_menu.addAction(self.about_action)

        # ---------------------------- Dependencies ---------------------------

        self.dependencies_action = QAction('Dependencies', self)
        self.dependencies_action.triggered.connect(
            lambda: self.show_dependencies(True))
        self.help_menu.addAction(self.dependencies_action)

        self.dockwidgets = []

        # ---------------------------------------------------------------------
        # -------------------------- Dock windows -----------------------------
        # ---------------------------------------------------------------------
        #: tab widget displaying the arrays in current main and sub project
        #: tree widget displaying the open datasets
        self.project_content = ProjectContentWidget(parent=self)
        self.ds_tree = DatasetTree(parent=self)
        #: tree widget displaying the open figures
        self.figures_tree = FiguresTree(parent=self)
        #: help explorer
        self.help_explorer = help_explorer = HelpExplorer(parent=self)
        if 'HTML help' in help_explorer.viewers and help_explorer.viewers[
                'HTML help'].sphinx_thread is not None:
            help_explorer.viewers[
                'HTML help'].sphinx_thread.html_ready.connect(
                    self.focus_on_console)
        #: the DataFrameEditor widgets
        self.dataframeeditors = []
        #: general formatoptions widget
        self.fmt_widget = FormatoptionWidget(parent=self,
                                             help_explorer=help_explorer,
                                             console=self.console)

        # load plugin widgets
        self.plugins = plugins = OrderedDict([
            ('console', self.console),
            ('project_content', self.project_content),
            ('ds_tree', self.ds_tree),
            ('figures_tree', self.figures_tree),
            ('help_explorer', self.help_explorer),
            ('fmt_widget', self.fmt_widget),
        ])
        self.default_plugins = list(plugins)
        for plugin_name, w_class in six.iteritems(rcParams.load_plugins()):
            plugins[plugin_name] = w_class(parent=self)

        self.add_mp_to_menu()
        psy.Project.oncpchange.connect(self.eventually_add_mp_to_menu)
        self.windows_menu.addSeparator()

        self.window_layouts_menu = QMenu('Window layouts', self)
        self.restore_layout_action = QAction('Restore default layout', self)
        self.restore_layout_action.triggered.connect(self.setup_default_layout)
        self.window_layouts_menu.addAction(self.restore_layout_action)
        self.windows_menu.addMenu(self.window_layouts_menu)

        self.panes_menu = QMenu('Panes', self)
        self.windows_menu.addMenu(self.panes_menu)

        self.dataframe_menu = QMenu('DataFrame editors', self)
        self.dataframe_menu.addAction(
            'New Editor',
            partial(self.new_data_frame_editor, None, 'DataFrame Editor'))
        self.dataframe_menu.addSeparator()
        self.windows_menu.addMenu(self.dataframe_menu)

        self.central_widgets_menu = menu = QMenu('Central widget', self)
        self.windows_menu.addMenu(menu)
        self.central_widgets_actions = group = QActionGroup(self)
        group.setExclusive(True)

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

        self.console.help_explorer = help_explorer
        psyp.default_print_func = partial(help_explorer.show_rst,
                                          oname='formatoption_docs')
        psy.PlotterInterface._print_func = psyp.default_print_func
        self.setCentralWidget(self.console)

        # make sure that the plots are shown between the project content and
        # the help explorer widget
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)

        # make sure that the formatoption widgets are shown between the
        # project content and the help explorer widget
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)

        # ---------------------------------------------------------------------
        # ------------------------------ closure ------------------------------
        # ---------------------------------------------------------------------
        if show:
            self.help_explorer.show_intro(self.console.intro_msg)

        # ---------------------------------------------------------------------
        # ------------------------- open_files_server -------------------------
        # ---------------------------------------------------------------------
        self.callbacks = {
            'new_plot': self.open_external.emit,
            'change_cwd': self._change_cwd,
            'run_script': self.console.run_script.emit,
            'command': self.console.run_command.emit,
        }

        # Server to open external files on a single instance
        self.open_files_server = socket.socket(socket.AF_INET,
                                               socket.SOCK_STREAM,
                                               socket.IPPROTO_TCP)

        if rcParams['main.listen_to_port']:
            self._file_thread = Thread(target=self.start_open_files_server)
            self._file_thread.setDaemon(True)
            self._file_thread.start()

            self.open_external.connect(self._open_external_files)

        self.config_pages.extend([GuiRcParamsWidget, PsyRcParamsWidget])

        # display the statusBar
        statusbar = self.statusBar()
        self.figures_label = QLabel()
        statusbar.addWidget(self.figures_label)
        self.plugin_label = QLabel()
        statusbar.addWidget(self.plugin_label)

        self.default_widths = {}

        self.setup_default_layout()

        if show:
            self.showMaximized()

        # save the default widths after they have been shown
        for w in self.plugins.values():
            if w.dock is not None:
                self.default_widths[w] = w.dock.size().width()

        # hide plugin widgets that should be hidden at startup. Although this
        # has been executed by :meth:`setup_default_layout`, we have to execute
        # it again after the call of showMaximized
        for name, w in self.plugins.items():
            if name != self.central_widget_key:
                w.to_dock(self)
                if w.hidden:
                    w.hide_plugin()
            else:
                w.create_central_widget_action(self).setChecked(True)

        self._is_open = True

    def focus_on_console(self, *args, **kwargs):
        """Put focus on the ipython console"""
        self.console._control.setFocus()

    def new_data_frame_editor(self, df=None, title='DataFrame Editor'):
        """Open a new dataframe editor

        Parameters
        ----------
        df: pandas.DataFrame
            The dataframe to display
        title: str
            The title of the dock window

        Returns
        -------
        psyplot_gui.dataframeeditor.DataFrameEditor
            The newly created editor"""
        editor = DataFrameEditor()
        self.dataframeeditors.append(editor)
        editor.to_dock(self, title, Qt.RightDockWidgetArea, docktype='df')
        if df is not None:
            editor.set_df(df)
        editor.show_plugin()
        editor.maybe_tabify()
        editor.raise_()
        return editor

    def setup_default_layout(self):
        """Set up the default window layout"""
        self.project_content.to_dock(self, 'Plot objects',
                                     Qt.LeftDockWidgetArea)
        self.ds_tree.to_dock(self, 'Datasets', Qt.LeftDockWidgetArea)
        self.figures_tree.to_dock(self, 'Figures', Qt.LeftDockWidgetArea)
        self.help_explorer.to_dock(self, 'Help explorer',
                                   Qt.RightDockWidgetArea)
        self.fmt_widget.to_dock(self, 'Formatoptions', Qt.BottomDockWidgetArea)

        modify_widths = bool(self.default_widths)
        for w in map(self.plugins.__getitem__, self.default_plugins):
            if w.dock is not None:
                w.show_plugin()

                if modify_widths and with_qt5:
                    self.resizeDocks([w.dock], [self.default_widths[w]],
                                     Qt.Horizontal)

        # hide plugin widgets that should be hidden at startup
        for name, w in self.plugins.items():
            if name != self.central_widget_key:
                w.to_dock(self)
                if w.hidden:
                    w.hide_plugin()

        action2shortcut = defaultdict(list)
        for s, a in self.default_shortcuts:
            action2shortcut[a].append(s)

        for a, s in action2shortcut.items():
            self.register_shortcut(a, s)

    def set_central_widget(self, name):
        """Set the central widget

        Parameters
        ----------
        name: str or QWidget
            The key or the plugin widget in the :attr:`plugins` dictionary"""
        from PyQt5.QtCore import QTimer
        self.setUpdatesEnabled(False)
        current = self.centralWidget()
        if isinstance(name, six.string_types):
            new = self.plugins[name]
        else:
            new = name
            name = next(key for key, val in self.plugins.items() if val is new)
        if new is not current:

            self._dock_widths = dock_widths = OrderedDict()
            self._dock_heights = dock_heights = OrderedDict()
            for key, w in self.plugins.items():
                if w.dock is not None and w.is_shown:
                    s = w.dock.size()
                    dock_widths[w] = s.width()
                    if w is not new:
                        dock_heights[w] = s.height()

            new_pos = self.dockWidgetArea(new.dock)

            self.removeDockWidget(new.dock)
            new.dock.close()
            self.panes_menu.removeAction(new._view_action)
            self.dataframe_menu.removeAction(new._view_action)
            new.dock = new._view_action = None
            self.central_widget_key = name
            current.to_dock(self)
            self.setCentralWidget(new)
            new._set_central_action.setChecked(True)
            current.show_plugin()
            current.to_dock(self)
            new_width = dock_widths.pop(new, None)
            if current.hidden:
                current.hide_plugin()
            else:
                current_pos = self.dockWidgetArea(current.dock)
                if current_pos == new_pos and new_width:
                    dock_widths[current] = new_width

        self._custom_layout_timer = QTimer(self)
        self._custom_layout_timer.timeout.connect(self._reset_dock_widths)
        self._custom_layout_timer.setSingleShot(True)
        self._custom_layout_timer.start(5000)

    def _reset_dock_widths(self):
        # resize the plugins
        if with_qt5:
            for w, width in self._dock_widths.items():
                if w.dock is not None:
                    self.resizeDocks([w.dock], [width], Qt.Horizontal)
            for w, height in self._dock_heights.items():
                if w.dock is not None:
                    self.resizeDocks([w.dock], [height], Qt.Vertical)

        self.setUpdatesEnabled(True)

    def _save_project(self, p, new_fname=False, *args, **kwargs):
        if new_fname or 'project_file' not in p.attrs:
            fname = QFileDialog.getSaveFileName(
                self, 'Project destination', os.getcwd(),
                'Pickle files (*.pkl);;'
                'All files (*)')
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
            if not fname:
                return
        else:
            fname = p.attrs['project_file']
        try:
            p.save_project(fname, *args, **kwargs)
        except Exception:
            self.error_msg.showTraceback('<b>Could not save the project!</b>')
        else:
            p.attrs['project_file'] = fname
            if p.is_main:
                self.update_project_action(p.num)

    def load_mp_preset(self):
        self._load_preset(psy.gcp(True))

    def load_sp_preset(self):
        self._load_preset(psy.gcp())

    def _load_preset(self, project, *args, **kwargs):
        fname, ok = QFileDialog.getOpenFileName(
            self, 'Load preset', os.path.join(get_configdir(), "presets"),
            'YAML files (*.yml *.yaml);;'
            'All files (*)')
        if ok:
            project.load_preset(fname, *args, **kwargs)

    def open_mp(self, *args, **kwargs):
        """Open a new main project"""
        self._open_project(main=True)

    def open_sp(self, *args, **kwargs):
        """Open a subproject and add it to the current main project"""
        self._open_project(main=False)

    def _open_project(self, *args, **kwargs):
        fname = QFileDialog.getOpenFileName(
            self, 'Project file', os.getcwd(), 'Pickle files (*.pkl);;'
            'All files (*)')
        if with_qt5:  # the filter is passed as well
            fname = fname[0]
        if not fname:
            return
        p = psy.Project.load_project(fname, *args, **kwargs)
        p.attrs['project_file'] = fname
        self.update_project_action(p.num)

    def save_mp(self, *args, **kwargs):
        """Save the current main project."""
        self._save_project(psy.gcp(True), **kwargs)

    def save_sp(self, *args, **kwargs):
        """Save the current sub project."""
        self._save_project(psy.gcp(), **kwargs)

    def save_sp_preset(self):
        self._save_preset(psy.gcp())

    def save_mp_preset(self):
        self._save_preset(psy.gcp(True))

    def _save_preset(self, project, *args, **kwargs):
        fname, ok = QFileDialog.getSaveFileName(
            self, 'Save preset', os.path.join(get_configdir(), 'presets'),
            'YAML file (*.yml *.yaml);;'
            'All files (*)')
        if ok:
            project.save_preset(fname, *args, **kwargs)

    def _export_project(self, p, *args, **kwargs):
        fname = QFileDialog.getSaveFileName(
            self, 'Picture destination', os.getcwd(), 'PDF files (*.pdf);;'
            'Postscript file (*.ps);;'
            'PNG image (*.png);;'
            'JPG image (*.jpg *.jpeg);;'
            'TIFF image (*.tif *.tiff);;'
            'GIF image (*.gif);;'
            'All files (*)')
        if with_qt5:  # the filter is passed as well
            fname = fname[0]
        if not fname:
            return
        try:
            p.export(fname, *args, **kwargs)
        except Exception:
            self.error_msg.showTraceback(
                '<b>Could not export the figures!</b>')

    def export_mp(self, *args, **kwargs):
        self._export_project(psy.gcp(True), **kwargs)

    def export_sp(self, *args, **kwargs):
        self._export_project(psy.gcp(), **kwargs)

    def new_plots(self, exec_=None):
        if hasattr(self, 'plot_creator'):
            try:
                self.plot_creator.close()
            except RuntimeError:
                pass
        self.plot_creator = PlotCreator(help_explorer=self.help_explorer,
                                        parent=self)
        available_width = QDesktopWidget().availableGeometry().width() / 3.
        width = self.plot_creator.sizeHint().width()
        height = self.plot_creator.sizeHint().height()
        # The plot creator window should cover at least one third of the screen
        self.plot_creator.resize(max(available_width, width), height)
        if exec_:
            self.plot_creator.exec_()

    def excepthook(self, type, value, traceback):
        """A method to replace the sys.excepthook"""
        self.error_msg.excepthook(type, value, traceback)

    def edit_preferences(self, exec_=None):
        """Edit Spyder preferences"""
        if hasattr(self, 'preferences'):
            try:
                self.preferences.close()
            except RuntimeError:
                pass
        self.preferences = dlg = Prefences(self)
        for PrefPageClass in self.config_pages:
            widget = PrefPageClass(dlg)
            widget.initialize()
            dlg.add_page(widget)
        available_width = int(0.667 *
                              QDesktopWidget().availableGeometry().width())
        width = dlg.sizeHint().width()
        height = dlg.sizeHint().height()
        # The preferences window should cover at least one third of the screen
        dlg.resize(max(available_width, width), height)
        if exec_:
            dlg.exec_()

    def about(self):
        """About the tool"""
        versions = {
            key: d['version']
            for key, d in psyplot.get_versions(False).items()
        }
        versions.update(psyplot_gui.get_versions()['requirements'])
        versions.update(psyplot._get_versions()['requirements'])
        versions['github'] = 'https://github.com/psyplot/psyplot'
        versions['author'] = psyplot.__author__
        QMessageBox.about(
            self, "About psyplot",
            u"""<b>psyplot: Interactive data visualization with python</b>
            <br>Copyright &copy; 2017- Philipp Sommer
            <br>Licensed under the terms of the GNU General Public License v2
            (GPLv2)
            <p>Created by %(author)s</p>
            <p>Most of the icons come from the
            <a href="https://www.iconfinder.com/"> iconfinder</a>.</p>
            <p>For bug reports and feature requests, please go
            to our <a href="%(github)s">Github website</a> or contact the
            author via mail.</p>
            <p>This package uses (besides others) the following packages:<br>
            <ul>
                <li>psyplot %(psyplot)s</li>
                <li>Python %(python)s </li>
                <li>numpy %(numpy)s</li>
                <li>xarray %(xarray)s</li>
                <li>pandas %(pandas)s</li>
                <li>psyplot_gui %(psyplot_gui)s</li>
                <li>Qt %(qt)s</li>
                <li>PyQt %(pyqt)s</li>
                <li>qtconsole %(qtconsole)s</li>
            </ul></p>
            <p>For a full list of requirements see the <em>dependencies</em>
            in the <em>Help</em> menu.</p>
            <p>This software is provided "as is", without warranty or support
            of any kind.</p>""" % versions)

    def show_dependencies(self, exec_=None):
        """Open a dialog that shows the dependencies"""
        if hasattr(self, 'dependencies'):
            try:
                self.dependencies.close()
            except RuntimeError:
                pass
        self.dependencies = dlg = DependenciesDialog(psyplot.get_versions(),
                                                     parent=self)
        dlg.resize(630, 420)
        if exec_:
            dlg.exec_()

    def reset_rcParams(self):
        rcParams.update_from_defaultParams()
        psy.rcParams.update_from_defaultParams()

    def add_mp_to_menu(self):
        mp = psy.gcp(True)
        action = QAction(
            os.path.basename(
                mp.attrs.get('project_file', 'Untitled %s*' % mp.num)), self)
        action.setStatusTip('Make project %s the current project' % mp.num)
        action.triggered.connect(lambda: psy.scp(psy.project(mp.num)))
        self.project_actions[mp.num] = action
        self.windows_menu.addAction(action)

    def update_project_action(self, num):
        action = self.project_actions.get(num)
        p = psy.project(num)
        if action:
            action.setText(
                os.path.basename(
                    p.attrs.get('project_file', 'Untitled %s*' % num)))

    def eventually_add_mp_to_menu(self, p):
        for num in set(self.project_actions).difference(
                psy.get_project_nums()):
            self.windows_menu.removeAction(self.project_actions.pop(num))
        if p is None or not p.is_main:
            return
        if p.num not in self.project_actions:
            self.add_mp_to_menu()

    def start_open_files_server(self):
        """This method listens to the open_files_port and opens the plot
        creator for new files

        This method is inspired and to most parts copied from spyder"""
        self.open_files_server.setsockopt(socket.SOL_SOCKET,
                                          socket.SO_REUSEADDR, 1)
        port = rcParams['main.open_files_port']
        try:
            self.open_files_server.bind(('127.0.0.1', port))
        except Exception:
            return
        self.open_files_server.listen(20)
        while 1:  # 1 is faster than True
            try:
                req, dummy = self.open_files_server.accept()
            except socket.error as e:
                # See Issue 1275 for details on why errno EINTR is
                # silently ignored here.
                eintr = errno.EINTR
                # To avoid a traceback after closing on Windows
                if e.args[0] == eintr:
                    continue
                # handle a connection abort on close error
                enotsock = (errno.WSAENOTSOCK
                            if os.name == 'nt' else errno.ENOTSOCK)
                if e.args[0] in [errno.ECONNABORTED, enotsock]:
                    return
                raise
            args = pickle.loads(req.recv(1024))
            callback = args[0]
            func = self.callbacks[callback]
            self.logger.debug('Emitting %s callback %s', callback, func)
            func(args[1:])
            req.sendall(b' ')

    def change_cwd(self, path):
        """Change the current working directory"""
        import os
        os.chdir(path)

    def _change_cwd(self, args):
        path = args[0][0]
        self.change_cwd(path)

    docstrings.keep_params('make_plot.parameters', 'fnames', 'project',
                           'engine', 'plot_method', 'name', 'dims', 'encoding',
                           'enable_post', 'seaborn_style', 'concat_dim',
                           'chname', 'preset')

    def open_files(self, fnames):
        """Open a file and ask the user how"""
        fnames_s = ', '.join(map(os.path.basename, fnames))
        if len(fnames_s) > 30:
            fnames_s = fnames_s[:27] + '...'
        item, ok = QInputDialog.getItem(self,
                                        'Open file...',
                                        'Open %s as...' % fnames_s,
                                        list(self.open_file_options),
                                        current=0,
                                        editable=False)
        if ok:
            return self.open_file_options[item](fnames)

    @docstrings.get_sections(base='MainWindow.open_external_files')
    @docstrings.dedent
    def open_external_files(self,
                            fnames=[],
                            project=None,
                            engine=None,
                            plot_method=None,
                            name=None,
                            dims=None,
                            encoding=None,
                            enable_post=False,
                            seaborn_style=None,
                            concat_dim=get_default_value(
                                xr.open_mfdataset, 'concat_dim'),
                            chname={},
                            preset=None):
        """
        Open external files

        Parameters
        ----------
        %(make_plot.parameters.fnames|project|engine|plot_method|name|dims|encoding|enable_post|seaborn_style|concat_dim|chname|preset)s
        """
        if seaborn_style is not None:
            import seaborn as sns
            sns.set_style(seaborn_style)
        if project is not None:
            fnames = [s.split(',') for s in fnames]
            if not isinstance(project, dict):
                project = psyd.safe_list(project)[0]
            single_files = (l[0] for l in fnames if len(l) == 1)
            alternative_paths = defaultdict(lambda: next(single_files, None))
            alternative_paths.update(list(l for l in fnames if len(l) == 2))
            p = psy.Project.load_project(project,
                                         alternative_paths=alternative_paths,
                                         engine=engine,
                                         main=not psy.gcp(),
                                         encoding=encoding,
                                         enable_post=enable_post,
                                         chname=chname)
            if preset:
                p.load_preset(preset)
            if isinstance(project, six.string_types):
                p.attrs.setdefault('project_file', project)
            return True
        else:
            self.new_plots(False)
            self.plot_creator.open_dataset(fnames,
                                           engine=engine,
                                           concat_dim=concat_dim)
            if name == 'all':
                ds = self.plot_creator.get_ds()
                name = sorted(set(ds.variables) - set(ds.coords))
            self.plot_creator.insert_array(
                list(filter(None, psy.safe_list(name))))
            if dims is not None:
                ds = self.plot_creator.get_ds()
                dims = {
                    key: ', '.join(map(str, val))
                    for key, val in six.iteritems(dims)
                }
                for i, vname in enumerate(
                        self.plot_creator.array_table.vnames):
                    self.plot_creator.array_table.selectRow(i)
                    self.plot_creator.array_table.update_selected()
                self.plot_creator.array_table.selectAll()
                var = ds[vname[0]]
                self.plot_creator.array_table.update_selected(
                    dims=var.psy.decoder.correct_dims(var, dims.copy()))
            if preset:
                self.plot_creator.set_preset(preset)
            if plot_method:
                self.plot_creator.pm_combo.setCurrentIndex(
                    self.plot_creator.pm_combo.findText(plot_method))
            self.plot_creator.exec_()
            return True

    def _open_external_files(self, args):
        self.open_external_files(*args)

    @classmethod
    @docstrings.get_sections(base='MainWindow.run')
    @docstrings.dedent
    def run(cls,
            fnames=[],
            project=None,
            engine=None,
            plot_method=None,
            name=None,
            dims=None,
            encoding=None,
            enable_post=False,
            seaborn_style=None,
            concat_dim=get_default_value(xr.open_mfdataset, 'concat_dim'),
            chname={},
            preset=None,
            show=True):
        """
        Create a mainwindow and open the given files or project

        This class method creates a new mainwindow instance and sets the
        global :attr:`mainwindow` variable.

        Parameters
        ----------
        %(MainWindow.open_external_files.parameters)s
        %(MainWindow.parameters)s

        Notes
        -----
        - There can be only one mainwindow at the time
        - This method does not create a QApplication instance! See
          :meth:`run_app`

        See Also
        --------
        run_app
        """
        mainwindow = cls(show=show)
        _set_mainwindow(mainwindow)
        if fnames or project:
            mainwindow.open_external_files(fnames, project, engine,
                                           plot_method, name, dims, encoding,
                                           enable_post, seaborn_style,
                                           concat_dim, chname, preset)
        psyplot.with_gui = True
        return mainwindow

    def register_shortcut(self,
                          action,
                          shortcut,
                          context=Qt.ApplicationShortcut):
        """Register an action for a shortcut"""
        shortcuts = psy.safe_list(shortcut)
        for j, shortcut in enumerate(shortcuts):
            found = False
            for i, (s, a) in enumerate(self.current_shortcuts):
                if s == shortcut:
                    new_shortcuts = [
                        sc for sc in self.current_shortcuts[i][1].shortcuts()
                        if sc != s
                    ]
                    a.setShortcut(QKeySequence())
                    if new_shortcuts:
                        a.setShortcuts(new_shortcuts)
                    self.current_shortcuts[i][1] = action
                    found = True
                    break
            if not found:
                self.default_shortcuts.append([shortcut, action])
                self.current_shortcuts.append([shortcut, action])
        action.setShortcuts(shortcuts)
        action.setShortcutContext(context)

    @classmethod
    @docstrings.dedent
    def run_app(cls, *args, **kwargs):
        """
        Create a QApplication, open the given files or project and enter the
        mainloop

        Parameters
        ----------
        %(MainWindow.run.parameters)s

        See Also
        --------
        run
        """
        app = QApplication(sys.argv)
        cls.run(*args, **kwargs)
        sys.exit(app.exec_())

    def closeEvent(self, event):
        """closeEvent reimplementation"""
        if not self._is_open or (self._is_open and self.close()):
            self._is_open = False
            event.accept()

    def close(self):
        _set_mainwindow(None)
        if self.open_files_server is not None:
            self.open_files_server.close()
            del self.open_files_server
        for widget in self.plugins.values():
            widget.close()
        self.plugins.clear()
        return super(MainWindow, self).close()
Esempio n. 7
0
    def __init__(self, show=True):
        """
        Parameters
        ----------
        show: bool
            If True, the created mainwindow is show
        """
        if sys.stdout is None:
            sys.stdout = StreamToLogger(self.logger)
        if sys.stderr is None:
            sys.stderr = StreamToLogger(self.logger)
        super(MainWindow, self).__init__()
        self.setWindowIcon(QIcon(get_icon('logo.png')))

        #: list of figures from the psyplot backend
        self.figures = []
        self.error_msg = PyErrorMessage(self)
        self.setDockOptions(QMainWindow.AnimatedDocks
                            | QMainWindow.AllowNestedDocks
                            | QMainWindow.AllowTabbedDocks)
        #: Inprocess console
        self.console = ConsoleWidget(self)
        self.project_actions = {}

        self.config_pages = []

        self.open_file_options = OrderedDict([
            ('new psyplot plot from dataset', self.open_external_files),
            ('new psyplot project', partial(self.open_external_files, [])),
        ])

        # ---------------------------------------------------------------------
        # ----------------------------- Menus ---------------------------------
        # ---------------------------------------------------------------------

        # ######################## File menu ##################################

        # --------------------------- New plot --------------------------------

        self.file_menu = QMenu('File', parent=self)
        self.new_plot_action = QAction('New plot', self)
        self.new_plot_action.setStatusTip(
            'Use an existing dataset (or open a new one) to create one or '
            'more plots')
        self.register_shortcut(self.new_plot_action, QKeySequence.New)
        self.new_plot_action.triggered.connect(lambda: self.new_plots(True))
        self.file_menu.addAction(self.new_plot_action)

        # --------------------------- Open project ----------------------------

        self.open_project_menu = QMenu('Open project', self)
        self.file_menu.addMenu(self.open_project_menu)

        self.open_mp_action = QAction('New main project', self)
        self.register_shortcut(self.open_mp_action, QKeySequence.Open)
        self.open_mp_action.setStatusTip('Open a new main project')
        self.open_mp_action.triggered.connect(self.open_mp)
        self.open_project_menu.addAction(self.open_mp_action)

        self.open_sp_action = QAction('Add to current', self)

        self.register_shortcut(
            self.open_sp_action,
            QKeySequence('Ctrl+Shift+O', QKeySequence.NativeText))
        self.open_sp_action.setStatusTip(
            'Load a project as a sub project and add it to the current main '
            'project')
        self.open_sp_action.triggered.connect(self.open_sp)
        self.open_project_menu.addAction(self.open_sp_action)

        # ---------------------- load preset menu -----------------------------

        self.load_preset_menu = QMenu('Load preset', parent=self)
        self.file_menu.addMenu(self.load_preset_menu)

        self.load_sp_preset_action = self.load_preset_menu.addAction(
            "For selection", self.load_sp_preset)
        self.load_sp_preset_action.setStatusTip(
            "Load a preset for the selected project")

        self.load_mp_preset_action = self.load_preset_menu.addAction(
            "For full project", self.load_mp_preset)
        self.load_sp_preset_action.setStatusTip(
            "Load a preset for the full project")

        # ----------------------- Save project --------------------------------

        self.save_project_menu = QMenu('Save', parent=self)
        self.file_menu.addMenu(self.save_project_menu)

        self.save_mp_action = QAction('Full psyplot project', self)
        self.save_mp_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.register_shortcut(self.save_mp_action, QKeySequence.Save)
        self.save_mp_action.triggered.connect(self.save_mp)
        self.save_project_menu.addAction(self.save_mp_action)

        self.save_sp_action = QAction('Selected psyplot project', self)
        self.save_sp_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_action.triggered.connect(self.save_sp)
        self.save_project_menu.addAction(self.save_sp_action)

        # ------------------------ Save project as ----------------------------

        self.save_project_as_menu = QMenu('Save as', parent=self)
        self.file_menu.addMenu(self.save_project_as_menu)

        self.save_mp_as_action = QAction('Full psyplot project', self)
        self.save_mp_as_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.register_shortcut(self.save_mp_as_action, QKeySequence.SaveAs)
        self.save_mp_as_action.triggered.connect(
            partial(self.save_mp, new_fname=True))
        self.save_project_as_menu.addAction(self.save_mp_as_action)

        self.save_sp_as_action = QAction('Selected psyplot project', self)
        self.save_sp_as_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_as_action.triggered.connect(
            partial(self.save_sp, new_fname=True))
        self.save_project_as_menu.addAction(self.save_sp_as_action)

        # ------------------------ Save preset --------------------------------

        self.save_preset_menu = QMenu('Save preset', parent=self)
        self.file_menu.addMenu(self.save_preset_menu)

        self.save_sp_preset_action = self.save_preset_menu.addAction(
            "Selection", self.save_sp_preset)
        self.save_sp_preset_action.setStatusTip(
            "Save the formatoptions of the selected project as a preset")

        self.save_mp_preset_action = self.save_preset_menu.addAction(
            "Full project", self.save_mp_preset)
        self.save_sp_preset_action.setStatusTip(
            "Save the formatoptions of the full project as a preset")

        # -------------------------- Pack project -----------------------------

        self.pack_project_menu = QMenu('Zip project files', parent=self)
        self.file_menu.addMenu(self.pack_project_menu)

        self.pack_mp_action = QAction('Full psyplot project', self)
        self.pack_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.pack_mp_action.triggered.connect(partial(self.save_mp, pack=True))
        self.pack_project_menu.addAction(self.pack_mp_action)

        self.pack_sp_action = QAction('Selected psyplot project', self)
        self.pack_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.pack_sp_action.triggered.connect(partial(self.save_sp, pack=True))
        self.pack_project_menu.addAction(self.pack_sp_action)

        # ------------------------ Export figures -----------------------------

        self.export_project_menu = QMenu('Export figures', parent=self)
        self.file_menu.addMenu(self.export_project_menu)

        self.export_mp_action = QAction('Full psyplot project', self)
        self.export_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.export_mp_action.triggered.connect(self.export_mp)
        self.register_shortcut(self.export_mp_action,
                               QKeySequence('Ctrl+E', QKeySequence.NativeText))
        self.export_project_menu.addAction(self.export_mp_action)

        self.export_sp_action = QAction('Selected psyplot project', self)
        self.export_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.register_shortcut(
            self.export_sp_action,
            QKeySequence('Ctrl+Shift+E', QKeySequence.NativeText))
        self.export_sp_action.triggered.connect(self.export_sp)
        self.export_project_menu.addAction(self.export_sp_action)

        # ------------------------ Close project ------------------------------

        self.file_menu.addSeparator()

        self.close_project_menu = QMenu('Close project', parent=self)
        self.file_menu.addMenu(self.close_project_menu)

        self.close_mp_action = QAction('Full psyplot project', self)
        self.register_shortcut(
            self.close_mp_action,
            QKeySequence('Ctrl+Shift+W', QKeySequence.NativeText))
        self.close_mp_action.setStatusTip(
            'Close the main project and delete all data and plots out of '
            'memory')
        self.close_mp_action.triggered.connect(
            lambda: psy.close(psy.gcp(True).num))
        self.close_project_menu.addAction(self.close_mp_action)

        self.close_sp_action = QAction('Selected psyplot project', self)
        self.close_sp_action.setStatusTip(
            'Close the selected arrays project and delete all data and plots '
            'out of memory')
        self.register_shortcut(self.close_sp_action, QKeySequence.Close)
        self.close_sp_action.triggered.connect(
            lambda: psy.gcp().close(True, True))
        self.close_project_menu.addAction(self.close_sp_action)

        # ----------------------------- Quit ----------------------------------

        if sys.platform != 'darwin':  # mac os makes this anyway
            self.quit_action = QAction('Quit', self)
            self.quit_action.triggered.connect(self.close)
            self.quit_action.triggered.connect(
                QtCore.QCoreApplication.instance().quit)
            self.register_shortcut(self.quit_action, QKeySequence.Quit)
            self.file_menu.addAction(self.quit_action)

        self.menuBar().addMenu(self.file_menu)

        # ######################## Console menu ###############################

        self.console_menu = QMenu('Console', self)
        self.console_menu.addActions(self.console.actions())
        self.menuBar().addMenu(self.console_menu)

        # ######################## Windows menu ###############################

        self.windows_menu = QMenu('Windows', self)
        self.menuBar().addMenu(self.windows_menu)

        # ############################ Help menu ##############################

        self.help_menu = QMenu('Help', parent=self)
        self.menuBar().addMenu(self.help_menu)

        # -------------------------- Preferences ------------------------------

        self.help_action = QAction('Preferences', self)
        self.help_action.triggered.connect(lambda: self.edit_preferences(True))
        self.register_shortcut(self.help_action, QKeySequence.Preferences)
        self.help_menu.addAction(self.help_action)

        # ---------------------------- About ----------------------------------

        self.about_action = QAction('About', self)
        self.about_action.triggered.connect(self.about)
        self.help_menu.addAction(self.about_action)

        # ---------------------------- Dependencies ---------------------------

        self.dependencies_action = QAction('Dependencies', self)
        self.dependencies_action.triggered.connect(
            lambda: self.show_dependencies(True))
        self.help_menu.addAction(self.dependencies_action)

        self.dockwidgets = []

        # ---------------------------------------------------------------------
        # -------------------------- Dock windows -----------------------------
        # ---------------------------------------------------------------------
        #: tab widget displaying the arrays in current main and sub project
        #: tree widget displaying the open datasets
        self.project_content = ProjectContentWidget(parent=self)
        self.ds_tree = DatasetTree(parent=self)
        #: tree widget displaying the open figures
        self.figures_tree = FiguresTree(parent=self)
        #: help explorer
        self.help_explorer = help_explorer = HelpExplorer(parent=self)
        if 'HTML help' in help_explorer.viewers and help_explorer.viewers[
                'HTML help'].sphinx_thread is not None:
            help_explorer.viewers[
                'HTML help'].sphinx_thread.html_ready.connect(
                    self.focus_on_console)
        #: the DataFrameEditor widgets
        self.dataframeeditors = []
        #: general formatoptions widget
        self.fmt_widget = FormatoptionWidget(parent=self,
                                             help_explorer=help_explorer,
                                             console=self.console)

        # load plugin widgets
        self.plugins = plugins = OrderedDict([
            ('console', self.console),
            ('project_content', self.project_content),
            ('ds_tree', self.ds_tree),
            ('figures_tree', self.figures_tree),
            ('help_explorer', self.help_explorer),
            ('fmt_widget', self.fmt_widget),
        ])
        self.default_plugins = list(plugins)
        for plugin_name, w_class in six.iteritems(rcParams.load_plugins()):
            plugins[plugin_name] = w_class(parent=self)

        self.add_mp_to_menu()
        psy.Project.oncpchange.connect(self.eventually_add_mp_to_menu)
        self.windows_menu.addSeparator()

        self.window_layouts_menu = QMenu('Window layouts', self)
        self.restore_layout_action = QAction('Restore default layout', self)
        self.restore_layout_action.triggered.connect(self.setup_default_layout)
        self.window_layouts_menu.addAction(self.restore_layout_action)
        self.windows_menu.addMenu(self.window_layouts_menu)

        self.panes_menu = QMenu('Panes', self)
        self.windows_menu.addMenu(self.panes_menu)

        self.dataframe_menu = QMenu('DataFrame editors', self)
        self.dataframe_menu.addAction(
            'New Editor',
            partial(self.new_data_frame_editor, None, 'DataFrame Editor'))
        self.dataframe_menu.addSeparator()
        self.windows_menu.addMenu(self.dataframe_menu)

        self.central_widgets_menu = menu = QMenu('Central widget', self)
        self.windows_menu.addMenu(menu)
        self.central_widgets_actions = group = QActionGroup(self)
        group.setExclusive(True)

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

        self.console.help_explorer = help_explorer
        psyp.default_print_func = partial(help_explorer.show_rst,
                                          oname='formatoption_docs')
        psy.PlotterInterface._print_func = psyp.default_print_func
        self.setCentralWidget(self.console)

        # make sure that the plots are shown between the project content and
        # the help explorer widget
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)

        # make sure that the formatoption widgets are shown between the
        # project content and the help explorer widget
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)

        # ---------------------------------------------------------------------
        # ------------------------------ closure ------------------------------
        # ---------------------------------------------------------------------
        if show:
            self.help_explorer.show_intro(self.console.intro_msg)

        # ---------------------------------------------------------------------
        # ------------------------- open_files_server -------------------------
        # ---------------------------------------------------------------------
        self.callbacks = {
            'new_plot': self.open_external.emit,
            'change_cwd': self._change_cwd,
            'run_script': self.console.run_script.emit,
            'command': self.console.run_command.emit,
        }

        # Server to open external files on a single instance
        self.open_files_server = socket.socket(socket.AF_INET,
                                               socket.SOCK_STREAM,
                                               socket.IPPROTO_TCP)

        if rcParams['main.listen_to_port']:
            self._file_thread = Thread(target=self.start_open_files_server)
            self._file_thread.setDaemon(True)
            self._file_thread.start()

            self.open_external.connect(self._open_external_files)

        self.config_pages.extend([GuiRcParamsWidget, PsyRcParamsWidget])

        # display the statusBar
        statusbar = self.statusBar()
        self.figures_label = QLabel()
        statusbar.addWidget(self.figures_label)
        self.plugin_label = QLabel()
        statusbar.addWidget(self.plugin_label)

        self.default_widths = {}

        self.setup_default_layout()

        if show:
            self.showMaximized()

        # save the default widths after they have been shown
        for w in self.plugins.values():
            if w.dock is not None:
                self.default_widths[w] = w.dock.size().width()

        # hide plugin widgets that should be hidden at startup. Although this
        # has been executed by :meth:`setup_default_layout`, we have to execute
        # it again after the call of showMaximized
        for name, w in self.plugins.items():
            if name != self.central_widget_key:
                w.to_dock(self)
                if w.hidden:
                    w.hide_plugin()
            else:
                w.create_central_widget_action(self).setChecked(True)

        self._is_open = True
Esempio n. 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)
Esempio n. 9
0
class MainWindow(QMainWindow):

    open_external = QtCore.pyqtSignal(list)

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

        #: list of figures from the psyplot backend
        self.figures = []
        self.error_msg = PyErrorMessage(self)
        self.setDockOptions(
            QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks |
            QMainWindow.AllowTabbedDocks)
        #: Inprocess console
        self.console = ConsoleWidget(parent=self)
        self.project_actions = {}

        # ---------------------------------------------------------------------
        # ----------------------------- Menus ---------------------------------
        # ---------------------------------------------------------------------

        # ######################## File menu ##################################

        # --------------------------- New plot --------------------------------

        self.file_menu = QMenu('File', parent=self)
        self.new_plot_action = QAction('New plot', self)
        self.new_plot_action.setStatusTip(
            'Use an existing dataset (or open a new one) to create one or '
            'more plots')
        self.new_plot_action.setShortcut(QKeySequence.New)
        self.new_plot_action.triggered.connect(self.new_plots)
        self.file_menu.addAction(self.new_plot_action)

        # --------------------------- Open project ----------------------------

        self.open_project_menu = QMenu('Open project', self)
        self.file_menu.addMenu(self.open_project_menu)

        self.open_mp_action = QAction('New main project', self)
        self.open_mp_action.setShortcut(QKeySequence.Open)
        self.open_mp_action.setStatusTip('Open a new main project')
        self.open_mp_action.triggered.connect(self.open_mp)
        self.open_project_menu.addAction(self.open_mp_action)

        self.open_sp_action = QAction('Add to current', self)
        self.open_sp_action.setShortcut(QKeySequence(
            'Ctrl+Shift+O', QKeySequence.NativeText))
        self.open_sp_action.setStatusTip(
            'Load a project as a sub project and add it to the current main '
            'project')
        self.open_sp_action.triggered.connect(self.open_sp)
        self.open_project_menu.addAction(self.open_sp_action)

        # ----------------------- Save project --------------------------------

        self.save_project_menu = QMenu('Save project', parent=self)
        self.file_menu.addMenu(self.save_project_menu)

        self.save_mp_action = QAction('All', self)
        self.save_mp_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.save_mp_action.setShortcut(QKeySequence.Save)
        self.save_mp_action.triggered.connect(self.save_mp)
        self.save_project_menu.addAction(self.save_mp_action)

        self.save_sp_action = QAction('Selected', self)
        self.save_sp_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_action.triggered.connect(self.save_sp)
        self.save_project_menu.addAction(self.save_sp_action)

        # ------------------------ Save project as ----------------------------

        self.save_project_menu = QMenu('Save project', parent=self)
        self.file_menu.addMenu(self.save_project_menu)

        self.save_project_as_menu = QMenu('Save project as', parent=self)
        self.file_menu.addMenu(self.save_project_as_menu)

        self.save_mp_as_action = QAction('All', self)
        self.save_mp_as_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.save_mp_as_action.setShortcut(QKeySequence.SaveAs)
        self.save_mp_as_action.triggered.connect(
            partial(self.save_mp, new_name=True))
        self.save_project_as_menu.addAction(self.save_mp_as_action)

        self.save_sp_as_action = QAction('Selected', self)
        self.save_sp_as_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_as_action.triggered.connect(
            partial(self.save_sp, new_name=True))
        self.save_project_as_menu.addAction(self.save_sp_as_action)

        # -------------------------- Pack project -----------------------------

        self.pack_project_menu = QMenu('Zip project files', parent=self)
        self.file_menu.addMenu(self.pack_project_menu)

        self.pack_mp_action = QAction('All', self)
        self.pack_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.pack_mp_action.triggered.connect(partial(self.save_mp, pack=True))
        self.pack_project_menu.addAction(self.pack_mp_action)

        self.pack_sp_action = QAction('Selected', self)
        self.pack_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.pack_sp_action.triggered.connect(partial(self.save_sp, pack=True))
        self.pack_project_menu.addAction(self.pack_sp_action)

        # ------------------------ Export figures -----------------------------

        self.export_project_menu = QMenu('Export figures', parent=self)
        self.file_menu.addMenu(self.export_project_menu)

        self.export_mp_action = QAction('All', self)
        self.export_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.export_mp_action.triggered.connect(self.export_mp)
        self.export_mp_action.setShortcut(QKeySequence(
            'Ctrl+E', QKeySequence.NativeText))
        self.export_project_menu.addAction(self.export_mp_action)

        self.export_sp_action = QAction('Selected', self)
        self.export_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.export_sp_action.setShortcut(QKeySequence(
            'Ctrl+Shift+E', QKeySequence.NativeText))
        self.export_sp_action.triggered.connect(self.export_sp)
        self.export_project_menu.addAction(self.export_sp_action)

        # ------------------------ Close project ------------------------------

        self.file_menu.addSeparator()

        self.close_project_menu = QMenu('Close project', parent=self)
        self.file_menu.addMenu(self.close_project_menu)

        self.close_mp_action = QAction('Main project', self)
        self.close_mp_action.setStatusTip(
            'Close the main project and delete all data and plots out of '
            'memory')
        self.close_mp_action.setShortcut(QKeySequence.Close)
        self.close_mp_action.triggered.connect(
            lambda: psy.close(psy.gcp(True).num))
        self.close_project_menu.addAction(self.close_mp_action)

        self.close_sp_action = QAction('Only selected', self)
        self.close_sp_action.setStatusTip(
            'Close the selected arrays project and delete all data and plots '
            'out of memory')
        self.close_sp_action.setShortcut(QKeySequence(
            'Ctrl+Shift+W', QKeySequence.NativeText))
        self.close_sp_action.triggered.connect(
            lambda: psy.gcp().close(True, True))
        self.close_project_menu.addAction(self.close_sp_action)

        # ------------------------ Quit ------------------------------

        if sys.platform != 'darwin':  # mac os makes this anyway
            self.quit_action = QAction('Quit', self)
            self.quit_action.triggered.connect(
                QtCore.QCoreApplication.instance().quit)
            self.quit_action.setShortcut(QKeySequence.Quit)
            self.file_menu.addAction(self.quit_action)

        self.menuBar().addMenu(self.file_menu)

        # ######################## Console menu ###############################

        self.console_menu = QMenu('Console', self)
        self.console_menu.addActions(self.console.actions())
        self.menuBar().addMenu(self.console_menu)

        # ######################## Windows menu ###############################

        self.windows_menu = QMenu('Windows', self)
        self.menuBar().addMenu(self.windows_menu)

        # ---------------------------------------------------------------------
        # -------------------------- Dock windows -----------------------------
        # ---------------------------------------------------------------------
        #: tab widget displaying the arrays in current main and sub project
        self.project_content = ProjectContentWidget(parent=self)
        self.addDockWidget(Qt.LeftDockWidgetArea,
                           self.project_content.to_dock('Plot objects', self),
                           'pane')
        #: tree widget displaying the open datasets
        self.ds_tree = DatasetTree(parent=self)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.ds_tree.to_dock(
            'Datasets', self), 'pane')
        #: tree widget displaying the open figures
        self.figures_tree = FiguresTree(parent=self)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.figures_tree.to_dock(
            'Figures', self), 'pane')
        #: help explorer
        self.help_explorer = help_explorer = HelpExplorer(parent=self)
        self.addDockWidget(Qt.RightDockWidgetArea, help_explorer.to_dock(
            'Help explorer', self), 'pane')
        #: general formatoptions widget
        self.fmt_widget = FormatoptionWidget(
            parent=self, help_explorer=help_explorer,
            shell=self.console.kernel_client.kernel.shell)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.fmt_widget.to_dock(
            'Formatoptions', self), 'pane')

        self.windows_menu.addSeparator()
        self.add_mp_to_menu()
        psy.Project.oncpchange.connect(self.eventually_add_mp_to_menu)

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

        self.console.help_explorer = help_explorer
        psyp.default_print_func = partial(help_explorer.show_rst,
                                          oname='formatoption_docs')
        psy._PlotterInterface._print_func = psyp.default_print_func
        self.setCentralWidget(self.console)

        # make sure that the plots are shown between the project content and
        # the help explorer widget
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)

        # make sure that the formatoption widgets are shown between the
        # project content and the help explorer widget
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)

        # Server to open external files on a single instance
        self.open_files_server = socket.socket(socket.AF_INET,
                                               socket.SOCK_STREAM,
                                               socket.IPPROTO_TCP)

        self.showMaximized()

        self._file_thread = Thread(target=self.start_open_files_server)
        self._file_thread.setDaemon(True)
        self._file_thread.start()

        self.open_external.connect(self.open_external_files)

        # ---------------------------------------------------------------------
        # ------------------------------ closure ------------------------------
        # ---------------------------------------------------------------------
        self.help_explorer.show_intro(self.console.intro_msg)

    def _save_project(self, p, new_fname=False, *args, **kwargs):
        if new_fname or 'project_file' not in p.attrs:
            fname = QFileDialog.getSaveFileName(
                self, 'Project destination', os.getcwd(),
                'Pickle files (*.pkl);;'
                'All files (*)'
                )
            if not fname:
                return
        else:
            fname = p.attrs['project_file']
        try:
            p.save_project(fname, *args, **kwargs)
        except:
            self.error_msg.showTraceback('<b>Could not save the project!</b>')
        else:
            p.attrs['project_file'] = fname
            if p.is_main:
                self.update_project_action(p.num)

    def open_mp(self, *args, **kwargs):
        """Open a new main project"""
        self._open_project(main=True)

    def open_sp(self, *args, **kwargs):
        """Open a subproject and add it to the current main project"""
        self._open_project(main=False)

    def _open_project(self, *args, **kwargs):
        fname = QFileDialog.getOpenFileName(
            self, 'Project destination', os.getcwd(),
            'Pickle files (*.pkl);;'
            'All files (*)'
            )
        p = psy.Project.load_project(fname, *args, **kwargs)
        p.attrs['project_file'] = fname
        self.update_project_action(p.num)

    def save_mp(self, *args, **kwargs):
        """Save the current main project"""
        self._save_project(psy.gcp(True), **kwargs)

    def save_sp(self, *args, **kwargs):
        """Save the current sub project"""
        self._save_project(psy.gcp(), **kwargs)

    def _export_project(self, p, *args, **kwargs):
        fname = QFileDialog.getSaveFileName(
            self, 'Picture destination', os.getcwd(),
            'PDF files (*.pdf);;'
            'Postscript file (*.ps);;'
            'PNG image (*.png);;'
            'JPG image (*.jpg *.jpeg);;'
            'TIFF image (*.tif *.tiff);;'
            'GIF image (*.gif);;'
            'All files (*)'
            )
        if not fname:
            return
        try:
            p.export(fname, *args, **kwargs)
        except:
            self.error_msg.showTraceback(
                '<b>Could not export the figures!</b>')

    def export_mp(self, *args, **kwargs):
        self._export_project(psy.gcp(True), **kwargs)

    def export_sp(self, *args, **kwargs):
        self._export_project(psy.gcp(), **kwargs)

    def new_plots(self):
        if hasattr(self, 'plot_creator'):
            self.plot_creator.close()
        self.plot_creator = PlotCreator(
            self.console.get_obj, help_explorer=self.help_explorer)
        available_width = QDesktopWidget().availableGeometry().width() / 3.
        width = self.plot_creator.sizeHint().width()
        height = self.plot_creator.sizeHint().height()
        # The plot creator window shoul cover at least one third of the screen
        self.plot_creator.resize(max(available_width, width), height)
        self.plot_creator.show()

    def add_mp_to_menu(self):
        mp = psy.gcp(True)
        action = QAction(os.path.basename(mp.attrs.get(
            'project_file', 'Untitled %s*' % mp.num)), self)
        action.setStatusTip(
            'Make project %s the current project' % mp.num)
        action.triggered.connect(lambda: psy.scp(psy.project(mp.num)))
        self.project_actions[mp.num] = action
        self.windows_menu.addAction(action)

    def update_project_action(self, num):
        action = self.project_actions.get(num)
        p = psy.project(num)
        if action:
            action.setText(os.path.basename(p.attrs.get(
                'project_file', 'Untitled %s*' % num)))

    def eventually_add_mp_to_menu(self, p):
        for num in set(self.project_actions).difference(
                psy.get_project_nums()):
            self.windows_menu.removeAction(self.project_actions.pop(num))
        if p is None or not p.is_main:
            return
        if p.num not in self.project_actions:
            self.add_mp_to_menu()

    def addDockWidget(self, area, dockwidget, docktype=None, *args, **kwargs):
        """Reimplemented to add widgets to the windows menu"""
        ret = super(MainWindow, self).addDockWidget(area, dockwidget, *args,
                                                    **kwargs)
        if docktype == 'pane':
            self.windows_menu.addAction(dockwidget.toggleViewAction())
        return ret

    def start_open_files_server(self):
        """This method listens to the open_files_port and opens the plot
        creator for new files

        This method is inspired and to most parts copied from spyder"""
        self.open_files_server.setsockopt(socket.SOL_SOCKET,
                                          socket.SO_REUSEADDR, 1)
        port = rcParams['main.open_files_port']
        self.open_files_server.bind(('127.0.0.1', port))
        self.open_files_server.listen(20)
        while 1:  # 1 is faster than True
            try:
                req, dummy = self.open_files_server.accept()
            except socket.error as e:
                # See Issue 1275 for details on why errno EINTR is
                # silently ignored here.
                eintr = errno.EINTR
                # To avoid a traceback after closing on Windows
                if e.args[0] == eintr:
                    continue
                raise
            l = pickle.loads(req.recv(1024))
            self.open_external.emit(l)
            req.sendall(b' ')

    def open_external_files(self, l):
        fnames, project, engine, plot_method, name, dims = l
        if project is not None:
            fnames = [s.split(',') for s in fnames]
            single_files = (l[0] for l in fnames if len(l) == 1)
            alternative_paths = defaultdict(lambda: next(single_files, None))
            alternative_paths.update(list(l for l in fnames if len(l) == 2))
            psy.Project.load_project(
                project, alternative_paths=alternative_paths,
                engine=engine, main=False)
        else:
            self.new_plots()
            self.plot_creator.open_dataset(fnames, engine=engine)
            self.plot_creator.insert_array(name)
            if dims is not None:
                self.plot_creator.array_table.selectAll()
                self.plot_creator.array_table.update_selected(
                    dims={key: ', '.join(
                        map(str, val)) for key, val in six.iteritems(
                            dims)})
Esempio n. 10
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)
Esempio n. 11
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())
Esempio n. 12
0
class MainWindow(QMainWindow):

    #: A signal that is emmitted when the a signal is received through the
    #: open_files_server
    open_external = QtCore.pyqtSignal(list)

    #: The server to open external files
    open_files_server = None

    #: Inprocess console
    console = None

    #: tree widget displaying the open datasets
    ds_tree = None

    #: list of figures from the psyplot backend
    figures = []

    #: tree widget displaying the open figures
    figures_tree = None

    #: general formatoptions widget
    fmt_widget = None

    #: help explorer
    help_explorer = None

    #: the DataFrameEditor widgets, a widget to show and edit data frames
    dataframeeditors = None

    #: tab widget displaying the arrays in current main and sub project
    project_content = None

    #: The dockwidgets of this instance
    dockwidgets = []

    #: default widths of the dock widgets
    default_widths = {}

    _is_open = False

    #: The keyboard shortcuts of the default layout
    default_shortcuts = []

    #: The current keyboard shortcuts
    current_shortcuts = []

    #: The key for the central widget for the main window in the
    #: :attr:`plugins` dictionary
    central_widget_key = 'console'

    @property
    def logger(self):
        """The logger of this instance"""
        return logging.getLogger('%s.%s' % (self.__class__.__module__,
                                            self.__class__.__name__))

    @docstrings.get_sectionsf('MainWindow')
    @docstrings.dedent
    def __init__(self, show=True):
        """
        Parameters
        ----------
        show: bool
            If True, the created mainwindow is show
        """
        if sys.stdout is None:
            sys.stdout = StreamToLogger(self.logger)
        if sys.stderr is None:
            sys.stderr = StreamToLogger(self.logger)
        super(MainWindow, self).__init__()
        self.setWindowIcon(QIcon(get_icon('logo.png')))

        #: list of figures from the psyplot backend
        self.figures = []
        self.error_msg = PyErrorMessage(self)
        self.setDockOptions(
            QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks |
            QMainWindow.AllowTabbedDocks)
        #: Inprocess console
        self.console = ConsoleWidget(self)
        self.project_actions = {}

        self.config_pages = []

        self.open_file_options = OrderedDict([
            ('new psyplot plot from dataset', self.open_external_files),
            ('new psyplot project', partial(self.open_external_files, [])),
            ])

        # ---------------------------------------------------------------------
        # ----------------------------- Menus ---------------------------------
        # ---------------------------------------------------------------------

        # ######################## File menu ##################################

        # --------------------------- New plot --------------------------------

        self.file_menu = QMenu('File', parent=self)
        self.new_plot_action = QAction('New plot', self)
        self.new_plot_action.setStatusTip(
            'Use an existing dataset (or open a new one) to create one or '
            'more plots')
        self.register_shortcut(self.new_plot_action, QKeySequence.New)
        self.new_plot_action.triggered.connect(lambda: self.new_plots(True))
        self.file_menu.addAction(self.new_plot_action)

        # --------------------------- Open project ----------------------------

        self.open_project_menu = QMenu('Open project', self)
        self.file_menu.addMenu(self.open_project_menu)

        self.open_mp_action = QAction('New main project', self)
        self.register_shortcut(self.open_mp_action, QKeySequence.Open)
        self.open_mp_action.setStatusTip('Open a new main project')
        self.open_mp_action.triggered.connect(self.open_mp)
        self.open_project_menu.addAction(self.open_mp_action)

        self.open_sp_action = QAction('Add to current', self)

        self.register_shortcut(
            self.open_sp_action, QKeySequence(
                'Ctrl+Shift+O', QKeySequence.NativeText))
        self.open_sp_action.setStatusTip(
            'Load a project as a sub project and add it to the current main '
            'project')
        self.open_sp_action.triggered.connect(self.open_sp)
        self.open_project_menu.addAction(self.open_sp_action)

        # ----------------------- Save project --------------------------------

        self.save_project_menu = QMenu('Save', parent=self)
        self.file_menu.addMenu(self.save_project_menu)

        self.save_mp_action = QAction('Full psyplot project', self)
        self.save_mp_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.register_shortcut(self.save_mp_action, QKeySequence.Save)
        self.save_mp_action.triggered.connect(self.save_mp)
        self.save_project_menu.addAction(self.save_mp_action)

        self.save_sp_action = QAction('Selected psyplot project', self)
        self.save_sp_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_action.triggered.connect(self.save_sp)
        self.save_project_menu.addAction(self.save_sp_action)

        # ------------------------ Save project as ----------------------------

        self.save_project_as_menu = QMenu('Save as', parent=self)
        self.file_menu.addMenu(self.save_project_as_menu)

        self.save_mp_as_action = QAction('Full psyplot project', self)
        self.save_mp_as_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.register_shortcut(self.save_mp_as_action,
                                       QKeySequence.SaveAs)
        self.save_mp_as_action.triggered.connect(
            partial(self.save_mp, new_fname=True))
        self.save_project_as_menu.addAction(self.save_mp_as_action)

        self.save_sp_as_action = QAction('Selected psyplot project', self)
        self.save_sp_as_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_as_action.triggered.connect(
            partial(self.save_sp, new_fname=True))
        self.save_project_as_menu.addAction(self.save_sp_as_action)

        # -------------------------- Pack project -----------------------------

        self.pack_project_menu = QMenu('Zip project files', parent=self)
        self.file_menu.addMenu(self.pack_project_menu)

        self.pack_mp_action = QAction('Full psyplot project', self)
        self.pack_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.pack_mp_action.triggered.connect(partial(self.save_mp, pack=True))
        self.pack_project_menu.addAction(self.pack_mp_action)

        self.pack_sp_action = QAction('Selected psyplot project', self)
        self.pack_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.pack_sp_action.triggered.connect(partial(self.save_sp, pack=True))
        self.pack_project_menu.addAction(self.pack_sp_action)

        # ------------------------ Export figures -----------------------------

        self.export_project_menu = QMenu('Export figures', parent=self)
        self.file_menu.addMenu(self.export_project_menu)

        self.export_mp_action = QAction('Full psyplot project', self)
        self.export_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.export_mp_action.triggered.connect(self.export_mp)
        self.register_shortcut(
            self.export_mp_action, QKeySequence(
                'Ctrl+E', QKeySequence.NativeText))
        self.export_project_menu.addAction(self.export_mp_action)

        self.export_sp_action = QAction('Selected psyplot project', self)
        self.export_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.register_shortcut(
            self.export_sp_action, QKeySequence(
                'Ctrl+Shift+E', QKeySequence.NativeText))
        self.export_sp_action.triggered.connect(self.export_sp)
        self.export_project_menu.addAction(self.export_sp_action)

        # ------------------------ Close project ------------------------------

        self.file_menu.addSeparator()

        self.close_project_menu = QMenu('Close project', parent=self)
        self.file_menu.addMenu(self.close_project_menu)

        self.close_mp_action = QAction('Full psyplot project', self)
        self.register_shortcut(
            self.close_mp_action, QKeySequence(
                'Ctrl+Shift+W', QKeySequence.NativeText))
        self.close_mp_action.setStatusTip(
            'Close the main project and delete all data and plots out of '
            'memory')
        self.close_mp_action.triggered.connect(
            lambda: psy.close(psy.gcp(True).num))
        self.close_project_menu.addAction(self.close_mp_action)

        self.close_sp_action = QAction('Selected psyplot project', self)
        self.close_sp_action.setStatusTip(
            'Close the selected arrays project and delete all data and plots '
            'out of memory')
        self.register_shortcut(self.close_sp_action, QKeySequence.Close)
        self.close_sp_action.triggered.connect(
            lambda: psy.gcp().close(True, True))
        self.close_project_menu.addAction(self.close_sp_action)

        # ----------------------------- Quit ----------------------------------

        if sys.platform != 'darwin':  # mac os makes this anyway
            self.quit_action = QAction('Quit', self)
            self.quit_action.triggered.connect(self.close)
            self.quit_action.triggered.connect(
                QtCore.QCoreApplication.instance().quit)
            self.register_shortcut(
                self.quit_action, QKeySequence.Quit)
            self.file_menu.addAction(self.quit_action)

        self.menuBar().addMenu(self.file_menu)

        # ######################## Console menu ###############################

        self.console_menu = QMenu('Console', self)
        self.console_menu.addActions(self.console.actions())
        self.menuBar().addMenu(self.console_menu)

        # ######################## Windows menu ###############################

        self.windows_menu = QMenu('Windows', self)
        self.menuBar().addMenu(self.windows_menu)

        # ############################ Help menu ##############################

        self.help_menu = QMenu('Help', parent=self)
        self.menuBar().addMenu(self.help_menu)

        # -------------------------- Preferences ------------------------------

        self.help_action = QAction('Preferences', self)
        self.help_action.triggered.connect(lambda: self.edit_preferences(True))
        self.register_shortcut(self.help_action,
                                       QKeySequence.Preferences)
        self.help_menu.addAction(self.help_action)

        # ---------------------------- About ----------------------------------

        self.about_action = QAction('About', self)
        self.about_action.triggered.connect(self.about)
        self.help_menu.addAction(self.about_action)

        # ---------------------------- Dependencies ---------------------------

        self.dependencies_action = QAction('Dependencies', self)
        self.dependencies_action.triggered.connect(
            lambda: self.show_dependencies(True))
        self.help_menu.addAction(self.dependencies_action)

        self.dockwidgets = []

        # ---------------------------------------------------------------------
        # -------------------------- Dock windows -----------------------------
        # ---------------------------------------------------------------------
        #: tab widget displaying the arrays in current main and sub project
        #: tree widget displaying the open datasets
        self.project_content = ProjectContentWidget(parent=self)
        self.ds_tree = DatasetTree(parent=self)
        #: tree widget displaying the open figures
        self.figures_tree = FiguresTree(parent=self)
        #: help explorer
        self.help_explorer = help_explorer = HelpExplorer(parent=self)
        if help_explorer.viewers['HTML help'].sphinx_thread is not None:
            help_explorer.viewers[
                'HTML help'].sphinx_thread.html_ready.connect(
                    self.focus_on_console)
        #: the DataFrameEditor widgets
        self.dataframeeditors = []
        #: general formatoptions widget
        self.fmt_widget = FormatoptionWidget(
            parent=self, help_explorer=help_explorer,
            console=self.console)

        # load plugin widgets
        self.plugins = plugins = OrderedDict([
            ('console', self.console),
            ('project_content', self.project_content),
            ('ds_tree', self.ds_tree),
            ('figures_tree', self.figures_tree),
            ('help_explorer', self.help_explorer),
            ('fmt_widget', self.fmt_widget),
            ])
        self.default_plugins = list(plugins)
        for plugin_name, w_class in six.iteritems(rcParams.load_plugins()):
            plugins[plugin_name] = w_class(parent=self)

        self.add_mp_to_menu()
        psy.Project.oncpchange.connect(self.eventually_add_mp_to_menu)
        self.windows_menu.addSeparator()

        self.window_layouts_menu = QMenu('Window layouts', self)
        self.restore_layout_action = QAction('Restore default layout', self)
        self.restore_layout_action.triggered.connect(self.setup_default_layout)
        self.window_layouts_menu.addAction(self.restore_layout_action)
        self.windows_menu.addMenu(self.window_layouts_menu)

        self.panes_menu = QMenu('Panes', self)
        self.windows_menu.addMenu(self.panes_menu)

        self.dataframe_menu = QMenu('DataFrame editors', self)
        self.dataframe_menu.addAction(
            'New Editor', partial(self.new_data_frame_editor, None,
                                  'DataFrame Editor'))
        self.dataframe_menu.addSeparator()
        self.windows_menu.addMenu(self.dataframe_menu)

        self.central_widgets_menu = menu = QMenu('Central widget', self)
        self.windows_menu.addMenu(menu)
        self.central_widgets_actions = group = QActionGroup(self)
        group.setExclusive(True)

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

        self.console.help_explorer = help_explorer
        psyp.default_print_func = partial(help_explorer.show_rst,
                                          oname='formatoption_docs')
        psy.PlotterInterface._print_func = psyp.default_print_func
        self.setCentralWidget(self.console)

        # make sure that the plots are shown between the project content and
        # the help explorer widget
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)

        # make sure that the formatoption widgets are shown between the
        # project content and the help explorer widget
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)

        # ---------------------------------------------------------------------
        # ------------------------------ closure ------------------------------
        # ---------------------------------------------------------------------
        if show:
            self.help_explorer.show_intro(self.console.intro_msg)

        # ---------------------------------------------------------------------
        # ------------------------- open_files_server -------------------------
        # ---------------------------------------------------------------------
        self.callbacks = {'new_plot': self.open_external.emit,
                          'change_cwd': self._change_cwd,
                          'run_script': self.console.run_script.emit,
                          'command': self.console.run_command.emit,
                          }

        # Server to open external files on a single instance
        self.open_files_server = socket.socket(socket.AF_INET,
                                               socket.SOCK_STREAM,
                                               socket.IPPROTO_TCP)

        if rcParams['main.listen_to_port']:
            self._file_thread = Thread(target=self.start_open_files_server)
            self._file_thread.setDaemon(True)
            self._file_thread.start()

            self.open_external.connect(self._open_external_files)

        self.config_pages.extend([GuiRcParamsWidget, PsyRcParamsWidget])

        # display the statusBar
        statusbar = self.statusBar()
        self.figures_label = QLabel()
        statusbar.addWidget(self.figures_label)
        self.plugin_label = QLabel()
        statusbar.addWidget(self.plugin_label)

        self.default_widths = {}

        self.setup_default_layout()

        if show:
            self.showMaximized()

        # save the default widths after they have been shown
        for w in self.plugins.values():
            if w.dock is not None:
                self.default_widths[w] = w.dock.size().width()

        # hide plugin widgets that should be hidden at startup. Although this
        # has been executed by :meth:`setup_default_layout`, we have to execute
        # it again after the call of showMaximized
        for name, w in self.plugins.items():
            if name != self.central_widget_key:
                w.to_dock(self)
                if w.hidden:
                    w.hide_plugin()
            else:
                w.create_central_widget_action(self).setChecked(True)

        self._is_open = True

    def focus_on_console(self, *args, **kwargs):
        """Put focus on the ipython console"""
        self.console._control.setFocus()

    def new_data_frame_editor(self, df=None, title='DataFrame Editor'):
        """Open a new dataframe editor

        Parameters
        ----------
        df: pandas.DataFrame
            The dataframe to display
        title: str
            The title of the dock window

        Returns
        -------
        psyplot_gui.dataframeeditor.DataFrameEditor
            The newly created editor"""
        editor = DataFrameEditor()
        self.dataframeeditors.append(editor)
        editor.to_dock(self, title,
                       Qt.RightDockWidgetArea, docktype='df')
        if df is not None:
            editor.set_df(df)
        editor.show_plugin()
        editor.maybe_tabify()
        editor.raise_()
        return editor

    def setup_default_layout(self):
        """Set up the default window layout"""
        self.project_content.to_dock(self, 'Plot objects',
                                     Qt.LeftDockWidgetArea)
        self.ds_tree.to_dock(self, 'Datasets', Qt.LeftDockWidgetArea)
        self.figures_tree.to_dock(self, 'Figures', Qt.LeftDockWidgetArea)
        self.help_explorer.to_dock(self, 'Help explorer',
                                   Qt.RightDockWidgetArea)
        self.fmt_widget.to_dock(self, 'Formatoptions', Qt.BottomDockWidgetArea)

        modify_widths = bool(self.default_widths)
        for w in map(self.plugins.__getitem__, self.default_plugins):
            if w.dock is not None:
                w.show_plugin()

                if modify_widths and with_qt5:
                    self.resizeDocks([w.dock], [self.default_widths[w]],
                                     Qt.Horizontal)

        # hide plugin widgets that should be hidden at startup
        for name, w in self.plugins.items():
            if name != self.central_widget_key:
                w.to_dock(self)
                if w.hidden:
                    w.hide_plugin()

        action2shortcut = defaultdict(list)
        for s, a in self.default_shortcuts:
            action2shortcut[a].append(s)

        for a, s in action2shortcut.items():
            self.register_shortcut(a, s)

    def set_central_widget(self, name):
        """Set the central widget

        Parameters
        ----------
        name: str or QWidget
            The key or the plugin widget in the :attr:`plugins` dictionary"""
        from PyQt5.QtCore import QTimer
        self.setUpdatesEnabled(False)
        current = self.centralWidget()
        if isinstance(name, six.string_types):
            new = self.plugins[name]
        else:
            new = name
            name = next(key for key, val in self.plugins.items() if val is new)
        if new is not current:

            self._dock_widths = dock_widths = OrderedDict()
            self._dock_heights = dock_heights = OrderedDict()
            for key, w in self.plugins.items():
                if w.dock is not None and w.is_shown:
                    s = w.dock.size()
                    dock_widths[w] = s.width()
                    if w is not new:
                        dock_heights[w] = s.height()

            new_pos = self.dockWidgetArea(new.dock)

            self.removeDockWidget(new.dock)
            new.dock.close()
            self.panes_menu.removeAction(new._view_action)
            self.dataframe_menu.removeAction(new._view_action)
            new.dock = new._view_action = None
            self.central_widget_key = name
            current.to_dock(self)
            self.setCentralWidget(new)
            new._set_central_action.setChecked(True)
            current.show_plugin()
            current.to_dock(self)
            new_width = dock_widths.pop(new, None)
            if current.hidden:
                current.hide_plugin()
            else:
                current_pos = self.dockWidgetArea(current.dock)
                if current_pos == new_pos and new_width:
                    dock_widths[current] = new_width

        self._custom_layout_timer = QTimer(self)
        self._custom_layout_timer.timeout.connect(self._reset_dock_widths)
        self._custom_layout_timer.setSingleShot(True)
        self._custom_layout_timer.start(5000)

    def _reset_dock_widths(self):
        # resize the plugins
        if with_qt5:
            for w, width in self._dock_widths.items():
                if w.dock is not None:
                    self.resizeDocks([w.dock], [width], Qt.Horizontal)
            for w, height in self._dock_heights.items():
                if w.dock is not None:
                    self.resizeDocks([w.dock], [height], Qt.Vertical)

        self.setUpdatesEnabled(True)

    def _save_project(self, p, new_fname=False, *args, **kwargs):
        if new_fname or 'project_file' not in p.attrs:
            fname = QFileDialog.getSaveFileName(
                self, 'Project destination', os.getcwd(),
                'Pickle files (*.pkl);;'
                'All files (*)'
                )
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
            if not fname:
                return
        else:
            fname = p.attrs['project_file']
        try:
            p.save_project(fname, *args, **kwargs)
        except Exception:
            self.error_msg.showTraceback('<b>Could not save the project!</b>')
        else:
            p.attrs['project_file'] = fname
            if p.is_main:
                self.update_project_action(p.num)

    def open_mp(self, *args, **kwargs):
        """Open a new main project"""
        self._open_project(main=True)

    def open_sp(self, *args, **kwargs):
        """Open a subproject and add it to the current main project"""
        self._open_project(main=False)

    def _open_project(self, *args, **kwargs):
        fname = QFileDialog.getOpenFileName(
            self, 'Project file', os.getcwd(),
            'Pickle files (*.pkl);;'
            'All files (*)'
            )
        if with_qt5:  # the filter is passed as well
            fname = fname[0]
        if not fname:
            return
        p = psy.Project.load_project(fname, *args, **kwargs)
        p.attrs['project_file'] = fname
        self.update_project_action(p.num)

    def save_mp(self, *args, **kwargs):
        """Save the current main project"""
        self._save_project(psy.gcp(True), **kwargs)

    def save_sp(self, *args, **kwargs):
        """Save the current sub project"""
        self._save_project(psy.gcp(), **kwargs)

    def _export_project(self, p, *args, **kwargs):
        fname = QFileDialog.getSaveFileName(
            self, 'Picture destination', os.getcwd(),
            'PDF files (*.pdf);;'
            'Postscript file (*.ps);;'
            'PNG image (*.png);;'
            'JPG image (*.jpg *.jpeg);;'
            'TIFF image (*.tif *.tiff);;'
            'GIF image (*.gif);;'
            'All files (*)'
            )
        if with_qt5:  # the filter is passed as well
            fname = fname[0]
        if not fname:
            return
        try:
            p.export(fname, *args, **kwargs)
        except Exception:
            self.error_msg.showTraceback(
                '<b>Could not export the figures!</b>')

    def export_mp(self, *args, **kwargs):
        self._export_project(psy.gcp(True), **kwargs)

    def export_sp(self, *args, **kwargs):
        self._export_project(psy.gcp(), **kwargs)

    def new_plots(self, exec_=None):
        if hasattr(self, 'plot_creator'):
            try:
                self.plot_creator.close()
            except RuntimeError:
                pass
        self.plot_creator = PlotCreator(
            help_explorer=self.help_explorer, parent=self)
        available_width = QDesktopWidget().availableGeometry().width() / 3.
        width = self.plot_creator.sizeHint().width()
        height = self.plot_creator.sizeHint().height()
        # The plot creator window should cover at least one third of the screen
        self.plot_creator.resize(max(available_width, width), height)
        if exec_:
            self.plot_creator.exec_()

    def excepthook(self, type, value, traceback):
        """A method to replace the sys.excepthook"""
        self.error_msg.excepthook(type, value, traceback)

    def edit_preferences(self, exec_=None):
        """Edit Spyder preferences"""
        if hasattr(self, 'preferences'):
            try:
                self.preferences.close()
            except RuntimeError:
                pass
        self.preferences = dlg = Prefences(self)
        for PrefPageClass in self.config_pages:
            widget = PrefPageClass(dlg)
            widget.initialize()
            dlg.add_page(widget)
        available_width = 0.667 * QDesktopWidget().availableGeometry().width()
        width = dlg.sizeHint().width()
        height = dlg.sizeHint().height()
        # The preferences window should cover at least one third of the screen
        dlg.resize(max(available_width, width), height)
        if exec_:
            dlg.exec_()

    def about(self):
        """About the tool"""
        versions = {
            key: d['version'] for key, d in psyplot.get_versions(False).items()
            }
        versions.update(psyplot_gui.get_versions()['requirements'])
        versions.update(psyplot._get_versions()['requirements'])
        versions['github'] = 'https://github.com/Chilipp/psyplot'
        versions['author'] = psyplot.__author__
        QMessageBox.about(
            self, "About psyplot",
            u"""<b>psyplot: Interactive data visualization with python</b>
            <br>Copyright &copy; 2017- Philipp Sommer
            <br>Licensed under the terms of the GNU General Public License v2
            (GPLv2)
            <p>Created by %(author)s</p>
            <p>Most of the icons come from the
            <a href="https://www.iconfinder.com/"> iconfinder</a>.</p>
            <p>For bug reports and feature requests, please go
            to our <a href="%(github)s">Github website</a> or contact the
            author via mail.</p>
            <p>This package uses (besides others) the following packages:<br>
            <ul>
                <li>psyplot %(psyplot)s</li>
                <li>Python %(python)s </li>
                <li>numpy %(numpy)s</li>
                <li>xarray %(xarray)s</li>
                <li>pandas %(pandas)s</li>
                <li>psyplot_gui %(psyplot_gui)s</li>
                <li>Qt %(qt)s</li>
                <li>PyQt %(pyqt)s</li>
                <li>qtconsole %(qtconsole)s</li>
            </ul></p>
            <p>For a full list of requirements see the <em>dependencies</em>
            in the <em>Help</em> menu.</p>
            <p>This software is provided "as is", without warranty or support
            of any kind.</p>"""
            % versions)

    def show_dependencies(self, exec_=None):
        """Open a dialog that shows the dependencies"""
        if hasattr(self, 'dependencies'):
            try:
                self.dependencies.close()
            except RuntimeError:
                pass
        self.dependencies = dlg = DependenciesDialog(psyplot.get_versions(),
                                                     parent=self)
        dlg.resize(630, 420)
        if exec_:
            dlg.exec_()

    def reset_rcParams(self):
        rcParams.update_from_defaultParams()
        psy.rcParams.update_from_defaultParams()

    def add_mp_to_menu(self):
        mp = psy.gcp(True)
        action = QAction(os.path.basename(mp.attrs.get(
            'project_file', 'Untitled %s*' % mp.num)), self)
        action.setStatusTip(
            'Make project %s the current project' % mp.num)
        action.triggered.connect(lambda: psy.scp(psy.project(mp.num)))
        self.project_actions[mp.num] = action
        self.windows_menu.addAction(action)

    def update_project_action(self, num):
        action = self.project_actions.get(num)
        p = psy.project(num)
        if action:
            action.setText(os.path.basename(p.attrs.get(
                'project_file', 'Untitled %s*' % num)))

    def eventually_add_mp_to_menu(self, p):
        for num in set(self.project_actions).difference(
                psy.get_project_nums()):
            self.windows_menu.removeAction(self.project_actions.pop(num))
        if p is None or not p.is_main:
            return
        if p.num not in self.project_actions:
            self.add_mp_to_menu()

    def start_open_files_server(self):
        """This method listens to the open_files_port and opens the plot
        creator for new files

        This method is inspired and to most parts copied from spyder"""
        self.open_files_server.setsockopt(socket.SOL_SOCKET,
                                          socket.SO_REUSEADDR, 1)
        port = rcParams['main.open_files_port']
        try:
            self.open_files_server.bind(('127.0.0.1', port))
        except Exception:
            return
        self.open_files_server.listen(20)
        while 1:  # 1 is faster than True
            try:
                req, dummy = self.open_files_server.accept()
            except socket.error as e:
                # See Issue 1275 for details on why errno EINTR is
                # silently ignored here.
                eintr = errno.EINTR
                # To avoid a traceback after closing on Windows
                if e.args[0] == eintr:
                    continue
                # handle a connection abort on close error
                enotsock = (errno.WSAENOTSOCK if os.name == 'nt'
                            else errno.ENOTSOCK)
                if e.args[0] in [errno.ECONNABORTED, enotsock]:
                    return
                raise
            args = pickle.loads(req.recv(1024))
            callback = args[0]
            func = self.callbacks[callback]
            self.logger.debug('Emitting %s callback %s', callback, func)
            func(args[1:])
            req.sendall(b' ')

    def change_cwd(self, path):
        """Change the current working directory"""
        import os
        os.chdir(path)

    def _change_cwd(self, args):
        path = args[0][0]
        self.change_cwd(path)

    docstrings.keep_params(
        'make_plot.parameters', 'fnames', 'project', 'engine', 'plot_method',
        'name', 'dims', 'encoding', 'enable_post', 'seaborn_style',
        'concat_dim', 'chname')

    def open_files(self, fnames):
        """Open a file and ask the user how"""
        fnames_s = ', '.join(map(os.path.basename, fnames))
        if len(fnames_s) > 30:
            fnames_s = fnames_s[:27] + '...'
        item, ok = QInputDialog.getItem(
            self, 'Open file...', 'Open %s as...' % fnames_s,
            list(self.open_file_options), current=0, editable=False)
        if ok:
            return self.open_file_options[item](fnames)

    @docstrings.get_sectionsf('MainWindow.open_external_files')
    @docstrings.dedent
    def open_external_files(self, fnames=[], project=None, engine=None,
                            plot_method=None, name=None, dims=None,
                            encoding=None, enable_post=False,
                            seaborn_style=None, concat_dim=get_default_value(
                                xr.open_mfdataset, 'concat_dim'), chname={}):
        """
        Open external files

        Parameters
        ----------
        %(make_plot.parameters.fnames|project|engine|plot_method|name|dims|encoding|enable_post|seaborn_style|concat_dim|chname)s
        """
        if seaborn_style is not None:
            import seaborn as sns
            sns.set_style(seaborn_style)
        if project is not None:
            fnames = [s.split(',') for s in fnames]
            if not isinstance(project, dict):
                project = psyd.safe_list(project)[0]
            single_files = (l[0] for l in fnames if len(l) == 1)
            alternative_paths = defaultdict(lambda: next(single_files, None))
            alternative_paths.update(list(l for l in fnames if len(l) == 2))
            p = psy.Project.load_project(
                project, alternative_paths=alternative_paths,
                engine=engine, main=not psy.gcp(), encoding=encoding,
                enable_post=enable_post, chname=chname)
            if isinstance(project, six.string_types):
                p.attrs.setdefault('project_file', project)
            return True
        else:
            self.new_plots(False)
            self.plot_creator.open_dataset(fnames, engine=engine,
                                           concat_dim=concat_dim)
            if name == 'all':
                ds = self.plot_creator.get_ds()
                name = sorted(set(ds.variables) - set(ds.coords))
            self.plot_creator.insert_array(
                list(filter(None, psy.safe_list(name))))
            if dims is not None:
                ds = self.plot_creator.get_ds()
                dims = {key: ', '.join(
                    map(str, val)) for key, val in six.iteritems(
                        dims)}
                for i, vname in enumerate(
                        self.plot_creator.array_table.vnames):
                    self.plot_creator.array_table.selectRow(i)
                    self.plot_creator.array_table.update_selected(
                        )
                self.plot_creator.array_table.selectAll()
                var = ds[vname[0]]
                self.plot_creator.array_table.update_selected(
                    dims=var.psy.decoder.correct_dims(var, dims.copy()))
            if plot_method:
                self.plot_creator.pm_combo.setCurrentIndex(
                    self.plot_creator.pm_combo.findText(plot_method))
            self.plot_creator.exec_()
            return True

    def _open_external_files(self, args):
        self.open_external_files(*args)

    @classmethod
    @docstrings.get_sectionsf('MainWindow.run')
    @docstrings.dedent
    def run(cls, fnames=[], project=None, engine=None, plot_method=None,
            name=None, dims=None, encoding=None, enable_post=False,
            seaborn_style=None,
            concat_dim=get_default_value(xr.open_mfdataset, 'concat_dim'),
            chname={}, show=True):
        """
        Create a mainwindow and open the given files or project

        This class method creates a new mainwindow instance and sets the
        global :attr:`mainwindow` variable.

        Parameters
        ----------
        %(MainWindow.open_external_files.parameters)s
        %(MainWindow.parameters)s

        Notes
        -----
        - There can be only one mainwindow at the time
        - This method does not create a QApplication instance! See
          :meth:`run_app`

        See Also
        --------
        run_app
        """
        mainwindow = cls(show=show)
        _set_mainwindow(mainwindow)
        if fnames or project:
            mainwindow.open_external_files(
                fnames, project, engine, plot_method, name, dims, encoding,
                enable_post, seaborn_style, concat_dim, chname)
        psyplot.with_gui = True
        return mainwindow

    def register_shortcut(self, action, shortcut,
                          context=Qt.ApplicationShortcut):
        """Register an action for a shortcut"""
        shortcuts = psy.safe_list(shortcut)
        for j, shortcut in enumerate(shortcuts):
            found = False
            for i, (s, a) in enumerate(self.current_shortcuts):
                if s == shortcut:
                    new_shortcuts = [
                        sc for sc in self.current_shortcuts[i][1].shortcuts()
                        if sc != s]
                    a.setShortcut(QKeySequence())
                    if new_shortcuts:
                        a.setShortcuts(new_shortcuts)
                    self.current_shortcuts[i][1] = action
                    found = True
                    break
            if not found:
                self.default_shortcuts.append([shortcut, action])
                self.current_shortcuts.append([shortcut, action])
        action.setShortcuts(shortcuts)
        action.setShortcutContext(context)

    @classmethod
    @docstrings.dedent
    def run_app(cls, *args, **kwargs):
        """
        Create a QApplication, open the given files or project and enter the
        mainloop

        Parameters
        ----------
        %(MainWindow.run.parameters)s

        See Also
        --------
        run
        """
        app = QApplication(sys.argv)
        cls.run(*args, **kwargs)
        sys.exit(app.exec_())

    def closeEvent(self, event):
        """closeEvent reimplementation"""
        if not self._is_open or (self._is_open and self.close()):
            self._is_open = False
            event.accept()

    def close(self):
        _set_mainwindow(None)
        if self.open_files_server is not None:
            self.open_files_server.close()
            del self.open_files_server
        for widget in self.plugins.values():
            widget.close()
        self.plugins.clear()
        return super(MainWindow, self).close()
Esempio n. 13
0
    def __init__(self, show=True):
        """
        Parameters
        ----------
        show: bool
            If True, the created mainwindow is show
        """
        if sys.stdout is None:
            sys.stdout = StreamToLogger(self.logger)
        if sys.stderr is None:
            sys.stderr = StreamToLogger(self.logger)
        super(MainWindow, self).__init__()
        self.setWindowIcon(QIcon(get_icon('logo.png')))

        #: list of figures from the psyplot backend
        self.figures = []
        self.error_msg = PyErrorMessage(self)
        self.setDockOptions(
            QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks |
            QMainWindow.AllowTabbedDocks)
        #: Inprocess console
        self.console = ConsoleWidget(self)
        self.project_actions = {}

        self.config_pages = []

        self.open_file_options = OrderedDict([
            ('new psyplot plot from dataset', self.open_external_files),
            ('new psyplot project', partial(self.open_external_files, [])),
            ])

        # ---------------------------------------------------------------------
        # ----------------------------- Menus ---------------------------------
        # ---------------------------------------------------------------------

        # ######################## File menu ##################################

        # --------------------------- New plot --------------------------------

        self.file_menu = QMenu('File', parent=self)
        self.new_plot_action = QAction('New plot', self)
        self.new_plot_action.setStatusTip(
            'Use an existing dataset (or open a new one) to create one or '
            'more plots')
        self.register_shortcut(self.new_plot_action, QKeySequence.New)
        self.new_plot_action.triggered.connect(lambda: self.new_plots(True))
        self.file_menu.addAction(self.new_plot_action)

        # --------------------------- Open project ----------------------------

        self.open_project_menu = QMenu('Open project', self)
        self.file_menu.addMenu(self.open_project_menu)

        self.open_mp_action = QAction('New main project', self)
        self.register_shortcut(self.open_mp_action, QKeySequence.Open)
        self.open_mp_action.setStatusTip('Open a new main project')
        self.open_mp_action.triggered.connect(self.open_mp)
        self.open_project_menu.addAction(self.open_mp_action)

        self.open_sp_action = QAction('Add to current', self)

        self.register_shortcut(
            self.open_sp_action, QKeySequence(
                'Ctrl+Shift+O', QKeySequence.NativeText))
        self.open_sp_action.setStatusTip(
            'Load a project as a sub project and add it to the current main '
            'project')
        self.open_sp_action.triggered.connect(self.open_sp)
        self.open_project_menu.addAction(self.open_sp_action)

        # ----------------------- Save project --------------------------------

        self.save_project_menu = QMenu('Save', parent=self)
        self.file_menu.addMenu(self.save_project_menu)

        self.save_mp_action = QAction('Full psyplot project', self)
        self.save_mp_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.register_shortcut(self.save_mp_action, QKeySequence.Save)
        self.save_mp_action.triggered.connect(self.save_mp)
        self.save_project_menu.addAction(self.save_mp_action)

        self.save_sp_action = QAction('Selected psyplot project', self)
        self.save_sp_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_action.triggered.connect(self.save_sp)
        self.save_project_menu.addAction(self.save_sp_action)

        # ------------------------ Save project as ----------------------------

        self.save_project_as_menu = QMenu('Save as', parent=self)
        self.file_menu.addMenu(self.save_project_as_menu)

        self.save_mp_as_action = QAction('Full psyplot project', self)
        self.save_mp_as_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.register_shortcut(self.save_mp_as_action,
                                       QKeySequence.SaveAs)
        self.save_mp_as_action.triggered.connect(
            partial(self.save_mp, new_fname=True))
        self.save_project_as_menu.addAction(self.save_mp_as_action)

        self.save_sp_as_action = QAction('Selected psyplot project', self)
        self.save_sp_as_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_as_action.triggered.connect(
            partial(self.save_sp, new_fname=True))
        self.save_project_as_menu.addAction(self.save_sp_as_action)

        # -------------------------- Pack project -----------------------------

        self.pack_project_menu = QMenu('Zip project files', parent=self)
        self.file_menu.addMenu(self.pack_project_menu)

        self.pack_mp_action = QAction('Full psyplot project', self)
        self.pack_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.pack_mp_action.triggered.connect(partial(self.save_mp, pack=True))
        self.pack_project_menu.addAction(self.pack_mp_action)

        self.pack_sp_action = QAction('Selected psyplot project', self)
        self.pack_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.pack_sp_action.triggered.connect(partial(self.save_sp, pack=True))
        self.pack_project_menu.addAction(self.pack_sp_action)

        # ------------------------ Export figures -----------------------------

        self.export_project_menu = QMenu('Export figures', parent=self)
        self.file_menu.addMenu(self.export_project_menu)

        self.export_mp_action = QAction('Full psyplot project', self)
        self.export_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.export_mp_action.triggered.connect(self.export_mp)
        self.register_shortcut(
            self.export_mp_action, QKeySequence(
                'Ctrl+E', QKeySequence.NativeText))
        self.export_project_menu.addAction(self.export_mp_action)

        self.export_sp_action = QAction('Selected psyplot project', self)
        self.export_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.register_shortcut(
            self.export_sp_action, QKeySequence(
                'Ctrl+Shift+E', QKeySequence.NativeText))
        self.export_sp_action.triggered.connect(self.export_sp)
        self.export_project_menu.addAction(self.export_sp_action)

        # ------------------------ Close project ------------------------------

        self.file_menu.addSeparator()

        self.close_project_menu = QMenu('Close project', parent=self)
        self.file_menu.addMenu(self.close_project_menu)

        self.close_mp_action = QAction('Full psyplot project', self)
        self.register_shortcut(
            self.close_mp_action, QKeySequence(
                'Ctrl+Shift+W', QKeySequence.NativeText))
        self.close_mp_action.setStatusTip(
            'Close the main project and delete all data and plots out of '
            'memory')
        self.close_mp_action.triggered.connect(
            lambda: psy.close(psy.gcp(True).num))
        self.close_project_menu.addAction(self.close_mp_action)

        self.close_sp_action = QAction('Selected psyplot project', self)
        self.close_sp_action.setStatusTip(
            'Close the selected arrays project and delete all data and plots '
            'out of memory')
        self.register_shortcut(self.close_sp_action, QKeySequence.Close)
        self.close_sp_action.triggered.connect(
            lambda: psy.gcp().close(True, True))
        self.close_project_menu.addAction(self.close_sp_action)

        # ----------------------------- Quit ----------------------------------

        if sys.platform != 'darwin':  # mac os makes this anyway
            self.quit_action = QAction('Quit', self)
            self.quit_action.triggered.connect(self.close)
            self.quit_action.triggered.connect(
                QtCore.QCoreApplication.instance().quit)
            self.register_shortcut(
                self.quit_action, QKeySequence.Quit)
            self.file_menu.addAction(self.quit_action)

        self.menuBar().addMenu(self.file_menu)

        # ######################## Console menu ###############################

        self.console_menu = QMenu('Console', self)
        self.console_menu.addActions(self.console.actions())
        self.menuBar().addMenu(self.console_menu)

        # ######################## Windows menu ###############################

        self.windows_menu = QMenu('Windows', self)
        self.menuBar().addMenu(self.windows_menu)

        # ############################ Help menu ##############################

        self.help_menu = QMenu('Help', parent=self)
        self.menuBar().addMenu(self.help_menu)

        # -------------------------- Preferences ------------------------------

        self.help_action = QAction('Preferences', self)
        self.help_action.triggered.connect(lambda: self.edit_preferences(True))
        self.register_shortcut(self.help_action,
                                       QKeySequence.Preferences)
        self.help_menu.addAction(self.help_action)

        # ---------------------------- About ----------------------------------

        self.about_action = QAction('About', self)
        self.about_action.triggered.connect(self.about)
        self.help_menu.addAction(self.about_action)

        # ---------------------------- Dependencies ---------------------------

        self.dependencies_action = QAction('Dependencies', self)
        self.dependencies_action.triggered.connect(
            lambda: self.show_dependencies(True))
        self.help_menu.addAction(self.dependencies_action)

        self.dockwidgets = []

        # ---------------------------------------------------------------------
        # -------------------------- Dock windows -----------------------------
        # ---------------------------------------------------------------------
        #: tab widget displaying the arrays in current main and sub project
        #: tree widget displaying the open datasets
        self.project_content = ProjectContentWidget(parent=self)
        self.ds_tree = DatasetTree(parent=self)
        #: tree widget displaying the open figures
        self.figures_tree = FiguresTree(parent=self)
        #: help explorer
        self.help_explorer = help_explorer = HelpExplorer(parent=self)
        if help_explorer.viewers['HTML help'].sphinx_thread is not None:
            help_explorer.viewers[
                'HTML help'].sphinx_thread.html_ready.connect(
                    self.focus_on_console)
        #: the DataFrameEditor widgets
        self.dataframeeditors = []
        #: general formatoptions widget
        self.fmt_widget = FormatoptionWidget(
            parent=self, help_explorer=help_explorer,
            console=self.console)

        # load plugin widgets
        self.plugins = plugins = OrderedDict([
            ('console', self.console),
            ('project_content', self.project_content),
            ('ds_tree', self.ds_tree),
            ('figures_tree', self.figures_tree),
            ('help_explorer', self.help_explorer),
            ('fmt_widget', self.fmt_widget),
            ])
        self.default_plugins = list(plugins)
        for plugin_name, w_class in six.iteritems(rcParams.load_plugins()):
            plugins[plugin_name] = w_class(parent=self)

        self.add_mp_to_menu()
        psy.Project.oncpchange.connect(self.eventually_add_mp_to_menu)
        self.windows_menu.addSeparator()

        self.window_layouts_menu = QMenu('Window layouts', self)
        self.restore_layout_action = QAction('Restore default layout', self)
        self.restore_layout_action.triggered.connect(self.setup_default_layout)
        self.window_layouts_menu.addAction(self.restore_layout_action)
        self.windows_menu.addMenu(self.window_layouts_menu)

        self.panes_menu = QMenu('Panes', self)
        self.windows_menu.addMenu(self.panes_menu)

        self.dataframe_menu = QMenu('DataFrame editors', self)
        self.dataframe_menu.addAction(
            'New Editor', partial(self.new_data_frame_editor, None,
                                  'DataFrame Editor'))
        self.dataframe_menu.addSeparator()
        self.windows_menu.addMenu(self.dataframe_menu)

        self.central_widgets_menu = menu = QMenu('Central widget', self)
        self.windows_menu.addMenu(menu)
        self.central_widgets_actions = group = QActionGroup(self)
        group.setExclusive(True)

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

        self.console.help_explorer = help_explorer
        psyp.default_print_func = partial(help_explorer.show_rst,
                                          oname='formatoption_docs')
        psy.PlotterInterface._print_func = psyp.default_print_func
        self.setCentralWidget(self.console)

        # make sure that the plots are shown between the project content and
        # the help explorer widget
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)

        # make sure that the formatoption widgets are shown between the
        # project content and the help explorer widget
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)

        # ---------------------------------------------------------------------
        # ------------------------------ closure ------------------------------
        # ---------------------------------------------------------------------
        if show:
            self.help_explorer.show_intro(self.console.intro_msg)

        # ---------------------------------------------------------------------
        # ------------------------- open_files_server -------------------------
        # ---------------------------------------------------------------------
        self.callbacks = {'new_plot': self.open_external.emit,
                          'change_cwd': self._change_cwd,
                          'run_script': self.console.run_script.emit,
                          'command': self.console.run_command.emit,
                          }

        # Server to open external files on a single instance
        self.open_files_server = socket.socket(socket.AF_INET,
                                               socket.SOCK_STREAM,
                                               socket.IPPROTO_TCP)

        if rcParams['main.listen_to_port']:
            self._file_thread = Thread(target=self.start_open_files_server)
            self._file_thread.setDaemon(True)
            self._file_thread.start()

            self.open_external.connect(self._open_external_files)

        self.config_pages.extend([GuiRcParamsWidget, PsyRcParamsWidget])

        # display the statusBar
        statusbar = self.statusBar()
        self.figures_label = QLabel()
        statusbar.addWidget(self.figures_label)
        self.plugin_label = QLabel()
        statusbar.addWidget(self.plugin_label)

        self.default_widths = {}

        self.setup_default_layout()

        if show:
            self.showMaximized()

        # save the default widths after they have been shown
        for w in self.plugins.values():
            if w.dock is not None:
                self.default_widths[w] = w.dock.size().width()

        # hide plugin widgets that should be hidden at startup. Although this
        # has been executed by :meth:`setup_default_layout`, we have to execute
        # it again after the call of showMaximized
        for name, w in self.plugins.items():
            if name != self.central_widget_key:
                w.to_dock(self)
                if w.hidden:
                    w.hide_plugin()
            else:
                w.create_central_widget_action(self).setChecked(True)

        self._is_open = True
Esempio n. 14
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)
Esempio n. 15
0
    def __init__(self):
        super(MainWindow, self).__init__()

        #: list of figures from the psyplot backend
        self.figures = []
        self.error_msg = PyErrorMessage(self)
        self.setDockOptions(
            QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks |
            QMainWindow.AllowTabbedDocks)
        #: Inprocess console
        self.console = ConsoleWidget(parent=self)
        self.project_actions = {}

        # ---------------------------------------------------------------------
        # ----------------------------- Menus ---------------------------------
        # ---------------------------------------------------------------------

        # ######################## File menu ##################################

        # --------------------------- New plot --------------------------------

        self.file_menu = QMenu('File', parent=self)
        self.new_plot_action = QAction('New plot', self)
        self.new_plot_action.setStatusTip(
            'Use an existing dataset (or open a new one) to create one or '
            'more plots')
        self.new_plot_action.setShortcut(QKeySequence.New)
        self.new_plot_action.triggered.connect(self.new_plots)
        self.file_menu.addAction(self.new_plot_action)

        # --------------------------- Open project ----------------------------

        self.open_project_menu = QMenu('Open project', self)
        self.file_menu.addMenu(self.open_project_menu)

        self.open_mp_action = QAction('New main project', self)
        self.open_mp_action.setShortcut(QKeySequence.Open)
        self.open_mp_action.setStatusTip('Open a new main project')
        self.open_mp_action.triggered.connect(self.open_mp)
        self.open_project_menu.addAction(self.open_mp_action)

        self.open_sp_action = QAction('Add to current', self)
        self.open_sp_action.setShortcut(QKeySequence(
            'Ctrl+Shift+O', QKeySequence.NativeText))
        self.open_sp_action.setStatusTip(
            'Load a project as a sub project and add it to the current main '
            'project')
        self.open_sp_action.triggered.connect(self.open_sp)
        self.open_project_menu.addAction(self.open_sp_action)

        # ----------------------- Save project --------------------------------

        self.save_project_menu = QMenu('Save project', parent=self)
        self.file_menu.addMenu(self.save_project_menu)

        self.save_mp_action = QAction('All', self)
        self.save_mp_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.save_mp_action.setShortcut(QKeySequence.Save)
        self.save_mp_action.triggered.connect(self.save_mp)
        self.save_project_menu.addAction(self.save_mp_action)

        self.save_sp_action = QAction('Selected', self)
        self.save_sp_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_action.triggered.connect(self.save_sp)
        self.save_project_menu.addAction(self.save_sp_action)

        # ------------------------ Save project as ----------------------------

        self.save_project_menu = QMenu('Save project', parent=self)
        self.file_menu.addMenu(self.save_project_menu)

        self.save_project_as_menu = QMenu('Save project as', parent=self)
        self.file_menu.addMenu(self.save_project_as_menu)

        self.save_mp_as_action = QAction('All', self)
        self.save_mp_as_action.setStatusTip(
            'Save the entire project into a pickle file')
        self.save_mp_as_action.setShortcut(QKeySequence.SaveAs)
        self.save_mp_as_action.triggered.connect(
            partial(self.save_mp, new_name=True))
        self.save_project_as_menu.addAction(self.save_mp_as_action)

        self.save_sp_as_action = QAction('Selected', self)
        self.save_sp_as_action.setStatusTip(
            'Save the selected sub project into a pickle file')
        self.save_sp_as_action.triggered.connect(
            partial(self.save_sp, new_name=True))
        self.save_project_as_menu.addAction(self.save_sp_as_action)

        # -------------------------- Pack project -----------------------------

        self.pack_project_menu = QMenu('Zip project files', parent=self)
        self.file_menu.addMenu(self.pack_project_menu)

        self.pack_mp_action = QAction('All', self)
        self.pack_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.pack_mp_action.triggered.connect(partial(self.save_mp, pack=True))
        self.pack_project_menu.addAction(self.pack_mp_action)

        self.pack_sp_action = QAction('Selected', self)
        self.pack_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.pack_sp_action.triggered.connect(partial(self.save_sp, pack=True))
        self.pack_project_menu.addAction(self.pack_sp_action)

        # ------------------------ Export figures -----------------------------

        self.export_project_menu = QMenu('Export figures', parent=self)
        self.file_menu.addMenu(self.export_project_menu)

        self.export_mp_action = QAction('All', self)
        self.export_mp_action.setStatusTip(
            'Pack all the data of the main project into one folder')
        self.export_mp_action.triggered.connect(self.export_mp)
        self.export_mp_action.setShortcut(QKeySequence(
            'Ctrl+E', QKeySequence.NativeText))
        self.export_project_menu.addAction(self.export_mp_action)

        self.export_sp_action = QAction('Selected', self)
        self.export_sp_action.setStatusTip(
            'Pack all the data of the current sub project into one folder')
        self.export_sp_action.setShortcut(QKeySequence(
            'Ctrl+Shift+E', QKeySequence.NativeText))
        self.export_sp_action.triggered.connect(self.export_sp)
        self.export_project_menu.addAction(self.export_sp_action)

        # ------------------------ Close project ------------------------------

        self.file_menu.addSeparator()

        self.close_project_menu = QMenu('Close project', parent=self)
        self.file_menu.addMenu(self.close_project_menu)

        self.close_mp_action = QAction('Main project', self)
        self.close_mp_action.setStatusTip(
            'Close the main project and delete all data and plots out of '
            'memory')
        self.close_mp_action.setShortcut(QKeySequence.Close)
        self.close_mp_action.triggered.connect(
            lambda: psy.close(psy.gcp(True).num))
        self.close_project_menu.addAction(self.close_mp_action)

        self.close_sp_action = QAction('Only selected', self)
        self.close_sp_action.setStatusTip(
            'Close the selected arrays project and delete all data and plots '
            'out of memory')
        self.close_sp_action.setShortcut(QKeySequence(
            'Ctrl+Shift+W', QKeySequence.NativeText))
        self.close_sp_action.triggered.connect(
            lambda: psy.gcp().close(True, True))
        self.close_project_menu.addAction(self.close_sp_action)

        # ------------------------ Quit ------------------------------

        if sys.platform != 'darwin':  # mac os makes this anyway
            self.quit_action = QAction('Quit', self)
            self.quit_action.triggered.connect(
                QtCore.QCoreApplication.instance().quit)
            self.quit_action.setShortcut(QKeySequence.Quit)
            self.file_menu.addAction(self.quit_action)

        self.menuBar().addMenu(self.file_menu)

        # ######################## Console menu ###############################

        self.console_menu = QMenu('Console', self)
        self.console_menu.addActions(self.console.actions())
        self.menuBar().addMenu(self.console_menu)

        # ######################## Windows menu ###############################

        self.windows_menu = QMenu('Windows', self)
        self.menuBar().addMenu(self.windows_menu)

        # ---------------------------------------------------------------------
        # -------------------------- Dock windows -----------------------------
        # ---------------------------------------------------------------------
        #: tab widget displaying the arrays in current main and sub project
        self.project_content = ProjectContentWidget(parent=self)
        self.addDockWidget(Qt.LeftDockWidgetArea,
                           self.project_content.to_dock('Plot objects', self),
                           'pane')
        #: tree widget displaying the open datasets
        self.ds_tree = DatasetTree(parent=self)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.ds_tree.to_dock(
            'Datasets', self), 'pane')
        #: tree widget displaying the open figures
        self.figures_tree = FiguresTree(parent=self)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.figures_tree.to_dock(
            'Figures', self), 'pane')
        #: help explorer
        self.help_explorer = help_explorer = HelpExplorer(parent=self)
        self.addDockWidget(Qt.RightDockWidgetArea, help_explorer.to_dock(
            'Help explorer', self), 'pane')
        #: general formatoptions widget
        self.fmt_widget = FormatoptionWidget(
            parent=self, help_explorer=help_explorer,
            shell=self.console.kernel_client.kernel.shell)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.fmt_widget.to_dock(
            'Formatoptions', self), 'pane')

        self.windows_menu.addSeparator()
        self.add_mp_to_menu()
        psy.Project.oncpchange.connect(self.eventually_add_mp_to_menu)

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

        self.console.help_explorer = help_explorer
        psyp.default_print_func = partial(help_explorer.show_rst,
                                          oname='formatoption_docs')
        psy._PlotterInterface._print_func = psyp.default_print_func
        self.setCentralWidget(self.console)

        # make sure that the plots are shown between the project content and
        # the help explorer widget
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)

        # make sure that the formatoption widgets are shown between the
        # project content and the help explorer widget
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)

        # Server to open external files on a single instance
        self.open_files_server = socket.socket(socket.AF_INET,
                                               socket.SOCK_STREAM,
                                               socket.IPPROTO_TCP)

        self.showMaximized()

        self._file_thread = Thread(target=self.start_open_files_server)
        self._file_thread.setDaemon(True)
        self._file_thread.start()

        self.open_external.connect(self.open_external_files)

        # ---------------------------------------------------------------------
        # ------------------------------ closure ------------------------------
        # ---------------------------------------------------------------------
        self.help_explorer.show_intro(self.console.intro_msg)
Esempio n. 16
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)
Esempio n. 17
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())