Source code for pyopus.plotter.manager

"""
.. inheritance-diagram:: pyopus.plotter.manager
    :parts: 1
	
**Manager for Matplotlib plot windows**

The graphical part (PyQt + MatPlotLib) is running in a thread and the 
part that issues the plotting commands (defined in the 
:mod:`~pyopus.plotter.interface` module) runs in the main thread (or process). 

The main thread uses a :class:`QPController` object for sending and receiving 
messages from the graphical thread. The messages are sent to the GUI by 
calling the ``processMessage()`` method of a :class:`QPController` object 
via a meta object. A :class:`queue.Queue` object is used for sending the 
response back to the main thread. On the graphical thread's side a 
:class:`QPControlWindow` widget is handling the received commands. 
"""

import threading
import queue
import pickle
import traceback
import time
import os
import sys

from .. import PyOpusError


__all__ = [ 'QPController' ]


#
# Entry point of the GUI thread. Creates QApplication and QPControlWindow. 
#

def GUIentry(args, queue, lock):
	"""
	Entry point of the GUI thread. 
	
	This function creates a :class:`QPControlWindow` object and starts the 
	GUI application's main loop. *queue* is the queue that is used for 
	returning values from the GUI. Commands are sent to the GUI by emitting
	signals from a :class:`QPController` object whose ``messagePoster`` 
	signal is connected to the ``processMessage`` slot of the 
	:class:`QPControlWindow` object. 
	
	*lock* is a :class:`threading.Lock` object for preventing the main thread 
	from messing with gui data while gui events are handled. 
	"""
	
	# Importing takes place in GUI thread
	# messagePoster signal will be created in the GUI thread
	# app.exec_ must be called in the thread where app was created. 
	from PyQt5.QtWidgets import QApplication
	from .controlw import QPControlWindow
	
	app=QApplication(args)
	
	running=True
	
	w=QPControlWindow(None, queue=queue, lock=lock)
	
	w.show()
	w.raise_()
	
	# Send the QPControlWindow object to the main thread
	# so that we can connect to its slots
	queue.put(w, True)
	queue.put(app, True)
	
	# Enter GUI main loop. 
	app.exec_()
	
	# At the end of tghe GUi main loop send True via response queue. 
	# The join() method blocks until it receives this value. 
	queue.put(True, True)
	
#
# QPController running in main thread. Starts/stops gui thread. 
#

[docs]class QPController(object): """ This is the controller responsible for sending commands to the GUI and collection responses. *args* are passed to the :func:`GUIentry` function which forwards them as command line arguments to the :class:`QApplication` object. """ def __init__(self, args=[]): # QObject.__init__(self) self.controlWindow=None self.responseQueue=queue.Queue(-1) self.lock=threading.Lock() self.locked=False self.guiThread=None # Qt stuff will be imported after the GUI thread starts. QtCore=None
[docs] def startGUI(self): """ Starts the GUI thread. """ _, fn = os.path.split(__file__) if not self.checkIfAlive(): self.guiThread=threading.Thread( target=GUIentry, args=[[fn], self.responseQueue, self.lock] ) self.guiThread.setDaemon(True) self.guiThread.start() # Get the control app from the GUI (wait for thread to start) self.controlWindow=self.responseQueue.get(True) self.app=self.responseQueue.get(True) # Get meta object for the control app self.cwMetaObj=self.controlWindow.metaObject() # Get meta methods self.cwMetaProcessMessage=self.cwMetaObj.method( self.cwMetaObj.indexOfMethod( self.cwMetaObj.normalizedSignature("processMessage(PyQt_PyObject)") ) ) #print(self.cwMetaProcessMessage) #print("gaga", self.cwMetaFigureDraw.typeName()) # Import Qt stuff locally after the GUI is running and Qt was imported in the GUI thread. # This way the GUI thread will be the Qt main thread. from PyQt5 import QtCore #, PyQt_PyObject self.QtCore=QtCore
# self.PyQt_PyObject=PyQt_PyObject
[docs] def checkIfAlive(self): """ Returns ``True`` if the GUI thread is running. """ if self.guiThread is not None: if self.guiThread.is_alive(): return True else: self.guiThread=None return False else: return False
[docs] def stopGUI(self): """ Stops the GUI thread by sending it the ``exit`` command. """ resp=self.postMessage({ 'cmd':['plot', 'exit'], 'args': [], 'kwargs': {} } )
[docs] def join(self): """ Waits for the GUI thread to finish. :obj:`KeyboardInterrupt` and :obj:`SystemExit` are caught and the GUI is stopped upon which the exception is re-raised. """ # Wait for and object to be received via response queue indicating that the control window was closed self.responseQueue.get(True) # Register an ugly exit function because a freeze happens at c++ exit cleanup # Somebody screwed up something in Qt - previously GUI in a non-main program thread worked fine. def uglyExit(): import os os._exit(0) import atexit atexit.register(uglyExit)
[docs] def postMessage(self, message): """ This is the function that is invoked for every command that is sent to the GUI. It invokes the processMessage() method of the control window via a meta object and picks up the return value from the response queue. """ if self.checkIfAlive(): self.cwMetaProcessMessage.invoke( self.controlWindow, self.QtCore.Qt.BlockingQueuedConnection, self.QtCore.Q_ARG(object, message) ) response=self.responseQueue.get() return response else: return None
[docs] def figureAlive(self, tag): """ Checks if the window of the given :class:`Figure` is still open. """ try: retval=self.controlWindow.figureAlive(tag) except (KeyboardInterrupt, SystemExit): # Re-reaise these two exceptions raise except: # Everything else is an error raise PyOpusError("Matplotlib GUI thread is not running.") return retval
[docs] def figureDraw(self, tag): """ Forces redrawing of the given :class:`Figure`. """ try: self.controlWindow.figureDraw(tag) except (KeyboardInterrupt, SystemExit): # Re-reaise these two exceptions raise except: # Everything else is an error raise PyOpusError("Matplotlib GUI thread is not running.")
[docs] def lockGUI(self): """ Marks the beginning of a section of code where Matplotlib API calls are made. Locking prevents these calls from interfering with the PyQt event loop and crashing the application. """ # Lock if not already locked if not self.locked: # print("Locking GUI") self.locked=True self.lock.acquire(True)
[docs] def unlockGUI(self): """ Marks the end of a section of code where Matplotlib API calls are made. It reenables the PyQt event loop. """ if self.locked: # print("Unlocking GUI") self.lock.release() self.locked=False
if __name__=='__main__': import sip import sys sip.setdestroyonexit(False) c=QPController() c.startGUI() ret=c.postMessage({ 'cmd':['plot', 'new'], 'args': [], 'kwargs': {} }) print(ret) c.join()