diff --git a/photobooth/defaults.cfg b/photobooth/defaults.cfg index e33f27d..8eccbce 100644 --- a/photobooth/defaults.cfg +++ b/photobooth/defaults.cfg @@ -46,6 +46,8 @@ greeter_time = 3 countdown_time = 8 # Display time of assembled picture (shown after last shot) display_time = 5 +# Timeout for postprocessing (shown after review) +postprocess_time = 60 [Picture] # Basedir of output pictures diff --git a/photobooth/gui/GuiPostprocess.py b/photobooth/gui/GuiPostprocess.py deleted file mode 100644 index f3e44bd..0000000 --- a/photobooth/gui/GuiPostprocess.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Photobooth - a flexible photo booth software -# Copyright (C) 2018 Balthasar Reuter -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import logging - -from .. import printer -from ..util import lookup_and_import -from .GuiState import PrintState - - -class GuiPostprocess: - - def __init__(self, **kwargs): - - assert not kwargs - - def get(self, picture): - - raise NotImplementedError() - - def confirm(self, picture): - - raise NotImplementedError() - - -class PrintPostprocess(GuiPostprocess): - - def __init__(self, printer_module, page_size, **kwargs): - - super().__init__(**kwargs) - - Printer = lookup_and_import(printer.modules, printer_module, 'printer') - self._printer = Printer(page_size, True) - - def get(self, picture): - - return PrintState(lambda: self.do(picture), False) - - def confirm(self, picture): - - return PrintState(lambda: None, True) - - def do(self, picture): - - logging.info('Printing picture') - self._printer.print(picture) diff --git a/photobooth/gui/GuiPostprocessor.py b/photobooth/gui/GuiPostprocessor.py new file mode 100644 index 0000000..86e3ac8 --- /dev/null +++ b/photobooth/gui/GuiPostprocessor.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Photobooth - a flexible photo booth software +# Copyright (C) 2018 Balthasar Reuter +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import queue + +from .. import printer +from ..util import lookup_and_import + + +class GuiPostprocessor: + + 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 get(self, picture): + + tasks = [task.get(picture) for task in self._task_list] + return tasks + + +class PostprocessTask: + + def __init__(self): + + super().__init__() + + def get(self, picture): + + raise NotImplementedError() + + +class PostprocessItem: + + def __init__(self, label, action): + + super().__init__() + self.label = label + self.action = action + + @property + def label(self): + + return self._label + + @label.setter + def label(self, label): + + if not isinstance(label, str): + raise TypeError('Label must be a string') + + self._label = label + + @property + def action(self): + + return self._action + + @action.setter + def action(self, action): + + if not callable(action): + raise TypeError('Action must be callable') + + self._action = action + + +class PrintPostprocess(PostprocessTask): + + def __init__(self, printer_module, page_size, **kwargs): + + super().__init__(**kwargs) + + Printer = lookup_and_import(printer.modules, printer_module, 'printer') + self._printer = Printer(page_size, True) + + def get(self, picture): + + return PostprocessItem('Print', lambda: self._printer.print(picture)) diff --git a/photobooth/gui/Qt5Gui/Frames.py b/photobooth/gui/Qt5Gui/Frames.py index 2306e02..8efd287 100644 --- a/photobooth/gui/Qt5Gui/Frames.py +++ b/photobooth/gui/Qt5Gui/Frames.py @@ -309,6 +309,44 @@ class CountdownMessage(QtWidgets.QFrame): painter.end() +class PostprocessMessage(Widgets.TransparentOverlay): + + def __init__(self, parent, tasks, idle_handle, timeout=None, + timeout_handle=None): + + if timeout_handle is None: + timeout_handle = idle_handle + + super().__init__(parent, timeout, timeout_handle) + self.setObjectName('PostprocessMessage') + self.initFrame(tasks, idle_handle) + + def initFrame(self, tasks, idle_handle): + + def disableAndCall(button, handle): + button.setEnabled(False) + handle() + + def createButton(task): + button = QtWidgets.QPushButton(task.label) + button.clicked.connect(lambda: disableAndCall(button, task.action)) + return button + + buttons = [createButton(task) for task in tasks] + buttons.append(QtWidgets.QPushButton('Start over')) + buttons[-1].clicked.connect(idle_handle) + + button_lay = QtWidgets.QGridLayout() + for i, button in enumerate(buttons): + pos = divmod(i, 2) + button_lay.addWidget(button, *pos) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(QtWidgets.QLabel('Happy?')) + layout.addLayout(button_lay) + self.setLayout(layout) + + class SetDateTime(QtWidgets.QFrame): def __init__(self, cancel_action, restart_action): @@ -539,11 +577,18 @@ class Settings(QtWidgets.QFrame): displ_time.setValue(self._cfg.getInt('Photobooth', 'display_time')) self.add('Photobooth', 'display_time', displ_time) + postproc_time = QtWidgets.QSpinBox() + postproc_time.setRange(0, 1000) + postproc_time.setValue(self._cfg.getInt('Photobooth', + 'postprocess_time')) + self.add('Photobooth', 'postprocess_time', postproc_time) + layout = QtWidgets.QFormLayout() layout.addRow('Show preview during countdown:', preview) layout.addRow('Greeter time before countdown [s]:', greet_time) layout.addRow('Countdown time [s]:', count_time) layout.addRow('Picture display time [s]:', displ_time) + layout.addRow('Postprocess timeout [s]:', postproc_time) widget = QtWidgets.QWidget() widget.setLayout(layout) @@ -734,6 +779,8 @@ class Settings(QtWidgets.QFrame): str(self.get('Photobooth', 'countdown_time').text())) self._cfg.set('Photobooth', 'display_time', str(self.get('Photobooth', 'display_time').text())) + self._cfg.set('Photobooth', 'postprocess_time', + str(self.get('Photobooth', 'postprocess_time').text())) self._cfg.set('Camera', 'module', camera.modules[self.get('Camera', diff --git a/photobooth/gui/Qt5Gui/Postprocessor.py b/photobooth/gui/Qt5Gui/Postprocessor.py deleted file mode 100644 index f3338b8..0000000 --- a/photobooth/gui/Qt5Gui/Postprocessor.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Photobooth - a flexible photo booth software -# Copyright (C) 2018 Balthasar Reuter -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -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 index 1e17f88..db7b295 100644 --- a/photobooth/gui/Qt5Gui/PyQt5Gui.py +++ b/photobooth/gui/Qt5Gui/PyQt5Gui.py @@ -28,10 +28,10 @@ from PIL import ImageQt from .. import GuiState from ..GuiSkeleton import GuiSkeleton +from ..GuiPostprocessor import GuiPostprocessor from . import styles from . import Frames -from . import Postprocessor from . import Receiver @@ -48,7 +48,7 @@ class PyQt5Gui(GuiSkeleton): self._initUI(argv) self._initReceiver() - self._postprocess = Postprocessor.Postprocessor(self._cfg) + self._postprocess = GuiPostprocessor(self._cfg) def run(self): @@ -152,12 +152,6 @@ class PyQt5Gui(GuiSkeleton): 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() @@ -239,22 +233,27 @@ class PyQt5Gui(GuiSkeleton): review_time = self._cfg.getInt('Photobooth', 'display_time') * 1000 self._setWidget(Frames.PictureMessage(img)) QtCore.QTimer.singleShot(review_time, lambda: - self._postprocessPicture(state.picture)) + self._showPostprocess(state.picture)) + + def _showPostprocess(self, picture): + + tasks = self._postprocess.get(picture) + postproc_t = self._cfg.getInt('Photobooth', 'postprocess_time') + + Frames.PostprocessMessage(self._gui.centralWidget(), tasks, + self._sendAck, postproc_t * 1000) 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() + + def exec(*handles): + for handle in handles: + handle() + + MessageBox(self, MessageBox.RETRY, state.title, state.message, + exec(self._sendAck, self._lastState), + exec(self._sendCancel, self._showWelcomeScreen)) class PyQt5MainWindow(QtWidgets.QMainWindow): @@ -296,23 +295,67 @@ class PyQt5MainWindow(QtWidgets.QMainWindow): self._handle_key(event) -class MessageBox: +class MessageBox(QtWidgets.QWidget): - def __init__(self, parent): + QUESTION = 1 + RETRY = 2 + INFORMATION = 3 - super().__init__() + def __init__(self, parent, type, title, message, *handles): - self._parent = parent + super().__init__(parent) - def question(self, title, message): + if type == MessageBox.QUESTION: + self.question(title, message, *handles) + elif type == MessageBox.RETRY: + self.retry(title, message, *handles) + else: + raise ValueError('Unknown type specified') - reply = QtWidgets.QMessageBox.question(self._parent, title, message, - QtWidgets.QMessageBox.Yes | - QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) - return reply == QtWidgets.QMessageBox.Yes + def question(self, title, message, *handles): - def information(self, title, message): + lbl_title = QtWidgets.QLabel(title) + lbl_title.setObjectName('title') - QtWidgets.QMessageBox.information(self._parent, title, message, - QtWidgets.QMessageBox.Ok) + lbl_message = QtWidgets.QLabel(message) + lbl_message.setObjectName('message') + + btn_yes = QtWidgets.QPushButton('Yes') + btn_yes.clicked.connect(handles[0]) + + btn_no = QtWidgets.QPushButton('No') + btn_no.clicked.connect(handles[1]) + + lay_buttons = QtWidgets.QHBoxLayout() + lay_buttons.addWidget(btn_yes) + lay_buttons.addWidget(btn_no) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(lbl_title) + layout.addWidget(lbl_message) + layout.addLayout(lay_buttons) + self.setLayout(layout) + + def retry(self, title, message, *handles): + + lbl_title = QtWidgets.QLabel(title) + lbl_title.setObjectName('title') + + lbl_message = QtWidgets.QLabel(message) + lbl_message.setObjectName('message') + + btn_retry = QtWidgets.QPushButton('Retry') + btn_retry.clicked.connect(handles[0]) + + btn_cancel = QtWidgets.QPushButton('Cancel') + btn_cancel.clicked.connect(handles[1]) + + lay_buttons = QtWidgets.QHBoxLayout() + lay_buttons.addWidget(btn_retry) + lay_buttons.addWidget(btn_cancel) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(lbl_title) + layout.addWidget(lbl_message) + layout.addLayout(lay_buttons) + self.setLayout(layout) diff --git a/photobooth/gui/Qt5Gui/Widgets.py b/photobooth/gui/Qt5Gui/Widgets.py index 6f87f5d..026d637 100644 --- a/photobooth/gui/Qt5Gui/Widgets.py +++ b/photobooth/gui/Qt5Gui/Widgets.py @@ -178,3 +178,35 @@ class RoundProgressBar(QtWidgets.QWidget): self._drawText(painter, inner_rect, inner_radius) painter.end() + + +class TransparentOverlay(QtWidgets.QWidget): + + def __init__(self, parent, timeout=None, timeout_handle=None): + + super().__init__(parent) + self.setObjectName('TransparentOverlay') + + rect = parent.rect() + rect.adjust(50, 50, -50, -50) + self.setGeometry(rect) + + if timeout is not None: + self._handle = timeout_handle + self._timer = self.startTimer(timeout) + + self.show() + + def paintEvent(self, event): + + opt = QtWidgets.QStyleOption() + opt.initFrom(self) + painter = QtGui.QPainter(self) + self.style().drawPrimitive(QtWidgets.QStyle.PE_Widget, opt, painter, + self) + painter.end() + + def timerEvent(self, event): + + self.killTimer(self._timer) + self._handle() diff --git a/photobooth/gui/Qt5Gui/stylesheets/pastel.qss b/photobooth/gui/Qt5Gui/stylesheets/pastel.qss index ea4ff12..8c47e02 100644 --- a/photobooth/gui/Qt5Gui/stylesheets/pastel.qss +++ b/photobooth/gui/Qt5Gui/stylesheets/pastel.qss @@ -24,14 +24,14 @@ QPushButton { } QPushButton:pressed { - background-color: #66ffffff; + background-color: #66eeeeee; } /* Idle Screen */ QFrame#IdleMessage { background-image: url(photobooth/gui/Qt5Gui/images/arrow.png); - background-repeat:; no-repeat; + background-repeat: no-repeat; padding: 80px 400px 120px 80px; } @@ -80,9 +80,13 @@ QFrame#GreeterMessage QLabel#message { /* Countdown Screen */ QFrame#CountdownMessage { + background-color: #eeeeee; + border-style: outset; + border-width: 2px; + border-radius: 30px; + border-color: #eeeeee; margin: 20px; padding: 30px; - background-color: #eeeeee; } /* Pose Screen */ @@ -109,12 +113,48 @@ QFrame#WaitMessage QLabel { qproperty-alignment: AlignCenter; } -/* Picture Screen*/ +/* Picture Screen */ QFrame#PictureMessage { margin: 30px; } +/* Overlay message */ + +QWidget#TransparentOverlay { + background-color: #aaeeeeee; + border-style: outset; + border-width: 2px; + border-radius: 30px; + border-color: #eeeeee; + color: #333333; + padding: 40px; +} + +/* Postprocess message */ + +QWidget#PostprocessMessage QLabel { + color: #333333; + font-size: 110px; + qproperty-alignment: AlignCenter; +} + +QWidget#PostprocessMessage QPushButton { + color: #333333; + border-color: #333333; + margin: 20px; +} + +QWidget#PostprocessMessage QPushButton:pressed { + background-color: #66eeeeee; +} + +QWidget#PostprocessMessage QPushButton:disabled { + background-color: #66eeeeee; + color: #33eeeeee; + border-color: #33eeeeee; +} + /* Customizing settings */ QTabWidget::pane { diff --git a/photobooth/printer/PrinterPyQt5.py b/photobooth/printer/PrinterPyQt5.py index b8d86cc..02eedde 100644 --- a/photobooth/printer/PrinterPyQt5.py +++ b/photobooth/printer/PrinterPyQt5.py @@ -53,6 +53,8 @@ class PrinterPyQt5(Printer): self._printer.setOutputFileName('print_%d.pdf' % self._counter) self._counter += 1 + logging.info('Printing picture') + img = ImageQt.ImageQt(picture) img = img.scaled(self._printer.pageRect().size(), QtCore.Qt.KeepAspectRatio,