From 2a59d2743782829aff34229b58e0752f30fe41b9 Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Mon, 16 Jul 2018 00:31:24 +0200 Subject: [PATCH] Rudimentary support for new communication scheme in GUI --- photobooth/StateMachine.py | 44 +++---- photobooth/Worker.py | 23 +++- photobooth/camera/__init__.py | 11 +- photobooth/defaults.cfg | 2 + photobooth/gui/GuiSkeleton.py | 3 - photobooth/gui/Qt5Gui/Frames.py | 19 +-- photobooth/gui/Qt5Gui/PyQt5Gui.py | 211 ++++++++++++++---------------- photobooth/gui/Qt5Gui/Receiver.py | 18 +-- photobooth/main.py | 33 +++-- 9 files changed, 178 insertions(+), 186 deletions(-) diff --git a/photobooth/StateMachine.py b/photobooth/StateMachine.py index 63753a8..8a25023 100644 --- a/photobooth/StateMachine.py +++ b/photobooth/StateMachine.py @@ -55,6 +55,10 @@ class Context: self.state = ErrorState(event.exception, self.state) elif isinstance(event, TeardownEvent): self.state = TeardownState(event.target) + if event.target == TeardownEvent.EXIT: + return 0 + elif event.target == TeardownEvent.RESTART: + return 123 else: self.state.handleEvent(event, self) @@ -206,7 +210,7 @@ class ErrorState(State): context.state = self.old_state context.state.update() elif isinstance(event, GuiEvent) and event.name == 'abort': - context.state = TeardownState() + context.state = TeardownState(TeardownEvent.WELCOME) else: raise TypeError('Unknown Event type "{}"'.format(event)) @@ -216,11 +220,27 @@ class TeardownState(State): def __init__(self, target): super().__init__() + self._target = target def __str__(self): return 'TeardownState' + @property + def target(self): + + return self._target + + def handleEvent(self, event, context): + + if self._target == TeardownEvent.WELCOME: + if isinstance(event, GuiEvent) and event.name == 'welcome': + context.state = WelcomeState() + else: + raise ValueError('Unknown GuiEvent "{}"'.format(event.name)) + else: + raise TypeError('Unknown Event type "{}"'.format(event)) + class WelcomeState(State): @@ -237,34 +257,14 @@ class WelcomeState(State): if isinstance(event, GuiEvent): if event.name == 'start': context.state = StartupState() - elif event.name == 'settings': - context.state = SettingsState() elif event.name == 'exit': - context.state = TeardownState() + context.state = TeardownState(TeardownEvent.EXIT) else: raise ValueError('Unknown GuiEvent "{}"'.format(event.name)) else: raise TypeError('Unknown Event type "{}"'.format(event)) -class SettingsState(State): - - def __init__(self): - - super().__init__() - - def __str__(self): - - return 'SettingsState' - - def handleEvent(self, event, context): - - if isinstance(event, GuiEvent) and event.name == 'welcome': - context.state = WelcomeState() - else: - raise TypeError('Unknown Event type "{}"'.format(event)) - - class StartupState(State): def __init__(self): diff --git a/photobooth/Worker.py b/photobooth/Worker.py index 12d68eb..fbe137e 100644 --- a/photobooth/Worker.py +++ b/photobooth/Worker.py @@ -19,10 +19,13 @@ import logging import os.path +import sys from time import localtime, strftime from .PictureList import PictureList +from .StateMachine import TeardownEvent, TeardownState +from .Threading import Workers class WorkerTask: @@ -60,13 +63,23 @@ class PictureSaver(WorkerTask): class Worker: - def __init__(self, config, queue): + def __init__(self, config, comm): - self._queue = queue + self._comm = comm def run(self): - for func, args in iter(self._queue.get, 'teardown'): - func(*args) + for state in self._comm.iter(Workers.WORKER): + self.handleState(state) - return 0 + def handleState(self, state): + + if isinstance(state, TeardownState): + self.teardown(state) + + def teardown(self, state): + + if state.target == TeardownEvent.EXIT: + sys.exit(0) + elif state.target == TeardownEvent.RESTART: + sys.exit(123) diff --git a/photobooth/camera/__init__.py b/photobooth/camera/__init__.py index 27e959d..270b87c 100644 --- a/photobooth/camera/__init__.py +++ b/photobooth/camera/__init__.py @@ -18,6 +18,7 @@ # along with this program. If not, see . import logging +import sys from PIL import Image, ImageOps @@ -51,13 +52,17 @@ class Camera: self._is_keep_pictures = config.getBool('Photobooth', 'keep_pictures') logging.info('Using camera {} preview functionality'.format( - 'with' if self.is_preview else 'without')) + 'with' if self._is_preview else 'without')) self.setIdle() - def teardown(self): + def teardown(self, state): self._cap.cleanup() + if state.target == StateMachine.TeardownEvent.EXIT: + sys.exit(0) + elif state.target == StateMachine.TeardownEvent.RESTART: + sys.exit(123) def run(self): @@ -75,7 +80,7 @@ class Camera: elif isinstance(state, StateMachine.AssembleState): self.assemblePicture() elif isinstance(state, StateMachine.TeardownState): - self.teardown() + self.teardown(state) def setActive(self): diff --git a/photobooth/defaults.cfg b/photobooth/defaults.cfg index 8eccbce..db8977e 100644 --- a/photobooth/defaults.cfg +++ b/photobooth/defaults.cfg @@ -48,6 +48,8 @@ countdown_time = 8 display_time = 5 # Timeout for postprocessing (shown after review) postprocess_time = 60 +# Keep single pictures (True/False) +keep_pictures = False [Picture] # Basedir of output pictures diff --git a/photobooth/gui/GuiSkeleton.py b/photobooth/gui/GuiSkeleton.py index c943a41..ca43814 100644 --- a/photobooth/gui/GuiSkeleton.py +++ b/photobooth/gui/GuiSkeleton.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -# from . import GuiState from .. import StateMachine @@ -86,8 +85,6 @@ class GuiSkeleton: self.showWelcome(state) elif isinstance(state, StateMachine.StartupState): self.showStartup(state) - elif isinstance(state, StateMachine.SettingsState): - self.showSettings(state) elif isinstance(state, StateMachine.IdleState): self.showIdle(state) elif isinstance(state, StateMachine.GreeterState): diff --git a/photobooth/gui/Qt5Gui/Frames.py b/photobooth/gui/Qt5Gui/Frames.py index 2f2b48a..ac74482 100644 --- a/photobooth/gui/Qt5Gui/Frames.py +++ b/photobooth/gui/Qt5Gui/Frames.py @@ -34,7 +34,7 @@ from . import Widgets from . import styles -class Start(QtWidgets.QFrame): +class Welcome(QtWidgets.QFrame): def __init__(self, start_action, set_date_action, settings_action, exit_action): @@ -79,7 +79,7 @@ class Start(QtWidgets.QFrame): class IdleMessage(QtWidgets.QFrame): - def __init__(self): + def __init__(self, trigger_action): super().__init__() self.setObjectName('IdleMessage') @@ -87,12 +87,13 @@ class IdleMessage(QtWidgets.QFrame): self._message_label = 'Hit the' self._message_button = 'Button!' - self.initFrame() + self.initFrame(trigger_action) - def initFrame(self): + def initFrame(self, trigger_action): lbl = QtWidgets.QLabel(self._message_label) btn = QtWidgets.QPushButton(self._message_button) + btn.clicked.connect(trigger_action) lay = QtWidgets.QVBoxLayout() lay.addWidget(lbl) @@ -102,7 +103,7 @@ class IdleMessage(QtWidgets.QFrame): class GreeterMessage(QtWidgets.QFrame): - def __init__(self, num_x, num_y): + def __init__(self, num_x, num_y, countdown_action): super().__init__() self.setObjectName('GreeterMessage') @@ -114,14 +115,15 @@ class GreeterMessage(QtWidgets.QFrame): else: self._text_label = '' - self.initFrame() + self.initFrame(countdown_action) - def initFrame(self): + def initFrame(self, countdown_action): ttl = QtWidgets.QLabel(self._text_title) ttl.setObjectName('title') btn = QtWidgets.QPushButton(self._text_button) btn.setObjectName('button') + btn.clicked.connect(countdown_action) lbl = QtWidgets.QLabel(self._text_label) lbl.setObjectName('message') @@ -132,7 +134,7 @@ class GreeterMessage(QtWidgets.QFrame): self.setLayout(lay) -class PoseMessage(QtWidgets.QFrame): +class CaptureMessage(QtWidgets.QFrame): def __init__(self, num_picture, num_x, num_y): @@ -325,6 +327,7 @@ class PostprocessMessage(Widgets.TransparentOverlay): def disableAndCall(button, handle): button.setEnabled(False) + button.update() handle() def createButton(task): diff --git a/photobooth/gui/Qt5Gui/PyQt5Gui.py b/photobooth/gui/Qt5Gui/PyQt5Gui.py index 7ceb3c6..d6a6196 100644 --- a/photobooth/gui/Qt5Gui/PyQt5Gui.py +++ b/photobooth/gui/Qt5Gui/PyQt5Gui.py @@ -27,10 +27,9 @@ from PyQt5 import QtWidgets from PIL import ImageQt -# from ... import StateMachine -# from ...Threading import Workers +from ...StateMachine import GuiEvent, TeardownEvent +from ...Threading import Workers -from .. import GuiState from ..GuiSkeleton import GuiSkeleton from ..GuiPostprocessor import GuiPostprocessor @@ -41,70 +40,54 @@ from . import Receiver class PyQt5Gui(GuiSkeleton): - def __init__(self, argv, config, camera_conn, worker_queue, communicator): + def __init__(self, argv, config, communicator): super().__init__(communicator) self._cfg = config - self._conn = camera_conn - parser = argparse.ArgumentParser() - parser.add_argument('--run', action='store_true', - help='omit welcome screen and run photobooth') - parsed_args, unparsed_args = parser.parse_known_args() - self._omit_welcome = parsed_args.run - - self._registerCallbacks() + is_start, unparsed_args = self._parseArgs() self._initUI(argv[:1] + unparsed_args) self._initReceiver() + self._picture = None self._postprocess = GuiPostprocessor(self._cfg) + if is_start: + self._comm.send(Workers.MASTER, GuiEvent('start')) + def run(self): - if self._omit_welcome: - self._showStart(None) - else: - self._showWelcomeScreen() exit_code = self._app.exec_() self._gui = None return exit_code - def close(self): + def _parseArgs(self): - self._gui.close() + # Add parameter for direct startup + parser = argparse.ArgumentParser() + parser.add_argument('--run', action='store_true', + help='omit welcome screen and run photobooth') + parsed_args, unparsed_args = parser.parse_known_args() - def restart(self): - - self._app.exit(123) - - def _registerCallbacks(self): - - self.idle = self._showIdle - self.trigger = self._sendTrigger - self.greeter = self._showGreeter - self.countdown = self._showCountdown - self.preview = self._showPreview - self.pose = self._showPose - self.assemble = self._showAssemble - self.review = self._showReview - self.teardown = self._sendTeardown - self.error = self._showError + return (parsed_args.run, unparsed_args) def _initUI(self, argv): self._disableTrigger() + # Load stylesheet style = self._cfg.get('Gui', 'style') filename = next((file for name, file in styles if name == style)) - with open(os.path.join(os.path.dirname(__file__), filename), 'r') as f: stylesheet = f.read() + # Create application and main window self._app = QtWidgets.QApplication(argv) self._app.setStyleSheet(stylesheet) self._gui = PyQt5MainWindow(self._cfg, self._handleKeypressEvent) + # Load additional fonts fonts = ['photobooth/gui/Qt5Gui/fonts/AmaticSC-Regular.ttf', 'photobooth/gui/Qt5Gui/fonts/AmaticSC-Bold.ttf'] self._fonts = QtGui.QFontDatabase() @@ -113,14 +96,11 @@ class PyQt5Gui(GuiSkeleton): def _initReceiver(self): - self._receiver = Receiver.Receiver([self._conn]) + # Create receiver thread + self._receiver = Receiver.Receiver(self._comm) self._receiver.notify.connect(self.handleState) self._receiver.start() - def _setWidget(self, widget): - - self._gui.setCentralWidget(widget) - def _enableEscape(self): self._is_escape = True @@ -137,82 +117,58 @@ class PyQt5Gui(GuiSkeleton): self._is_trigger = False - def _sendStart(self): + def _setWidget(self, widget): - self._conn.send('start') + self._gui.setCentralWidget(widget) - def _sendTrigger(self, state): + def close(self): - self._conn.send('triggered') + if self._gui.close(): + self._comm.send(Workers.MASTER, TeardownEvent(TeardownEvent.EXIT)) - def _sendAck(self): + def teardown(self, state): - self._conn.send('ack') + if state.target == TeardownEvent.EXIT: + self._app.exit(0) + elif state.target == TeardownEvent.RESTART: + self._app.exit(123) + elif state.target == TeardownEvent.WELCOME: + self._comm.send(Workers.MASTER, GuiEvent('welcome')) - def _sendCancel(self): + def showError(self, state): - self._conn.send('cancel') + logging.error('%s: %s', state.title, state.message) - def _sendTeardown(self, state): + MessageBox(self, MessageBox.RETRY, state.title, state.message, + lambda: self._comm.send(Workers.MASTER, GuiEvent('retry')), + lambda: self._comm.send(Workers.MASTER, GuiEvent('abort'))) - self._conn.send('teardown') - self._showWelcomeScreen() - - def _handleKeypressEvent(self, event): - - if self._is_escape and event.key() == QtCore.Qt.Key_Escape: - self.handleState(GuiState.TeardownState()) - elif self._is_trigger and event.key() == QtCore.Qt.Key_Space: - self.handleState(GuiState.TriggerState()) - - def _showWelcomeScreen(self): + def showWelcome(self, state): self._disableTrigger() self._disableEscape() - self._lastHandle = self._showWelcomeScreen - self._setWidget(Frames.Start(self._showStart, self._showSetDateTime, - self._showSettings, self.close)) + self._setWidget(Frames.Welcome( + lambda: self._comm.send(Workers.MASTER, GuiEvent('start')), + self._showSetDateTime, self._showSettings, self.close)) if QtWidgets.QApplication.overrideCursor() != 0: QtWidgets.QApplication.restoreOverrideCursor() - def _showSetDateTime(self): - - self._disableTrigger() - self._disableEscape() - self._lastHandle = self._showSetDateTime - self._setWidget(Frames.SetDateTime(self._showWelcomeScreen, - self.restart)) - - def _showSettings(self): - - # self._comm.send(Workers.MASTER, StateMachine.GuiEvent('settings')) - - self._disableTrigger() - self._disableEscape() - self._lastHandle = self._showSettings - self._setWidget(Frames.Settings(self._cfg, self._showSettings, - self._showWelcomeScreen, self.restart)) - - def _showStart(self, state): - - # self._comm.send(Workers.MASTER, StateMachine.GuiEvent('start')) + def showStartup(self, state): self._disableTrigger() self._enableEscape() - self._lastHandle = self._showWelcomeScreen - self._sendStart() self._setWidget(Frames.WaitMessage('Starting the photobooth...')) if self._cfg.getBool('Gui', 'hide_cursor'): QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.BlankCursor) - def _showIdle(self, state): + def showIdle(self, state): self._enableEscape() self._enableTrigger() - self._lastHandle = self._showIdle - self._setWidget(Frames.IdleMessage()) + self._setWidget(Frames.IdleMessage( + lambda: self._comm.send(Workers.MASTER, GuiEvent('trigger')))) - def _showGreeter(self, state): + def showGreeter(self, state): self._enableEscape() self._disableTrigger() @@ -221,56 +177,79 @@ class PyQt5Gui(GuiSkeleton): self._cfg.getInt('Picture', 'num_y')) greeter_time = self._cfg.getInt('Photobooth', 'greeter_time') * 1000 - self._setWidget(Frames.GreeterMessage(*num_pic)) - QtCore.QTimer.singleShot(greeter_time, self._sendAck) + self._setWidget(Frames.GreeterMessage( + *num_pic, + lambda: self._comm.send(Workers.MASTER, GuiEvent('countdown')))) + QtCore.QTimer.singleShot( + greeter_time, + lambda: self._comm.send(Workers.MASTER, GuiEvent('countdown'))) - def _showCountdown(self, state): + def showCountdown(self, state): countdown_time = self._cfg.getInt('Photobooth', 'countdown_time') - self._setWidget(Frames.CountdownMessage(countdown_time, self._sendAck)) + self._setWidget(Frames.CountdownMessage( + countdown_time, + lambda: self._comm.send(Workers.MASTER, GuiEvent('capture')))) - def _showPreview(self, state): + def updateCountdown(self, event): - self._gui.centralWidget().picture = ImageQt.ImageQt(state.picture) + self._gui.centralWidget().picture = ImageQt.ImageQt(event.picture) self._gui.centralWidget().update() - def _showPose(self, state): + def showCapture(self, state): num_pic = (self._cfg.getInt('Picture', 'num_x'), self._cfg.getInt('Picture', 'num_y')) - self._setWidget(Frames.PoseMessage(state.num_picture, *num_pic)) + self._setWidget(Frames.CaptureMessage(state.num_picture, *num_pic)) - def _showAssemble(self, state): + def showAssemble(self, state): self._setWidget(Frames.WaitMessage('Processing picture...')) - def _showReview(self, state): + def showReview(self, state): - img = ImageQt.ImageQt(state.picture) + self._picture = ImageQt.ImageQt(state.picture) review_time = self._cfg.getInt('Photobooth', 'display_time') * 1000 - self._setWidget(Frames.PictureMessage(img)) - QtCore.QTimer.singleShot(review_time, lambda: - self._showPostprocess(state.picture)) + self._setWidget(Frames.PictureMessage(self._picture)) + QtCore.QTimer.singleShot( + review_time, + lambda: self._comm.send(Workers.MASTER, GuiEvent('postprocess'))) - def _showPostprocess(self, picture): + def showPostprocess(self, state): - tasks = self._postprocess.get(picture) + tasks = self._postprocess.get(self._picture) postproc_t = self._cfg.getInt('Photobooth', 'postprocess_time') - Frames.PostprocessMessage(self._gui.centralWidget(), tasks, - self._sendAck, postproc_t * 1000) + Frames.PostprocessMessage( + self._gui.centralWidget(), tasks, + lambda: self._comm.send(Workers.MASTER, GuiEvent('idle')), + postproc_t * 1000) - def _showError(self, state): + def _handleKeypressEvent(self, event): - logging.error('%s: %s', state.title, state.message) + if self._is_escape and event.key() == QtCore.Qt.Key_Escape: + self._comm.send(Workers.MASTER, + TeardownEvent(TeardownEvent.WELCOME)) + elif self._is_trigger and event.key() == QtCore.Qt.Key_Space: + self._comm.send(Workers.MASTER, GuiEvent('trigger')) - def exec(*handles): - for handle in handles: - handle() + def _showSetDateTime(self): - MessageBox(self, MessageBox.RETRY, state.title, state.message, - exec(self._sendAck, self._lastState), - exec(self._sendCancel, self._showWelcomeScreen)) + self._disableTrigger() + self._disableEscape() + self._setWidget(Frames.SetDateTime( + self.showWelcome, + lambda: self._comm.send(Workers.MASTER, + TeardownEvent(TeardownEvent.RESTART)))) + + def _showSettings(self): + + self._disableTrigger() + self._disableEscape() + self._setWidget(Frames.Settings( + self._cfg, self._showSettings, self.showWelcome, + lambda: self._comm.send(Workers.MASTER, + TeardownEvent(TeardownEvent.RESTART)))) class PyQt5MainWindow(QtWidgets.QMainWindow): diff --git a/photobooth/gui/Qt5Gui/Receiver.py b/photobooth/gui/Qt5Gui/Receiver.py index 4db9df4..53ede56 100644 --- a/photobooth/gui/Qt5Gui/Receiver.py +++ b/photobooth/gui/Qt5Gui/Receiver.py @@ -17,19 +17,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import multiprocessing as mp - from PyQt5 import QtCore +from ...Threading import Workers + class Receiver(QtCore.QThread): notify = QtCore.pyqtSignal(object) - def __init__(self, conn): + def __init__(self, comm): super().__init__() - self._conn = conn + self._comm = comm def handle(self, state): @@ -37,11 +37,5 @@ class Receiver(QtCore.QThread): def run(self): - while self._conn: - for c in mp.connection.wait(self._conn): - try: - state = c.recv() - except EOFError: - break - else: - self.handle(state) + for state in self._comm.iter(Workers.GUI): + self.handle(state) diff --git a/photobooth/main.py b/photobooth/main.py index 8b75040..101fc85 100644 --- a/photobooth/main.py +++ b/photobooth/main.py @@ -98,37 +98,34 @@ class CameraProcess(mp.Process): class WorkerProcess(mp.Process): - def __init__(self, config, queue): + def __init__(self, config, comm): super().__init__() self.daemon = True self.cfg = config - self.queue = queue + self.comm = comm def run(self): - sys.exit(Worker(self.cfg, self.queue).run()) + sys.exit(Worker(self.cfg, self.comm).run()) class GuiProcess(mp.Process): - def __init__(self, argv, config, conn, queue, communicator): + def __init__(self, argv, config, communicator): super().__init__() self.argv = argv self.cfg = config - self.conn = conn - self.queue = queue self.comm = communicator def run(self): Gui = lookup_and_import(gui.modules, self.cfg.get('Gui', 'module'), 'gui') - sys.exit(Gui(self.argv, self.cfg, self.conn, self.queue, - self.comm).run()) + sys.exit(Gui(self.argv, self.cfg, self.comm).run()) def run(argv): @@ -144,8 +141,8 @@ def run(argv): # Create communication objects: # 1. We use a pipe to connect GUI and camera process # 2. We use a queue to feed tasks to the postprocessing process - gui_conn, camera_conn = mp.Pipe() - worker_queue = mp.SimpleQueue() + # gui_conn, camera_conn = mp.Pipe() + # worker_queue = mp.SimpleQueue() # Initialize processes: We use three processes here: # 1. Camera processing @@ -154,25 +151,27 @@ def run(argv): camera_proc = CameraProcess(config, comm) # camera_conn, worker_queue) camera_proc.start() - worker_proc = WorkerProcess(config, worker_queue) + worker_proc = WorkerProcess(config, comm) worker_proc.start() - gui_proc = GuiProcess(argv, config, gui_conn, worker_queue, comm) + gui_proc = GuiProcess(argv, config, comm) gui_proc.start() for event in comm.iter(Workers.MASTER): - context.handleEvent(event) + exit_code = context.handleEvent(event) + if exit_code in (0, 123): + break # Close endpoints - gui_conn.close() - camera_conn.close() + # gui_conn.close() + # camera_conn.close() # Wait for processes to finish gui_proc.join() - worker_queue.put('teardown') + # worker_queue.put('teardown') worker_proc.join() camera_proc.join(1) - return gui_proc.exitcode + return exit_code def main(argv):