Source code for cytoflowgui.matplotlib_backend_local
#!/usr/bin/env python3.8
# coding: latin-1
# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2022
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
cytoflowgui.matplotlib_backend_local
------------------------------------
A matplotlib backend that renders across a process boundary. This module
has the "local" canvas -- the part that actually renders to a (Qt) window.
By default, matplotlib only works in one thread. For a GUI application, this
is a problem because when matplotlib is working (ie, scaling a bunch of data
points) the GUI freezes.
This module and `matplotlib_backend_remote` implement a matplotlib backend
where the plotting done in one process (ie via pyplot, etc) shows up in a
canvas running in another process (the GUI). The canvas is the interface
across the process boundary: a "local" canvas, which is a GUI widget (in this
case a QWidget) and a "remote" canvas (running in the process where
pyplot.plot() etc. are used.) The remote canvas is a subclass of the Agg
renderer; when draw() is called, the remote canvas pulls the current buffer
out of the renderer and pushes it through a pipe to the local canvas, which
draws it on the screen. blit() is implemented too.
This takes care of one direction of data flow, and would be enough if we were
just plotting. However, we want to use matplotlib widgets as well, which
means there's data flowing from the local canvas to the remote canvas too.
The local canvas is a subclass of ``matplotlib.backends.backend_qt5agg.FigureCanvasQTAgg``,
which is itself a sublcass of QWidget. The local canvas overrides several of
the event handlers, passing the event information to the remote canvas which
in turn runs the matplotlib event handlers.
"""
import time, threading, logging, sys, traceback
import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.backends.backend_agg import FigureCanvasAgg
from pyface.qt import QtCore, QtGui
logger = logging.getLogger(__name__)
DEBUG = 0
[docs]class Msg(object):
"""
Messages sent between the local and remote canvases.
There is an identical class in `matplotlib_backend_remote` because we
don't want these two modules requiring one another
"""
DRAW = "DRAW"
BLIT = "BLIT"
WORKING = "WORKING"
RESIZE_EVENT = "RESIZE"
MOUSE_PRESS_EVENT = "MOUSE_PRESS"
MOUSE_MOVE_EVENT = "MOUSE_MOVE"
MOUSE_RELEASE_EVENT = "MOUSE_RELEASE"
MOUSE_DOUBLE_CLICK_EVENT = "MOUSE_DOUBLE_CLICK"
DPI = "DPI"
PRINT = "PRINT"
[docs]def log_exception():
"""Catch and log exceptions (with their tracebacks"""
(exc_type, exc_value, tb) = sys.exc_info()
err_string = traceback.format_exception_only(exc_type, exc_value)[0]
err_loc = traceback.format_tb(tb)[-1]
err_ctx = threading.current_thread().name
logger.debug("Exception in {0}:\n{1}"
.format(err_ctx, "".join( traceback.format_exception(exc_type, exc_value, tb) )))
logger.error("Error: {0}\nLocation: {1}Thread: {2}" \
.format(err_string, err_loc, err_ctx) )
[docs]class FigureCanvasQTAggLocal(FigureCanvasQTAgg):
"""
The local canvas; ie, the one in the GUI.
"""
def __init__(self, figure, child_conn, working_pixmap):
FigureCanvasQTAgg.__init__(self, figure)
self._drawRect = None
self.child_conn = child_conn
# set up the "working" pixmap
self.working = False
self.working_pixmap = QtGui.QLabel(self)
self.working_pixmap.setVisible(False)
self.working_pixmap.setPixmap(working_pixmap)
self.working_pixmap.setScaledContents(True)
wp_size = min([self.width(), self.height()]) / 5
self.working_pixmap.resize(wp_size, wp_size)
self.working_pixmap.move(self.width() - wp_size,
self.height() - wp_size)
self.buffer = None
self.buffer_width = None
self.buffer_height = None
self.blit_buffer = None
self.blit_width = None
self.blit_height = None
self.blit_top = None
self.blit_left = None
# positions to send
self.move_x = None
self.move_y = None
self.resize_width = None
self.resize_height = None
self._resize_timer = None
self.send_event = threading.Event()
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
t = threading.Thread(target = self.listen_for_remote,
name = "canvas listen",
args = ())
t.daemon = True
t.start()
t = threading.Thread(target = self.send_to_remote,
name = "canvas send",
args = ())
t.daemon = True
t.start()
dpi = self.physicalDpiX()
figure.dpi = dpi
matplotlib.rcParams['figure.dpi'] = dpi
self.child_conn.send((Msg.DPI, self.physicalDpiX()))
[docs] def listen_for_remote(self):
"""
The main method for the thread that listens for messages from
the remote canvas
"""
while self.child_conn.poll(None):
try:
(msg, payload) = self.child_conn.recv()
except EOFError:
return
logger.debug("FigureCanvasQTAggLocal.listen_for_remote :: {}".format(msg))
try:
if msg == Msg.WORKING:
self.working = payload
self.working_pixmap.setVisible(self.working)
elif msg == Msg.DRAW:
(self.buffer,
self.buffer_width,
self.buffer_height) = payload
self.update()
elif msg == Msg.BLIT:
(self.blit_buffer,
self.blit_width,
self.blit_height,
self.blit_top,
self.blit_left) = payload
self.update()
else:
raise RuntimeError("FigureCanvasQTAggLocal received bad message {}".format(msg))
except Exception:
log_exception()
[docs] def send_to_remote(self):
"""
The main method for the thread that sends messages to the remote canvas
"""
while True:
self.send_event.wait()
self.send_event.clear()
if self.move_x is not None:
msg = (Msg.MOUSE_MOVE_EVENT, (self.move_x, self.move_y))
self.child_conn.send(msg)
self.move_x = self.move_y = None
if self.resize_width is not None:
logger.debug('FigureCanvasQTAggLocal.send_to_remote: {}'
.format((Msg.RESIZE_EVENT, self.resize_width, self.resize_height)))
msg = (Msg.RESIZE_EVENT, (self.resize_width, self.resize_height))
self.child_conn.send(msg)
self.resize_width = self.resize_height = None
# for performance reasons, make sure there are no more than
# 10 updates per second
time.sleep(0.1)
[docs] def leaveEvent(self, event):
"""
Override the Qt event leaveEvent
"""
QtGui.QApplication.restoreOverrideCursor()
[docs] def mousePressEvent(self, event):
"""
Override the Qt event mousePressEvent
"""
logger.debug('FigureCanvasQTAggLocal.mousePressEvent: {}'
.format(event.button()))
x = event.pos().x()
# flip y so y=0 is bottom of canvas
y = self.height() - event.pos().y()
button = self.buttond.get(event.button())
if button is not None:
msg = (Msg.MOUSE_PRESS_EVENT, (x, y, button))
self.child_conn.send(msg)
[docs] def mouseDoubleClickEvent(self, event):
"""
Override the Qt event mouseDoubleClickEvent
"""
logger.debug('FigureCanvasQTAggLocal.mouseDoubleClickEvent: {}'
.format(event.button()))
x = event.pos().x()
# flipy so y=0 is bottom of canvas
y = self.height() - event.pos().y()
button = self.buttond.get(event.button())
if button is not None:
msg = (Msg.MOUSE_DOUBLE_CLICK_EVENT, (x, y, button))
self.child_conn.send(msg)
[docs] def mouseMoveEvent(self, event):
"""
Override the Qt event mouseMoveEvent
"""
# if DEBUG:
# print('FigureCanvasQTAggLocal.mouseMoveEvent: {}', (event.x(), event.y()))
self.move_x = event.x()
# flip y so y=0 is bottom of canvas
self.move_y = self.height() - event.y()
self.send_event.set()
[docs] def mouseReleaseEvent(self, event):
"""
Override the Qt event mouseReleaseEvent
"""
logger.debug('FigureCanvasQTAggLocal.mouseReleaseEvent: {}'
.format(event.button()))
x = event.x()
# flip y so y=0 is bottom of canvas
y = self.height() - event.y()
button = self.buttond.get(event.button())
if button is not None:
msg = (Msg.MOUSE_RELEASE_EVENT, (x, y, button))
self.child_conn.send(msg)
[docs] def resizeEvent(self, event):
"""
Override the Qt event resizeEvent
"""
w = event.size().width()
h = event.size().height()
logger.debug("FigureCanvasQTAggLocal.resizeEvent : {}"
.format((w, h)))
dpival = self.physicalDpiX()
winch = w / dpival
hinch = h / dpival
self.figure.set_size_inches(winch, hinch)
FigureCanvasAgg.resize_event(self)
QtGui.QWidget.resizeEvent(self, event)
wp_size = min([self.width(), self.height()]) / 5
self.working_pixmap.resize(wp_size, wp_size)
self.working_pixmap.move(self.width() - wp_size,
self.height() - wp_size)
# redrawing the plot window is a heavyweight operation, and we really
# don't want to do it for every resize event. so, upon a resize event,
# start a 0.2 second timer. if there's another resize event before
# it fires, cancel the timer and restart it. otherwise, after 0.2
# seconds, make the window redraw. this minimizes redrawing and
# makes the user experience much better, even though it's a stupid
# hack because i can't (easily) stop the widget from receiving
# resize events during a resize.
if self._resize_timer is not None:
self._resize_timer.cancel()
self._resize_timer = None
def fire(self, width, height):
self.resize_width = width
self.resize_height = height
self.send_event.set()
self._resize_timer = None
self._resize_timer = threading.Timer(0.2, fire, (self, winch, hinch))
self._resize_timer.start()
[docs] def paintEvent(self, e):
"""
Copy the image from the buffer to the qt.drawable.
In Qt, all drawing should be done inside of here when a widget is
shown onscreen.
"""
if self.buffer is None:
return
logger.debug('FigureCanvasQtAggLocal.paintEvent: '
.format(self, self.get_width_height()))
if self.blit_buffer is None:
# convert the Agg rendered image -> qImage
qImage = QtGui.QImage(self.buffer, self.buffer_width,
self.buffer_height,
QtGui.QImage.Format_RGBA8888)
# get the rectangle for the image
rect = qImage.rect()
p = QtGui.QPainter(self)
# reset the image area of the canvas to be the back-ground color
p.eraseRect(rect)
# draw the rendered image on to the canvas
p.drawPixmap(QtCore.QPoint(0, 0), QtGui.QPixmap.fromImage(qImage))
p.end()
else:
qImage = QtGui.QImage(self.blit_buffer,
self.blit_width,
self.blit_height,
QtGui.QImage.Format_ARGB32)
pixmap = QtGui.QPixmap.fromImage(qImage)
p = QtGui.QPainter(self)
p.drawPixmap(QtCore.QPoint(self.blit_left,
self.buffer_height - self.blit_top),
pixmap)
p.end()
self.blit_buffer = None
[docs] def print_figure(self, *args, **kwargs):
"""
Pass a "print" request to the remote canvas
(actually this is for rastering a figure and saving it to disk)
"""
self.child_conn.send((Msg.PRINT, (args, kwargs)))