Source code for plotshop.fig_editor.main_window

"""
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