"""Common functions used for the psyplot gui"""
# 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 inspect
import logging
import os.path as osp
import sys
import traceback as tb
from functools import partial
import six
from psyplot_gui.compat.qtcompat import (
QAction,
QDesktopWidget,
QDockWidget,
QErrorMessage,
QIcon,
QInputDialog,
QRegExpValidator,
QtCore,
QToolButton,
)
if six.PY2:
try:
import CStringIO as io
except ImportError:
import StringIO as io
else:
import io
[docs]
def is_running_tests():
"""Check if there are any GUI tests running
This function returns the :attr:`psyplot_gui.UNIT_TESTING` variable"""
import psyplot_gui
return psyplot_gui.UNIT_TESTING
[docs]
def get_module_path(modname):
"""Return module `modname` base path"""
return osp.abspath(osp.dirname(sys.modules[modname].__file__))
[docs]
def get_icon(name):
"""Get the path to an icon in the icons directory"""
return osp.join(get_module_path("psyplot_gui"), "icons", name)
[docs]
class DockMixin(object):
"""A mixin class to define psyplot_gui plugins
Notes
-----
Each external plugin should set the :attr:`dock_position` and the
:attr:`title` attribute!
"""
_set_central_action = _view_action = None
#: The position of the plugin
dock_position = None
#: The title of the plugin
title = None
#: The class to use for the DockWidget
dock_cls = QDockWidget
#: The config page for this widget. Should inherit the
#: :class:`psyplot_gui.preferences.ConfigPage` widget
config_page = None
#: Boolean that is True if the dock widget should be hidden automatically
#: after startup
hidden = False
#: The instance of :class:`QDockWidget` of this plugin
dock = None
@property
def is_shown(self):
"""Boolean that is True, if the dock widget is shown"""
return (
self.dock is not None and self.dock.toggleViewAction().isChecked()
)
[docs]
def to_dock(
self, main, title=None, position=None, docktype="pane", *args, **kwargs
):
if title is None:
title = self.title
if title is None:
raise ValueError("No title specified for the %s widget" % (self))
if position is None:
position = self.dock_position
if position is None:
raise ValueError(
"No position specified for the %s widget (%s)" % (title, self)
)
self.title = title
self.dock_position = position
if self.dock is None:
self.dock = self.dock_cls(title, main)
self.dock.setWidget(self)
main.dockwidgets.append(self.dock)
self.create_central_widget_action(main)
self.create_view_action(main, docktype)
self.position_dock(main, *args, **kwargs)
config_page = self.config_page
if config_page is not None:
main.config_pages.append(config_page)
return self.dock
[docs]
def position_dock(self, main, *args, **kwargs):
"""Set the position of the dock widget
This method places the plugin widget at the desired dock position
(by default, indicated with the :attr:`dock_position` attribute)
Parameters
----------
main: psyplot_gui.main.Mainwindow
The main window where the dock is added"""
main.addDockWidget(self.dock_position, self.dock, *args, **kwargs)
[docs]
def show_plugin(self):
"""Show the plugin widget"""
a = self.dock.toggleViewAction()
if not a.isChecked():
a.trigger()
[docs]
def hide_plugin(self):
"""Hide the plugin widget"""
a = self.dock.toggleViewAction()
if a.isChecked():
a.trigger()
[docs]
def show_status_message(self, msg):
"""Show a status message"""
try:
self.dock.parent().plugin_label.setText(msg)
except AttributeError:
pass
[docs]
def create_view_action(self, main, docktype="pane"):
if self._view_action is None:
self._view_action = action = self.dock.toggleViewAction()
if docktype == "pane":
main.panes_menu.addAction(action)
elif docktype == "df":
main.dataframe_menu.addAction(action)
return self._view_action
[docs]
def remove_plugin(self):
"""Remove this plugin and close it"""
mainwindow = self.dock.parent() if self.dock else self.parent()
key = next(
(key for key, w in mainwindow.plugins.items() if w is self), None
)
if mainwindow.centralWidget() is self:
mainwindow.set_central_widget(
mainwindow.__class__.central_widget_key
)
if self._view_action is not None:
mainwindow.panes_menu.removeAction(self._view_action)
mainwindow.dataframe_menu.removeAction(self._view_action)
if self._set_central_action is not None:
mainwindow.central_widgets_menu.removeAction(
self._set_central_action
)
if key is not None:
del mainwindow.plugins[key]
if self.dock is not None:
mainwindow.removeDockWidget(self.dock)
self.dock.close()
self.close()
[docs]
class ListValidator(QRegExpValidator):
"""A validator class to validate that a string consists of strings in a
list of strings"""
def __init__(self, valid, sep=",", *args, **kwargs):
"""
Parameters
----------
valid: list of str
The possible choices
sep: str, optional
The separation pattern
``*args,**kwargs``
Determined by PyQt5.QtGui.QValidator
"""
patt = QtCore.QRegExp("^((%s)(;;)?)+$" % "|".join(valid))
super(QRegExpValidator, self).__init__(patt, *args, **kwargs)
[docs]
class PyErrorMessage(QErrorMessage):
"""Widget designed to display python errors via the :meth:`showTraceback`
method"""
[docs]
def showTraceback(self, header=None):
if is_running_tests():
raise
s = io.StringIO()
tb.print_exc(file=s)
last_tb = "<p>" + "<br>".join(s.getvalue().splitlines()) + "</p>"
header = header + "\n" if header else ""
self.showMessage(header + last_tb)
available_width = QDesktopWidget().availableGeometry().width() / 3.0
available_height = QDesktopWidget().availableGeometry().height() / 3.0
width = self.sizeHint().width()
height = self.sizeHint().height()
# The message window should cover at least one third of the screen
self.resize(max(available_width, width), max(available_height, height))
[docs]
def excepthook(self, type, value, traceback):
s = io.StringIO()
tb.print_exception(type, value, traceback, file=s)
last_tb = "<p>" + "<br>".join(s.getvalue().splitlines()) + "</p>"
header = value.message if six.PY2 else str(value)
self.showMessage(header + "\n" + last_tb)
available_width = QDesktopWidget().availableGeometry().width() / 3.0
available_height = QDesktopWidget().availableGeometry().height() / 3.0
width = self.sizeHint().width()
height = self.sizeHint().height()
# The message window should cover at least one third of the screen
self.resize(max(available_width, width), max(available_height, height))
[docs]
class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""
def __init__(self, logger, log_level=logging.INFO):
self.logger = logger
self.log_level = log_level
self.linebuf = ""
[docs]
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.log_level, line.rstrip())