"""
Module plotshop.fig_editor.main_window: QT-based Figure Editor
-----------------------------------------------------------------
This is an extended figure editor based on the PyQT5 backend of matplotlib.
Functionality:
- easily plot from tfs files
- change plot attributes by douple click on element
- change plot attributes via icon in navigation bar
- drag text around
- move the legend to predefined positions
Planned:
- copy paste between plots
- have a "draggable" switch for text objects
- add texts to options_figure to find invisible texts again
WARNING: Pre-Alpha version.
"""
import logging
import sys
from PyQt5 import QtGui, QtWidgets, QtCore
import matplotlib
import numpy as np
import options_artists
from gui_utils import get_icon
import io_utils as io
from main_window_widgets import (
FigureCanvasExt, NavigationToolbar, LogDialog, LogStatusBar, DragHandler
)
LOG = logging.getLogger(__name__)
_VERSION = "0.0_prealpha"
_AUTHOR = "Joschua"
[docs]class MainWindow(QtWidgets.QMainWindow):
dpi = 100
figure_size = (8.0, 4.0)
status_bar_height = 16
nav_toolbar_height = NavigationToolbar.ICON_SIZE + 8
zoomscale = 1.5 # scaling of scrollzoom
autozoom = 0.05 # percentage of points to cut
bordertol = 0.05 # after autozoom add this to limits
picktol = 5 # picker tolerance in pixels
def __init__(self, fig=None, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setWindowTitle('TFS-Plotter')
self.setWindowIcon(get_icon("photo"))
self._create_logger()
self._create_menu()
self._create_main_frame(fig)
# Creators ###############################################################
def _create_logger(self):
""" Create a logging dialog window and a status bar """
root_logger = logging.getLogger("")
root_logger.setLevel(logging.DEBUG)
fmt = "%(asctime)s %(levelname)s %(message)s"
datefmt = '%d/%m/%Y %H:%M:%S'
formatter = logging.Formatter(fmt, datefmt)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
console_handler.setLevel(logging.DEBUG)
root_logger.addHandler(console_handler)
self._log_dialog = LogDialog(
fmt=fmt,
datefmt=datefmt,
parent=self
)
root_logger.addHandler(self._log_dialog)
self._status_bar = LogStatusBar(
fmt="%(asctime)s %(message)s",
datefmt='%H:%M:%S',
parent=self
)
self._status_bar.setFixedHeight(self.status_bar_height)
root_logger.addHandler(self._status_bar)
self.setStatusBar(self._status_bar)
def _create_menu(self):
""" Add a menu """
self.file_menu = self.menuBar().addMenu("&File")
load_action = self._create_action("&Load Data", slot=self._load_data,
shortcut="Ctrl+O", tip="Load Data")
save_action = self._create_action("&Save plot", slot=self._save_figure,
shortcut="Ctrl+S", tip="Save the plot")
import_action = self._create_action("&Import plot", slot=self._import_dly,
shortcut="Ctrl+Shift+O", tip="Import the plot")
export_action = self._create_action("&Export plot", slot=self._export_dly,
shortcut="Ctrl+Shift+S", tip="Export the plot")
quit_action = self._create_action("&Quit", slot=self.close,
shortcut="Ctrl+Q", tip="Close the application")
self._add_actions(self.file_menu, (load_action, save_action, import_action, export_action,
None, quit_action))
self.help_menu = self.menuBar().addMenu("&Help")
about_action = self._create_action("&About", slot=self._on_about,
shortcut='F1', tip='About the demo')
log_action = self._create_action("&Show Log", slot=self.show_log,
shortcut='F2', tip='Show the log.')
self._add_actions(self.help_menu, (about_action, log_action))
def _create_main_frame(self, figure=None):
""" Sets up the main frame of the window """
self.main_frame = QtWidgets.QWidget(self)
if figure is None:
figure = matplotlib.figure.Figure()
# self.figure.add_subplot(111)
self.canvas = FigureCanvasExt(figure)
self.canvas.setParent(self.main_frame)
# Connect events and save them to list
self.cids = [] # save events for disconnecting later
self._connect_events()
# Create the navigation toolbar, tied to the canvas
#
self.mpl_toolbar = NavigationToolbar(self.canvas,
save_fun=self._save_figure,
load_fun=self._load_data,
import_fun=self._import_dly,
export_fun=self._export_dly,
parent=self
)
self.mpl_toolbar.setFixedHeight(self.nav_toolbar_height)
# create a placeholder for the zoomstack:
self.scrolling = False
self.zoom_stack = []
#
# Layout with box sizers
#
hbox = QtWidgets.QHBoxLayout()
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.canvas)
vbox.addWidget(self.mpl_toolbar)
vbox.addLayout(hbox)
self.main_frame.setLayout(vbox)
self.setCentralWidget(self.main_frame)
# Update and draw
self.update_figure(figure)
# Creator Helpers ########################################################
@staticmethod
def _add_actions(target, actions):
for action in actions:
if action is None:
target.addSeparator()
else:
target.addAction(action)
def _create_action(self, text, slot=None, shortcut=None,
icon=None, tip=None, checkable=False,
):
""" QAction wrapper """
action = QtWidgets.QAction(text, self)
if icon is not None:
action.setIcon(QtGui.QIcon(":/%s.png" % icon))
if shortcut is not None:
action.setShortcut(shortcut)
if tip is not None:
action.setToolTip(tip)
action.setStatusTip(tip)
if slot is not None:
action.triggered.connect(slot)
if checkable:
action.setCheckable(True)
return action
def _connect_events(self):
# Bind the clicking events to our handlers
#
self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) # needed for key events to regisiter
self.canvas.setFocus() # sets focus without clicking on it
self.cids.append(self.canvas.mpl_connect('pick_event', self._on_pick))
self.cids.append(self.canvas.mpl_connect('draw_event', self._on_draw))
self.cids.append(self.canvas.mpl_connect('key_press_event', self._on_key_press))
self.cids.append(self.canvas.mpl_connect('key_release_event', self._on_key_release))
self.cids.append(self.canvas.mpl_connect('button_press_event', self._on_mouse_click))
self.cids.append(self.canvas.mpl_connect("button_release_event", self._on_mouse_release))
self.cids.append(self.canvas.mpl_connect("motion_notify_event", self._on_mouse_move))
self.cids.append(self.canvas.mpl_connect('scroll_event', self._on_scroll))
self._dragged = None
def _disconnect_events(self):
for cid in self.cids:
self.canvas.mpl_disconnect(cid)
self.cids = []
# Listeners ###############################################################
def _on_about(self):
msg = "A small gui to plot tfs files easily and adapt matplotlib plots on the fly.\n"
msg += "\n"
msg += "Version: {:s}\n".format(_VERSION)
msg += "Author: {:s}\n".format(_AUTHOR)
QtWidgets.QMessageBox.about(self, "About the tfs plotter.", msg)
def _on_pick(self, event):
# The event received here is of the type
# matplotlib.backend_bases.PickEvent
#
if event.mouseevent.button == 1 and event.mouseevent.dblclick:
LOG.debug("You've dblclicked on : {:s}".format(event.artist))
options_artists.change_properties(event.artist, self)
self.update_figure()
self._dragged = None
elif isinstance(event.artist, matplotlib.text.Text):
self._dragged = DragHandler(event.artist, event.mouseevent)
def _on_draw(self, event):
pass
def _on_mouse_click(self, event):
""" Single mouse click into window """
ax = event.canvas.figure.gca()
if event.button == 2:
# middle button -> zoom in to 90% of visible data
if self.mpl_toolbar._nav_stack() is None:
self.mpl_toolbar.push_current() # set current view as home
xlim = ax.get_xlim()
ylim = ax.get_ylim()
all_data = np.array([])
for line in ax.lines:
data = line.get_data()
# filter
xmask = (xlim[0] < data[0]) & (data[0] < xlim[1])
ymask = (ylim[0] < data[1]) & (data[1] < ylim[1])
all_data = np.append(all_data, np.array(data[1])[xmask & ymask])
if len(all_data):
drop_idx = int(round(len(all_data) * self.autozoom))
if drop_idx > 0:
all_data = sorted(all_data)[drop_idx:-drop_idx]
ymin = min(all_data)
ymax = max(all_data)
d = (ymax - ymin) * self.bordertol
ax.set_ylim((ymin-d, ymax+d))
self.update_figure()
self.mpl_toolbar.push_current()
if event.button == 3:
# right button -> return in history
self.mpl_toolbar.back()
def _on_mouse_release(self, event):
if self._dragged is not None:
self._dragged.move(event)
self._dragged = None
self.update_figure()
def _on_mouse_move(self, event):
if self._dragged is not None:
self._dragged.move(event)
self.update_figure()
def _on_scroll(self, event):
""" Zoom with respect to mouse-position or center of axes. """
if self.mpl_toolbar._nav_stack() is None:
self.mpl_toolbar.push_current() # set current view as home
ax = event.canvas.figure.gca()
xlim = ax.get_xlim()
ylim = ax.get_ylim()
if event.inaxes:
origin_x = event.xdata
origin_y = event.ydata
else:
origin_x = (xlim[0] + xlim[1]) / 2.
origin_y = (ylim[0] + ylim[1]) / 2.
scale = self.zoomscale ** event.step
new_xlim = [None, None]
new_ylim = [None, None]
new_xlim[0] = origin_x + (xlim[0] - origin_x) * scale
new_xlim[1] = origin_x + (xlim[1] - origin_x) * scale
new_ylim[0] = origin_y + (ylim[0] - origin_y) * scale
new_ylim[1] = origin_y + (ylim[1] - origin_y) * scale
ax.set_xlim(new_xlim)
ax.set_ylim(new_ylim)
self.update_figure()
def _on_key_press(self, event):
# LOG.info("Key press '{}' has been registered".format(event.key))
if "ctrl+c" == event.key:
self._copy_selection()
elif "ctrl+v" == event.key:
self._paste_from_clipboard()
elif "ctrl+x" == event.key:
self._copy_selection()
self._delete_selection()
elif "delete" == event.key:
self._delete_selection()
def _on_key_release(self, event):
# LOG.info("Key release '{}' has been registered".format(event.key))
pass
# Copy/Pase ######################################
def _copy_selection(self):
LOG.debug("Copy selection has been triggered.")
pass # TODO https://stackoverflow.com/questions/40225270/copy-paste-multiple-items-from-qtableview-in-pyqt4
def _delete_selection(self):
LOG.debug("Delete selection has been triggered.")
pass # TODO
def _paste_from_clipboard(self):
LOG.debug("Paste has been triggered.")
pass # TODO
# Import/Export ##################################
def _import_dly(self):
figure = io.load_dly()
if figure:
self.update_figure(figure)
def _export_dly(self):
io.save_dly(self.canvas.figure)
def _load_data(self):
figure = io.load_tfs()
if figure:
self.update_figure(figure)
def _save_figure(self):
io.save_figure(self.canvas.figure)
# Public Functions #######################################################
[docs] def show_log(self):
""" Show the logging window. """
self._log_dialog.show()
[docs] def update_figure(self, figure=None):
""" Redraw current figure or provide new one. """
if figure:
self.canvas.update_figure(figure)
figure.set_dpi(self.dpi)
figure.set_size_inches(self.figure_size)
figure.tight_layout()
self.canvas.set_pickers(self.picktol)
self.canvas.draw()
[docs] def get_figure(self):
""" Return the currently used figure. """
return self.canvas.figure