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):