"""Core module for the psyplot graphical user interface
This module redefines the :class:`psyplot.project.Project` class with
additional features for an interactive usage with graphical qt user interface.
There is no need to import this module because the
:class:`GuiProject` class defined here replaces the project class in the
:mod:`psyplot.project` module."""
# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
# SPDX-License-Identifier: LGPL-3.0-only
import errno
import logging
import os
import pickle
import socket
import sys
from collections import OrderedDict, defaultdict
from functools import partial
from threading import Thread
import matplotlib as mpl
import psyplot
import psyplot.data as psyd
import psyplot.plotter as psyp
import psyplot.project as psy
import six
import xarray as xr
from psyplot.config.rcsetup import get_configdir
from psyplot.docstring import docstrings
from psyplot.utils import get_default_value
import psyplot_gui
from psyplot_gui import rcParams
from psyplot_gui.common import PyErrorMessage, StreamToLogger, get_icon
from psyplot_gui.compat.qtcompat import (
QAction,
QActionGroup,
QApplication,
QDesktopWidget,
QFileDialog,
QIcon,
QInputDialog,
QKeySequence,
QLabel,
QMainWindow,
QMenu,
QMessageBox,
Qt,
QtCore,
with_qt5,
)
from psyplot_gui.console import ConsoleWidget
from psyplot_gui.content_widget import (
DatasetTree,
FiguresTree,
ProjectContentWidget,
)
from psyplot_gui.dataframeeditor import DataFrameEditor
from psyplot_gui.dependencies import DependenciesDialog
from psyplot_gui.fmt_widget import FormatoptionWidget
from psyplot_gui.help_explorer import HelpExplorer
from psyplot_gui.plot_creator import PlotCreator
from psyplot_gui.preferences import (
GuiRcParamsWidget,
Prefences,
PsyRcParamsWidget,
)
# change backend here before the project module is imported
backend = rcParams["backend"]
if backend is not None:
if backend == "psyplot":
backend = "module://psyplot_gui.backend"
mpl.use(backend)
#: The :class:`PyQt5.QtWidgets.QMainWindow` of the graphical user interface
mainwindow = None
def _set_mainwindow(obj):
global mainwindow
mainwindow = obj
[docs]
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
[docs]
def focus_on_console(self, *args, **kwargs):
"""Put focus on the ipython console"""
self.console._control.setFocus()
[docs]
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
[docs]
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)
[docs]
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)
[docs]
def load_mp_preset(self):
self._load_preset(psy.gcp(True))
[docs]
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)
[docs]
def open_mp(self, *args, **kwargs):
"""Open a new main project"""
self._open_project(main=True)
[docs]
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)
[docs]
def save_mp(self, *args, **kwargs):
"""Save the current main project."""
self._save_project(psy.gcp(True), **kwargs)
[docs]
def save_sp(self, *args, **kwargs):
"""Save the current sub project."""
self._save_project(psy.gcp(), **kwargs)
[docs]
def save_sp_preset(self):
self._save_preset(psy.gcp())
[docs]
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>"
)
[docs]
def export_mp(self, *args, **kwargs):
self._export_project(psy.gcp(True), **kwargs)
[docs]
def export_sp(self, *args, **kwargs):
self._export_project(psy.gcp(), **kwargs)
[docs]
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_()
[docs]
def excepthook(self, type, value, traceback):
"""A method to replace the sys.excepthook"""
self.error_msg.excepthook(type, value, traceback)
[docs]
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_()
[docs]
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",
"""<b>psyplot: Interactive data visualization with python</b>
<br>Copyright © 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://fontawesome.com/">Fontawesom</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,
)
[docs]
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_()
[docs]
def reset_rcParams(self):
rcParams.update_from_defaultParams()
psy.rcParams.update_from_defaultParams()
[docs]
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)
[docs]
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)
)
)
[docs]
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()
[docs]
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" ")
[docs]
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",
"decoder",
)
[docs]
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)
[docs]
@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,
decoder=None,
):
"""
Open external files
Parameters
----------
%(make_plot.parameters.fnames|project|engine|plot_method|name|dims|encoding|enable_post|seaborn_style|concat_dim|chname|preset|decoder)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 = (files[0] for files in fnames if len(files) == 1)
alternative_paths = defaultdict(lambda: next(single_files, None))
alternative_paths.update(
list(files for files in fnames if len(files) == 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)
)
if decoder:
self.plot_creator.set_decoder(decoder)
self.plot_creator.exec_()
return True
def _open_external_files(self, args):
self.open_external_files(*args)
[docs]
@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,
decoder=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,
decoder,
)
psyplot.with_gui = True
return mainwindow
[docs]
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)
[docs]
@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_())
[docs]
def closeEvent(self, event):
"""closeEvent reimplementation"""
if not self._is_open or (self._is_open and self.close()):
self._is_open = False
event.accept()
[docs]
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()