"""
.. 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 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()