diff --git a/photobooth/Photobooth.py b/photobooth/Photobooth.py index edcb96d..f3ef1e0 100644 --- a/photobooth/Photobooth.py +++ b/photobooth/Photobooth.py @@ -7,7 +7,7 @@ from PIL import Image, ImageOps from .PictureDimensions import PictureDimensions -from . import gui +from .gui import GuiState from .Worker import PictureSaver @@ -113,7 +113,7 @@ class Photobooth: def initRun(self): self.setCameraIdle() - self._conn.send(gui.IdleState()) + self._conn.send(GuiState.IdleState()) self.triggerOn() def run(self): @@ -131,7 +131,7 @@ class Photobooth: self.trigger() except RuntimeError as e: logging.error('Camera error: %s', str(e)) - self._conn.send(gui.ErrorState('Camera error', str(e))) + self._conn.send(GuiState.ErrorState('Cam error', str(e))) self.recvAck() except TeardownException: @@ -149,22 +149,22 @@ class Photobooth: def showCountdownPreview(self): - self._conn.send(gui.CountdownState()) + self._conn.send(GuiState.CountdownState()) while not self._conn.poll(): picture = ImageOps.mirror(self._cap.getPreview()) - self._conn.send(gui.PreviewState(picture=picture)) + self._conn.send(GuiState.PreviewState(picture=picture)) self.recvAck() def showCountdownNoPreview(self): - self._conn.send(gui.CountdownState()) + self._conn.send(GuiState.CountdownState()) self.recvAck() def showPose(self, num_picture): - self._conn.send(gui.PoseState(num_picture)) + self._conn.send(GuiState.PoseState(num_picture)) def captureSinglePicture(self, num_picture): @@ -199,24 +199,24 @@ class Photobooth: logging.info('Photobooth triggered') - self._conn.send(gui.GreeterState()) + self._conn.send(GuiState.GreeterState()) self.triggerOff() self.setCameraActive() self.recvAck() pics = self.capturePictures() - self._conn.send(gui.AssembleState()) + self._conn.send(GuiState.AssembleState()) img = self.assemblePictures(pics) - self._conn.send(gui.PictureState(img)) + self._conn.send(GuiState.ReviewState(picture=img)) self.enqueueWorkerTasks(img) self.setCameraIdle() self.recvAck() - self._conn.send(gui.IdleState()) + self._conn.send(GuiState.IdleState()) self.triggerOn() def gpioTrigger(self): @@ -225,7 +225,7 @@ class Photobooth: def gpioExit(self): - self._conn.send(gui.TeardownState()) + self._conn.send(GuiState.TeardownState()) def triggerOff(self): @@ -235,4 +235,4 @@ class Photobooth: def triggerOn(self): self._lampOn() - self._gpioTrigger = lambda: self._conn.send(gui.TriggerState()) + self._gpioTrigger = lambda: self._conn.send(GuiState.TriggerState()) diff --git a/photobooth/gui/GuiPostprocess.py b/photobooth/gui/GuiPostprocess.py index c966223..9320cff 100644 --- a/photobooth/gui/GuiPostprocess.py +++ b/photobooth/gui/GuiPostprocess.py @@ -30,7 +30,7 @@ class PrintPostprocess(GuiPostprocess): super().__init__(**kwargs) Printer = lookup_and_import(printer.modules, printer_module, 'printer') - self._printer = Printer(page_size) + self._printer = Printer(page_size, True) def get(self, picture): diff --git a/photobooth/gui/GuiSkeleton.py b/photobooth/gui/GuiSkeleton.py new file mode 100644 index 0000000..db1c90b --- /dev/null +++ b/photobooth/gui/GuiSkeleton.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from . import GuiState + + +class GuiSkeleton: + + def __init__(self): + + super().__init__() + + @property + def idle(self): + + return self._idle + + @idle.setter + def idle(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "idle" must be callable') + + self._idle = handle + + @property + def trigger(self): + + return self._trigger + + @trigger.setter + def trigger(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "trigger" must be callable') + + self._trigger = handle + + @property + def greeter(self): + + return self._greeter + + @greeter.setter + def greeter(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "greeter" must be callable') + + self._greeter = handle + + @property + def countdown(self): + + return self._countdown + + @countdown.setter + def countdown(self, handle): + + if not callable(handle): + raise ValueError(('Function handle for "countdown" must be ' + 'callable')) + + self._countdown = handle + + @property + def preview(self): + + return self._preview + + @preview.setter + def preview(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "preview" must be callable') + + self._preview = handle + + @property + def pose(self): + + return self._pose + + @pose.setter + def pose(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "pose" must be callable') + + self._pose = handle + + @property + def assemble(self): + + return self._assemble + + @assemble.setter + def assemble(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "assemble" must be callable') + + self._assemble = handle + + @property + def review(self): + + return self._review + + @review.setter + def review(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "review" must be callable') + + self._review = handle + + @property + def teardown(self): + + return self._teardown + + @teardown.setter + def teardown(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "teardown" must be callable') + + self._teardown = handle + + @property + def error(self): + + return self._error + + @error.setter + def error(self, handle): + + if not callable(handle): + raise ValueError('Function handle for "error" must be callable') + + self._error = handle + + def handleState(self, state): + + if not isinstance(state, GuiState.GuiState): + raise ValueError('Not a GuiState object received') + + if isinstance(state, GuiState.IdleState): + self.idle(state) + elif isinstance(state, GuiState.TriggerState): + self.trigger(state) + elif isinstance(state, GuiState.GreeterState): + self.greeter(state) + elif isinstance(state, GuiState.CountdownState): + self.countdown(state) + elif isinstance(state, GuiState.PreviewState): + self.preview(state) + elif isinstance(state, GuiState.PoseState): + self.pose(state) + elif isinstance(state, GuiState.AssembleState): + self.assemble(state) + elif isinstance(state, GuiState.ReviewState): + self.review(state) + elif isinstance(state, GuiState.TeardownState): + self.teardown(state) + elif isinstance(state, GuiState.ErrorState): + self.error(state) + else: + raise ValueError('Unknown state received') diff --git a/photobooth/gui/GuiState.py b/photobooth/gui/GuiState.py index bf50fa7..cf4d401 100644 --- a/photobooth/gui/GuiState.py +++ b/photobooth/gui/GuiState.py @@ -144,6 +144,13 @@ class PreviewState(PictureState): super().__init__(**kwargs) +class ReviewState(PictureState): + + def __init__(self, **kwargs): + + super().__init__(**kwargs) + + class TeardownState(GuiState): def __init__(self, **kwargs): diff --git a/photobooth/gui/PyQt5Gui.py b/photobooth/gui/PyQt5Gui.py deleted file mode 100644 index 65edd01..0000000 --- a/photobooth/gui/PyQt5Gui.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import multiprocessing as mp -import queue -import logging - -from PIL import ImageQt - -from PyQt5 import QtGui, QtCore, QtWidgets - -import math - -from .Qt5Gui import Frames - -from .PyQt5GuiHelpers import QRoundProgressBar - -from . import * - - -class PyQt5Gui(Gui): - - def __init__(self, argv, config): - - super().__init__() - - global cfg - cfg = config - - self._app = QtWidgets.QApplication(argv) - self._p = PyQt5MainWindow() - self._lastState = self.showStart - - self._postprocessList = [] - self._postprocessQueue = queue.Queue() - - if cfg.getBool('Printer', 'enable'): - self._postprocessList.append( PrintPostprocess( cfg.get('Printer', 'module'), - (cfg.getInt('Printer', 'width'), cfg.getInt('Printer', 'height')) ) ) - - - def run(self, camera_conn, worker_queue): - - receiver = PyQt5Receiver([camera_conn]) - receiver.notify.connect(self.handleState) - receiver.start() - - self._conn = camera_conn - self._queue = worker_queue - - self.showStart() - - exit_code = self._app.exec_() - self._p = None - - return exit_code - - - def close(self): - - self._p.close() - - - def restart(self): - - self._app.exit(123) - - - def sendAck(self): - - self._conn.send('ack') - - - def sendCancel(self): - - self._conn.send('cancel') - - - def sendTrigger(self): - - self._conn.send('triggered') - - - def sendTeardown(self): - - self._conn.send('teardown') - - - def handleKeypressEvent(self, event): - - if event.key() == QtCore.Qt.Key_Escape: - self.handleState(TeardownState()) - elif event.key() == QtCore.Qt.Key_Space: - self.handleState(TriggerState()) - - - def handleKeypressEventNoTrigger(self, event): - - if event.key() == QtCore.Qt.Key_Escape: - self.handleState(TeardownState()) - - - def handleState(self, state): - - if not isinstance(state, GuiState): - raise ValueError('Invalid data received') - - if isinstance(state, IdleState): - self.showIdle() - - elif isinstance(state, TriggerState): - self.sendTrigger() - - elif isinstance(state, GreeterState): - global cfg - self._p.handleKeypressEvent = self.handleKeypressEventNoTrigger - # self._p.setCentralWidget( PyQt5GreeterMessage( - self._p.setCentralWidget( Frames.GreeterMessage( - cfg.getInt('Picture', 'num_x'), cfg.getInt('Picture', 'num_y') ) ) - QtCore.QTimer.singleShot(cfg.getInt('Photobooth', 'greeter_time') * 1000, self.sendAck) - - elif isinstance(state, CountdownState): - # self._p.setCentralWidget(PyQt5CountdownMessage(cfg.getInt('Photobooth', 'countdown_time'), self.sendAck)) - countdown_time = cfg.getInt('Photobooth', 'countdown_time') - self._p.setCentralWidget(Frames.CountdownMessage(countdown_time, - self.sendAck)) - - elif isinstance(state, PreviewState): - self._p.centralWidget().picture = ImageQt.ImageQt(state.picture) - self._p.centralWidget().update() - - elif isinstance(state, PoseState): - # self._p.setCentralWidget(PyQt5PoseMessage()) - self._p.setCentralWidget(Frames.PoseMessage(state.num_picture, - cfg.getInt('Picture', 'num_x'), cfg.getInt('Picture', 'num_y'))) - - elif isinstance(state, AssembleState): - self._p.setCentralWidget(Frames.WaitMessage('Processing picture...')) - # self._p.setCentralWidget(PyQt5WaitMessage('Processing picture...')) - - elif isinstance(state, PictureState): - img = ImageQt.ImageQt(state.picture) - # self._p.setCentralWidget(PyQt5PictureMessage(img)) - self._p.setCentralWidget(Frames.PictureMessage(img)) - QtCore.QTimer.singleShot(cfg.getInt('Photobooth', 'display_time') * 1000, - lambda : self.postprocessPicture(state.picture)) - - elif isinstance(state, TeardownState): - self._conn.send('teardown') - self.showStart() - - elif isinstance(state, ErrorState): - self.showError(state.title, state.message) - - else: - raise ValueError('Unknown state') - - - def postprocessPicture(self, picture): - - for task in self._postprocessList: - self._postprocessQueue.put(task.get(picture)) - - self.handleQueue() - - - def handleQueue(self): - - while True: - try: - task = self._postprocessQueue.get(block = False) - except queue.Empty: - self.sendAck() - break - else: - if isinstance(task, PrintState): - reply = QtWidgets.QMessageBox.question(self._p, 'Print picture?', - 'Do you want to print the picture?', - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) - if reply == QtWidgets.QMessageBox.Yes: - task.handler() - QtWidgets.QMessageBox.information(self._p, 'Printing', - 'Picture sent to printer.', QtWidgets.QMessageBox.Ok) - else: - raise ValueError('Unknown task') - - - def showStart(self): - - self._p.handleKeypressEvent = lambda event : None - self._lastState = self.showStart - self._p.setCentralWidget(Frames.Start(self.showStartPhotobooth, self.showSettings, self.close)) - if QtWidgets.QApplication.overrideCursor() != 0: - QtWidgets.QApplication.restoreOverrideCursor() - - - def showSettings(self): - - global cfg - self._p.handleKeypressEvent = lambda event : None - self._lastState = self.showSettings - self._p.setCentralWidget(Frames.Settings(cfg, self.showSettings, self.showStart, self.restart)) - - - def showStartPhotobooth(self): - - self._lastState = self.showStartPhotobooth - self._conn.send('start') - # self._p.setCentralWidget(PyQt5WaitMessage('Starting the photobooth...')) - self._p.setCentralWidget(Frames.WaitMessage('Starting the photobooth...')) - if cfg.getBool('Gui', 'hide_cursor'): - QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.BlankCursor) - - - def showIdle(self): - - self._p.handleKeypressEvent = self.handleKeypressEvent - self._lastState = self.showIdle - self._p.setCentralWidget(Frames.IdleMessage()) - # self._p.setCentralWidget(PyQt5IdleMessage()) - - - def showError(self, title, message): - - logging.error('%s: %s', title, message) - reply = QtWidgets.QMessageBox.warning(self._p, title, message, - QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Retry, QtWidgets.QMessageBox.Retry) - if reply == QtWidgets.QMessageBox.Retry: - self.sendAck() - self._lastState() - else: - self.sendCancel() - self.showStart() - - -class PyQt5Receiver(QtCore.QThread): - - notify = QtCore.pyqtSignal(object) - - def __init__(self, conn): - - super().__init__() - - self._conn = conn - - - def handle(self, state): - - self.notify.emit(state) - - - 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) - - - -class PyQt5MainWindow(QtWidgets.QMainWindow): - - def __init__(self): - - super().__init__() - - self.handleKeypressEvent = lambda event : None - - self.initUI() - - - @property - def handleKeypressEvent(self): - - return self._handle_key - - - @handleKeypressEvent.setter - def handleKeypressEvent(self, func): - - if not callable(func): - raise ValueError('Keypress event handler must be callable') - - self._handle_key = func - - - def initUI(self): - - global cfg - - self.setWindowTitle('Photobooth') - - if cfg.getBool('Gui', 'fullscreen'): - self.showFullScreen() - else: - self.resize(cfg.getInt('Gui', 'width'), - cfg.getInt('Gui', 'height')) - self.show() - - - def closeEvent(self, e): - - reply = QtWidgets.QMessageBox.question(self, 'Confirmation', "Quit Photobooth?", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) - - if reply == QtWidgets.QMessageBox.Yes: - e.accept() - else: - e.ignore() - - - def keyPressEvent(self, event): - - self.handleKeypressEvent(event) diff --git a/photobooth/gui/PyQt5GuiHelpers.py b/photobooth/gui/PyQt5GuiHelpers.py deleted file mode 100644 index 6ce3ce4..0000000 --- a/photobooth/gui/PyQt5GuiHelpers.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Adaptation of QRoundProgressBar from -# https://sourceforge.net/projects/qroundprogressbar/ -# to PyQt5, using the PyQt4-version offered at -# https://stackoverflow.com/a/33583019 - -from math import ceil - -from PyQt5 import QtCore, QtGui, Qt, QtWidgets - - -class QRoundProgressBar(QtWidgets.QWidget): - - StyleDonut = 1 - StylePie = 2 - StyleLine = 3 - - PositionLeft = 180 - PositionTop = 90 - PositionRight = 0 - PositionBottom = -90 - - UF_VALUE = 1 - UF_PERCENT = 2 - UF_MAX = 4 - - def __init__(self): - super().__init__() - self.min = 0 - self.max = 100 - self.value = 25 - - self.nullPosition = self.PositionTop - self.barStyle = self.StyleDonut - self.outlinePenWidth = 1 - self.dataPenWidth = 1 - self.rebuildBrush = False - self.format = "%p%" - self.decimals = 1 - self.updateFlags = self.UF_PERCENT - self.gradientData = [] - self.donutThicknessRatio = 0.75 - - def setRange(self, min, max): - self.min = min - self.max = max - - if self.max < self.min: - self.max, self.min = self.min, self.max - - if self.value < self.min: - self.value = self.min - elif self.value > self.max: - self.value = self.max - - if not self.gradientData: - self.rebuildBrush = True - self.update() - - def setMinimum(self, min): - self.setRange(min, self.max) - - def setMaximum(self, max): - self.setRange(self.min, max) - - def setValue(self, val): - if self.value != val: - if val < self.min: - self.value = self.min - elif val > self.max: - self.value = self.max - else: - self.value = val - self.update() - - def setNullPosition(self, position): - if position != self.nullPosition: - self.nullPosition = position - if not self.gradientData: - self.rebuildBrush = True - self.update() - - def setBarStyle(self, style): - if style != self.barStyle: - self.barStyle = style - self.update() - - def setOutlinePenWidth(self, penWidth): - if penWidth != self.outlinePenWidth: - self.outlinePenWidth = penWidth - self.update() - - def setDataPenWidth(self, penWidth): - if penWidth != self.dataPenWidth: - self.dataPenWidth = penWidth - self.update() - - def setDataColors(self, stopPoints): - if stopPoints != self.gradientData: - self.gradientData = stopPoints - self.rebuildBrush = True - self.update() - - def setFormat(self, format): - if format != self.format: - self.format = format - self.valueFormatChanged() - - def resetFormat(self): - self.format = '' - self.valueFormatChanged() - - def setDecimals(self, count): - if count >= 0 and count != self.decimals: - self.decimals = count - self.valueFormatChanged() - - def setDonutThicknessRatio(self, val): - self.donutThicknessRatio = max(0., min(val, 1.)) - self.update() - - def paintEvent(self, event): - outerRadius = min(self.width(), self.height()) - baseRect = QtCore.QRectF(1, 1, outerRadius-2, outerRadius-2) - - buffer = QtGui.QImage(outerRadius, outerRadius, - QtGui.QImage.Format_ARGB32) - buffer.fill(0) - - p = QtGui.QPainter(buffer) - p.setRenderHint(QtGui.QPainter.Antialiasing) - - # data brush - self.rebuildDataBrushIfNeeded() - - # background - # self.drawBackground(p, buffer.rect()) - - # base circle - self.drawBase(p, baseRect) - - # data circle - arcStep = 360.0 / (self.max - self.min) * self.value - self.drawValue(p, baseRect, self.value, arcStep) - - # center circle - innerRect, innerRadius = self.calculateInnerRect(baseRect, outerRadius) - self.drawInnerBackground(p, innerRect) - - # text - self.drawText(p, innerRect, innerRadius, ceil(self.value)) - - # finally draw the bar - p.end() - - painter = QtGui.QPainter(self) - painter.drawImage(0, 0, buffer) - - def drawBackground(self, p, baseRect): - p.fillRect(baseRect, self.palette().window()) - - def drawBase(self, p, baseRect): - bs = self.barStyle - if bs == self.StyleDonut: - p.setPen(QtGui.QPen(self.palette().shadow().color(), - self.outlinePenWidth)) - p.setBrush(self.palette().base()) - p.drawEllipse(baseRect) - elif bs == self.StylePie: - p.setPen(QtGui.QPen(self.palette().base().color(), - self.outlinePenWidth)) - p.setBrush(self.palette().base()) - p.drawEllipse(baseRect) - elif bs == self.StyleLine: - color = self.palette().base().color() - color.setAlpha(100) - brush = self.palette().base() - brush.setColor(color) - p.setPen(QtGui.QPen(self.palette().base().color(), - self.outlinePenWidth)) - p.setBrush(brush) - # p.drawEllipse(baseRect) - # p.setPen(QtGui.QPen(self.palette().base().color(), - # self.outlinePenWidth)) - # p.setBrush(Qt.Qt.NoBrush) - p.drawEllipse(baseRect.adjusted(self.outlinePenWidth/2, - self.outlinePenWidth/2, - -self.outlinePenWidth/2, - -self.outlinePenWidth/2)) - - def drawValue(self, p, baseRect, value, arcLength): - # nothing to draw - if value == self.min: - return - - # for Line style - if self.barStyle == self.StyleLine: - p.setPen(QtGui.QPen(self.palette().highlight().color(), - self.dataPenWidth)) - p.setBrush(Qt.Qt.NoBrush) - p.drawArc(baseRect.adjusted(self.outlinePenWidth/2, - self.outlinePenWidth/2, - -self.outlinePenWidth/2, - -self.outlinePenWidth/2), - self.nullPosition * 16, - -arcLength * 16) - return - - # for Pie and Donut styles - dataPath = QtGui.QPainterPath() - dataPath.setFillRule(Qt.Qt.WindingFill) - - # pie segment outer - dataPath.moveTo(baseRect.center()) - dataPath.arcTo(baseRect, self.nullPosition, -arcLength) - dataPath.lineTo(baseRect.center()) - - p.setBrush(self.palette().highlight()) - p.setPen(QtGui.QPen(self.palette().shadow().color(), - self.dataPenWidth)) - p.drawPath(dataPath) - - def calculateInnerRect(self, baseRect, outerRadius): - # for Line style - if self.barStyle == self.StyleLine: - innerRadius = outerRadius - self.outlinePenWidth - else: # for Pie and Donut styles - innerRadius = outerRadius * self.donutThicknessRatio - - delta = (outerRadius - innerRadius) / 2. - innerRect = QtCore.QRectF(delta, delta, innerRadius, innerRadius) - return innerRect, innerRadius - - def drawInnerBackground(self, p, innerRect): - if self.barStyle == self.StyleDonut: - p.setBrush(self.palette().alternateBase()) - - cmod = p.compositionMode() - p.setCompositionMode(QtGui.QPainter.CompositionMode_Source) - - p.drawEllipse(innerRect) - - p.setCompositionMode(cmod) - - def drawText(self, p, innerRect, innerRadius, value): - if not self.format: - return - - text = self.valueToText(value) - - # !!! to revise - f = self.font() - f.setPixelSize(innerRadius * 0.8 / len(text)) - p.setFont(f) - - textRect = innerRect - p.setPen(self.palette().text().color()) - p.drawText(textRect, Qt.Qt.AlignCenter, text) - - def valueToText(self, value): - textToDraw = self.format - - format_string = '{' + ':.{}f'.format(self.decimals) + '}' - - if self.updateFlags & self.UF_VALUE: - textToDraw = textToDraw.replace("%v", format_string.format(value)) - - if self.updateFlags & self.UF_PERCENT: - perc = (value - self.min) / (self.max - self.min) * 100.0 - textToDraw = textToDraw.replace("%p", format_string.format(perc)) - - if self.updateFlags & self.UF_MAX: - m = self.max - self.min + 1 - textToDraw = textToDraw.replace("%m", format_string.format(m)) - - return textToDraw - - def valueFormatChanged(self): - self.updateFlags = 0 - - if "%v" in self.format: - self.updateFlags |= self.UF_VALUE - - if "%p" in self.format: - self.updateFlags |= self.UF_PERCENT - - if "%m" in self.format: - self.updateFlags |= self.UF_MAX - - self.update() - - def rebuildDataBrushIfNeeded(self): - if self.rebuildBrush: - self.rebuildBrush = False - - dataBrush = QtGui.QConicalGradient() - dataBrush.setCenter(0.5, 0.5) - dataBrush.setCoordinateMode(QtGui.QGradient.StretchToDeviceMode) - - for pos, color in self.gradientData: - dataBrush.setColorAt(1.0 - pos, color) - - # angle - dataBrush.setAngle(self.nullPosition) - - p = self.palette() - p.setBrush(QtGui.QPalette.Highlight, dataBrush) - self.setPalette(p) diff --git a/photobooth/gui/Qt5Gui/Postprocessor.py b/photobooth/gui/Qt5Gui/Postprocessor.py new file mode 100644 index 0000000..97bf1f3 --- /dev/null +++ b/photobooth/gui/Qt5Gui/Postprocessor.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import queue + +from .. import GuiState +from ..GuiPostprocess import PrintPostprocess + + +class Postprocessor: + + def __init__(self, config): + + super().__init__() + + self._task_list = [] + self._queue = queue.Queue() + + if config.getBool('Printer', 'enable'): + module = config.get('Printer', 'module') + size = (config.getInt('Printer', 'width'), + config.getInt('Printer', 'height')) + self._task_list.append(PrintPostprocess(module, size)) + + def fill(self, picture): + + for task in self._task_list: + self._queue.put(task.get(picture)) + + def work(self, msg_box): + + while True: + try: + task = self._queue.get(block=False) + except queue.Empty: + return + + if isinstance(task, GuiState.PrintState): + if msg_box.question('Print picture?', + 'Do you want to print the picture?'): + task.handler() + msg_box.information('Printing...', + 'Picture sent to printer.') + else: + raise ValueError('Unknown task') diff --git a/photobooth/gui/Qt5Gui/PyQt5Gui.py b/photobooth/gui/Qt5Gui/PyQt5Gui.py new file mode 100644 index 0000000..38b39dc --- /dev/null +++ b/photobooth/gui/Qt5Gui/PyQt5Gui.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging + +from PyQt5 import QtCore +from PyQt5 import QtWidgets + +from PIL import ImageQt + +from .. import GuiState +from ..GuiSkeleton import GuiSkeleton + +from . import Frames +from . import Postprocessor +from . import Receiver + + +class PyQt5Gui(GuiSkeleton): + + def __init__(self, argv, config, camera_conn, worker_queue): + + super().__init__() + + self._cfg = config + self._conn = camera_conn + + self._registerCallbacks() + self._initUI(argv) + self._initReceiver() + + self._postprocess = Postprocessor.Postprocessor(self._cfg) + + def run(self): + + self._showWelcomeScreen() + exit_code = self._app.exec_() + self._gui = None + return exit_code + + def close(self): + + self._gui.close() + + 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 + + def _initUI(self, argv): + + self._disableTrigger() + + self._app = QtWidgets.QApplication(argv) + self._gui = PyQt5MainWindow(self._cfg, self._handleKeypressEvent) + + def _initReceiver(self): + + self._receiver = Receiver.Receiver([self._conn]) + self._receiver.notify.connect(self.handleState) + self._receiver.start() + + def _setWidget(self, widget): + + self._gui.setCentralWidget(widget) + + def _enableEscape(self): + + self._is_escape = True + + def _disableEscape(self): + + self._is_escape = False + + def _enableTrigger(self): + + self._is_trigger = True + + def _disableTrigger(self): + + self._is_trigger = False + + def _sendStart(self): + + self._conn.send('start') + + def _sendTrigger(self, state): + + self._conn.send('triggered') + + def _sendAck(self): + + self._conn.send('ack') + + def _sendCancel(self): + + self._conn.send('cancel') + + def _sendTeardown(self, state): + + 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 _postprocessPicture(self, picture): + + self._postprocess.fill(picture) + self._postprocess.work(MessageBox(self._gui)) + self._sendAck() + + def _showWelcomeScreen(self): + + self._disableTrigger() + self._disableEscape() + self._lastHandle = self._showWelcomeScreen + self._setWidget(Frames.Start(self._showStart, self._showSettings, + self.close)) + if QtWidgets.QApplication.overrideCursor() != 0: + QtWidgets.QApplication.restoreOverrideCursor() + + def _showSettings(self): + + 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._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): + + self._enableEscape() + self._enableTrigger() + self._lastHandle = self._showIdle + self._setWidget(Frames.IdleMessage()) + + def _showGreeter(self, state): + + self._enableEscape() + self._disableTrigger() + + num_pic = (self._cfg.getInt('Picture', 'num_x'), + self._cfg.getInt('Picture', 'num_x')) + greeter_time = self._cfg.getInt('Photobooth', 'greeter_time') * 1000 + + self._setWidget(Frames.GreeterMessage(*num_pic)) + QtCore.QTimer.singleShot(greeter_time, self._sendAck) + + def _showCountdown(self, state): + + countdown_time = self._cfg.getInt('Photobooth', 'countdown_time') + self._setWidget(Frames.CountdownMessage(countdown_time, self._sendAck)) + + def _showPreview(self, state): + + self._gui.centralWidget().picture = ImageQt.ImageQt(state.picture) + self._gui.centralWidget().update() + + def _showPose(self, state): + + num_pic = (self._cfg.getInt('Picture', 'num_x'), + self._cfg.getInt('Picture', 'num_x')) + self._setWidget(Frames.PoseMessage(state.num_picture, *num_pic)) + + def _showAssemble(self, state): + + self._setWidget(Frames.WaitMessage('Processing picture...')) + + def _showReview(self, state): + + img = 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._postprocessPicture(state.picture)) + + def _showError(self, state): + + logging.error('%s: %s', state.title, state.message) + reply = QtWidgets.QMessageBox.warning(self._gui, state.title, + state.message, + QtWidgets.QMessageBox.Close | + QtWidgets.QMessageBox.Retry, + QtWidgets.QMessageBox.Retry) + if reply == QtWidgets.QMessageBox.Retry: + self._sendAck() + self._lastState() + else: + self._sendCancel() + self._showWelcomeScreen() + + +class PyQt5MainWindow(QtWidgets.QMainWindow): + + def __init__(self, config, keypress_handler): + + super().__init__() + + self._cfg = config + self._handle_key = keypress_handler + self._initUI() + + def _initUI(self): + + self.setWindowTitle('Photobooth') + + if self._cfg.getBool('Gui', 'fullscreen'): + self.showFullScreen() + else: + self.resize(self._cfg.getInt('Gui', 'width'), + self._cfg.getInt('Gui', 'height')) + self.show() + + def closeEvent(self, e): + + reply = QtWidgets.QMessageBox.question(self, 'Confirmation', + "Quit Photobooth?", + QtWidgets.QMessageBox.Yes | + QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No) + + if reply == QtWidgets.QMessageBox.Yes: + e.accept() + else: + e.ignore() + + def keyPressEvent(self, event): + + self._handle_key(event) + + +class MessageBox: + + def __init__(self, parent): + + super().__init__() + + self._parent = parent + + def question(self, title, message): + + reply = QtWidgets.QMessageBox.question(self._parent, title, message, + QtWidgets.QMessageBox.Yes | + QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No) + return reply == QtWidgets.QMessageBox.Yes + + def information(self, title, message): + + QtWidgets.QMessageBox.information(self._parent, title, message, + QtWidgets.QMessageBox.Ok) diff --git a/photobooth/gui/Qt5Gui/Receiver.py b/photobooth/gui/Qt5Gui/Receiver.py new file mode 100644 index 0000000..b647aea --- /dev/null +++ b/photobooth/gui/Qt5Gui/Receiver.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import multiprocessing as mp + +from PyQt5 import QtCore + + +class Receiver(QtCore.QThread): + + notify = QtCore.pyqtSignal(object) + + def __init__(self, conn): + + super().__init__() + self._conn = conn + + def handle(self, state): + + self.notify.emit(state) + + 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) diff --git a/photobooth/gui/Qt5Gui/__init__.py b/photobooth/gui/Qt5Gui/__init__.py new file mode 100644 index 0000000..4cd9112 --- /dev/null +++ b/photobooth/gui/Qt5Gui/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from .PyQt5Gui import PyQt5Gui # noqa diff --git a/photobooth/gui/__init__.py b/photobooth/gui/__init__.py index 9af3d86..ba9626c 100644 --- a/photobooth/gui/__init__.py +++ b/photobooth/gui/__init__.py @@ -1,19 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from .GuiState import * # noqa -from .GuiPostprocess import * # noqa +# from .GuiState import * # noqa +# from .GuiPostprocess import * # noqa + +from . import GuiState # noqa # Available gui modules as tuples of (config name, module name, class name) -modules = (('PyQt5', 'PyQt5Gui', 'PyQt5Gui'), ) +modules = (('PyQt5', 'Qt5Gui', 'PyQt5Gui'), ) -class Gui: +# class Gui: - def __init__(self): +# def __init__(self): - pass +# pass - def run(self, camera_conn, worker_queue): +# def run(self, camera_conn, worker_queue): - raise NotImplementedError() +# raise NotImplementedError() diff --git a/photobooth/main.py b/photobooth/main.py index 5ec6ad7..709b6c7 100644 --- a/photobooth/main.py +++ b/photobooth/main.py @@ -32,7 +32,7 @@ class CameraProcess(mp.Process): def run_camera(self): - try: + # try: cap = lookup_and_import( camera.modules, self.cfg.get('Camera', 'module'), 'camera') @@ -40,14 +40,14 @@ class CameraProcess(mp.Process): self.cfg, cap, self.conn, self.worker_queue) return photobooth.run() - except BaseException as e: - self.conn.send(gui.ErrorState('Camera error', str(e))) - event = self.conn.recv() - if str(event) in ('cancel', 'ack'): - return 123 - else: - logging.error('Unknown event received: %s', str(event)) - raise RuntimeError('Unknown event received', str(event)) + # except BaseException as e: + # self.conn.send(gui.GuiState.ErrorState('Camera error', str(e))) + # event = self.conn.recv() + # if str(event) in ('cancel', 'ack'): + # return 123 + # else: + # logging.error('Unknown event received: %s', str(event)) + # raise RuntimeError('Unknown event received', str(event)) def run(self): @@ -96,7 +96,7 @@ class GuiProcess(mp.Process): Gui = lookup_and_import(gui.modules, self.cfg.get('Gui', 'module'), 'gui') - sys.exit(Gui(self.argv, self.cfg).run(self.conn, self.queue)) + sys.exit(Gui(self.argv, self.cfg, self.conn, self.queue).run()) def run(argv):