diff --git a/README.md b/README.md index 6a37684..bba286c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The goal of the project is to provide a configurable photobooth software for Ras Main features are: - User Interface with touchscreen input (input via mouse is not recommended). Control via buttons may be added in future. - Photo preview and capture via V4L2 (Raspberry Pi Camera or Webcam) or via GPhoto2 supported cameras. -- Printout with Canon Selphy photo printer. +- Printout with Canon Selphy photo printer or standard printer via CUPS. - Configurable image collages with own templates. User can select current template in application. - Password protected settings menu: - Copy all photos to USB storage. @@ -20,7 +20,7 @@ Main features are: '------------' '---------' ^ | - PWM| .-----------------------. + ENABLE/PWM| .-----------------------. | | DSLR Camera | | | connected via GPhoto2 | .------------------. | or raspi cam | @@ -31,7 +31,7 @@ Main features are: | QT/Quick | (_ -' | | .-------------. ] | | | | Touchscreen | | _,') - | | USB/VGA | | [_,-'_-'( + | | USB/HDMI | | [_,-'_-'( | |---------->| | (_).-' \ | | | | / / \ '------------------' | | @@ -62,15 +62,15 @@ Platform: Tested on PC plattform, Raspberry Pi 3B and Raspbarry Pi 4 (Raspberry Camera: Tested with Canon EOS 450D and Raspberry Pi camera. Every camera compatible with gPhoto2 or v4l2 will do. -Printer: Tested Canon Selphy Photo Printer CP910 +Printer: Tested Canon Selphy Photo Printer CP910 over wifi via https://github.com/saeugetier/go-selphy-cp. Tested with standard inkjet printer via CUPS. Light/Flash: - LED Driver: https://www.aliexpress.com/item/14-37-Inch-LED-LCD-Universal-TV-Backlight-Constant-Current-Board-Driver-Boost-Structure-Step-Up/32834942970.html - 20W LED: https://www.aliexpress.com/item/1Pcs-High-Power-10W-20W-30W-50W-100W-COB-Integrated-LED-Lamp-Chip-SMD-Bead-DC/32822371892.html -Display: A touchscreen is highly recommended +Display: A touchscreen connected via HDMI is highly recommended. -RTC: If using a Raspberry Pi, it is recommended to use a realtime clock. +I2C RTC: If using a Raspberry Pi, it is recommended to use a realtime clock. ### Housing / Electronics My own housing is documented in a seperate git repository: https://github.com/saeugetier/photobooth_hardware @@ -86,15 +86,15 @@ Prebuild images will be available soon. For more information how to create an ow ### Raspbian an a Raspberry Pi -Minimum Raspbian Buster must be used, which provides QT 5.11 development packages. In order to use GPhoto2 cameras, the Qt GPhoto2 plugin must be installed. +Minimum Raspbian Buster must be used, which provides QT 5.11 development packages. In order to use GPhoto2 cameras, the Qt GPhoto2 plugin (https://github.com/saeugetier/qtmultimedia-gphoto) must be installed. -For now there is no further support for deployment on Raspbian. +For now there is no further support for deployment on Raspbian. Please compile the program yourself from sources. ### Local PC -At least QT 5.11 development packages must be installed in order to compile the application. In order to use GPhoto2 cameras, the Qt GPhoto2 plugin must be installed. +At least QT 5.11 development packages must be installed in order to compile the application. In order to use GPhoto2 cameras, the Qt GPhoto2 plugin (https://github.com/saeugetier/qtmultimedia-gphoto) must be installed. -For now there is no further support for deployment on local pc. +For now there is no further support for deployment on local pc. Please compile the program yourself from sources. ## Configuration @@ -103,7 +103,7 @@ The local configuration file is stored in /home//.config/saeugetier/qtboot The file contains all application settings and the pin code for the settings menu password protection (only numbers are supported). Default pin code is: 0815 ### Template files -The local template files for your collage images are stored in /home//.local/share/saeugetier/qtbooth +The local template files for your collage images are stored in /home/user/.local/share/saeugetier/qtbooth It contains: - Background images for the image collages @@ -111,3 +111,56 @@ It contains: - Border images The templates can be imported from USB storage. All files with extension "xml,jpg,png,svg" in the folder "layout" will be copied to local template folder. + +### How to create own templates +Create your own "Collages.xml" file. You can use the file "XmlData.xml" to customize. + +The root node of the XML is named "catalog". It contains nodes for the templates for collages named "collage". The collage must contain at least one "image", a "name", a "background", a "foreground" and an "icon". You can use builtin backgrounds like "WhiteBackground.png" or create your custom one. Forground will be painted in the front layer. So it is highly recommended to have an alpha channel and some cutouts for your photos. + +Images will need information about the position and size. The range of the values for position and size is between 0.0 and 1.0. It is possible to define a image boarder for each image. + +There are optional properties like "printable", which generates a none printable collage (just a single image). So you can use the photobox as an simple camera without printing capabilities. + +Example for "Collages.xml": + +```xml + + + + Single Image + false + Single.svg + WhiteBackground.png + ExampleForeground.png + + + + + + Four Images Border + FourBorder.svg + StarsBackground.jpg + + + + RedBorder.png + + + + RedBorder.png + + + + RedBorder.png + + + + RedBorder.png + + + + +``` + +## Issue reporting +Please use the [issue tracker](https://github.com/saeugetier/photobooth/issues) for bug reporting and feature request. If there are specific bugs or feature request belonging to the Yocto image, please use [the issue tracker for poky-photobooth](https://github.com/saeugetier/poky-photobooth). diff --git a/XmlData.xml b/XmlData.xml index 0777fcd..2758858 100644 --- a/XmlData.xml +++ b/XmlData.xml @@ -1,8 +1,17 @@ - Single Image + Single Image No Print false + SingleNoPrint.svg + WhiteBackground.png + ExampleForeground.png + + + + + + Single Image Single.svg WhiteBackground.png @@ -54,11 +63,11 @@ - RedBorder.png + RedBorder.png - RedBorder.png + RedBorder.png @@ -66,7 +75,7 @@ - RedBorder.png + RedBorder.png diff --git a/docs/index.md b/docs/index.md index a0bee0b..2e4c775 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,14 +1,15 @@ # Overview -This project contains yet another [Photobooth](https://github.com/saeugetier/photobooth). The software is intended to run on a Raspberry Pi 2/3, but it can run on any linux PC. A preview image is displayed in capture mode. After countdown, a picture is taken and can be discarded or saved. Saved images can be printed immediately or printed as 2 by 2 collage image. All saved images are shown in the gallery page. +This project contains yet another [Photobooth](https://github.com/saeugetier/photobooth). The software is intended to run on a Raspberry Pi 2 and upwards, but it can run on any linux PC. A preview image is displayed in capture mode. After countdown, a picture is taken and can be discarded or saved. Saved images can be printed immediately or printed collage image with multiple photos. All saved images are shown on front page when the photobox is idle. As image source a DSLR over GPhoto2 or a V4L2 camera (Raspberry Pi Camera or webcam) can be used. -The application can be either compiled and deployed on an existing Raspbian installation, or a ready to go image can be build by Yocto build system: [https://github.com/saeugetier/poky-photobooth/](https://github.com/saeugetier/poky-photobooth/) +The application can be either compiled and deployed on an existing Raspberry Pi OS installation, or a ready to go image can be build by Yocto build system: [https://github.com/saeugetier/poky-photobooth/](https://github.com/saeugetier/poky-photobooth/) ## Video -[![Video](https://img.youtube.com/vi/Z9pVK-X5Wz4/0.jpg)](https://youtu.be/Z9pVK-X5Wz4) +[![](https://markdown-videos-api.jorgenkh.no/youtube/fB2aQGPT-wg?width=640&height=360)](https://youtu.be/fB2aQGPT-wg) +(Link to Youtube) # Technology ## Software diff --git a/images/ExampleForeground.png b/images/ExampleForeground.png new file mode 100644 index 0000000..516907a Binary files /dev/null and b/images/ExampleForeground.png differ diff --git a/images/icon/SingleNoPrint.svg b/images/icon/SingleNoPrint.svg new file mode 100644 index 0000000..3d99592 --- /dev/null +++ b/images/icon/SingleNoPrint.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/qml.qrc b/qml.qrc index 97b21c5..55d1423 100644 --- a/qml.qrc +++ b/qml.qrc @@ -25,8 +25,6 @@ qml/ImagePreviewForm.ui.qml qml/MainMenu.qml qml/MainMenuForm.ui.qml - qml/PrinterBusyPopup.qml - qml/PrinterBusyPopupForm.ui.qml qml/SettingsPassword.qml qml/SettingsPasswordForm.ui.qml qml/SettingsPopup.qml @@ -73,5 +71,8 @@ shaders/posterize.fsh qml/EffectSelectionPopup.qml qml/EffectSelectionPopupForm.ui.qml + qml/content/PrinterPopup.qml + images/ExampleForeground.png + images/icon/SingleNoPrint.svg diff --git a/qml/Application.qml b/qml/Application.qml index 9268d39..5aca03a 100644 --- a/qml/Application.qml +++ b/qml/Application.qml @@ -18,6 +18,11 @@ ApplicationWindow { property Printer printer : printerFactory.getPrinter(applicationSettings.printerName) + Component.onCompleted: + { + filesystem.checkImageFolders() + } + /* FontLoader { name: "fontello" @@ -75,6 +80,21 @@ ApplicationWindow { applicationSettings.printEnable = mainMenu.settingsPopup.switchPrinter.checked } + mainMenu.settingsPopup.switchMultiplePrints.onCheckedChanged: + { + applicationSettings.multiplePrints = mainMenu.settingsPopup.switchMultiplePrints.checked + } + + mainMenu.settingsPopup.switchHideSnapshotSettings.onCheckedChanged: + { + applicationSettings.disableSnapshotSettingsPane = mainMenu.settingsPopup.switchHideSnapshotSettings.checked + } + + mainMenu.settingsPopup.switchHideEffectPopup.onCheckedChanged: + { + applicationSettings.disableEffectPopup = mainMenu.settingsPopup.switchHideEffectPopup.checked + } + mainMenu.settingsPopup.comboBoxLanguages.onDisplayTextChanged: { applicationSettings.language = mainMenu.settingsPopup.comboBoxLanguages.displayText @@ -110,6 +130,13 @@ ApplicationWindow { { applicationSettings.windowMode = mainMenu.settingsPopup.comboWindowMode.currentIndex == 0 ? Window.Maximized : Window.FullScreen } + + mainMenu.settingsPopup.comboBoxCamera.onCurrentIndexChanged: + { + applicationSettings.cameraName = mainMenu.settingsPopup.comboBoxCamera.currentText + } + + mainMenu.printerBusy: printer.busy } Settings @@ -123,13 +150,23 @@ ApplicationWindow { property bool cameraMirrored: true property string printerName: "No Printer" property int windowMode: Window.Maximized + property bool multiplePrints: false + property bool disableSnapshotSettingsPane: false + property bool disableEffectPopup: false + property string cameraName: "" Component.onCompleted: { flow.mainMenu.settingsPopup.printerEnabled.checked = printEnable + flow.mainMenu.settingsPopup.switchMultiplePrints.checked = multiplePrints flow.mainMenu.settingsPinCode = password flow.mainMenu.settingsPopup.mirrorCamera.checked = cameraMirrored + flow.mainMenu.settingsPopup.switchHideSnapshotSettings.checked = disableSnapshotSettingsPane + flow.mainMenu.settingsPopup.switchHideEffectPopup.checked = disableEffectPopup flow.mainMenuModel.setShowPrintable(printEnable) + flow.collageMenu.multiplePrints = multiplePrints + flow.snapshotMenu.hideSnapshotSettingsPane = disableSnapshotSettingsPane + flow.imagePreview.effectButton.visible = !disableEffectPopup } onPrinterNameChanged: @@ -141,5 +178,28 @@ ApplicationWindow { { flow.mainMenuModel.setShowPrintable(printEnable) } + + onMultiplePrintsChanged: + { + flow.collageMenu.multiplePrints = multiplePrints + } + + onDisableSnapshotSettingsPaneChanged: + { + flow.snapshotMenu.hideSnapshotSettingsPane = disableSnapshotSettingsPane + } + + onDisableEffectPopupChanged: + { + flow.imagePreview.effectButton.visible = !disableEffectPopup + } + + onCameraNameChanged: + { + print("Camera changed to " + cameraName) + var id = flow.mainMenu.settingsPopup.findDeviceId(cameraName) + print("Found ID: " + id) + flow.snapshotMenu.cameraRenderer.deviceId = id + } } } diff --git a/qml/ApplicationFlow.qml b/qml/ApplicationFlow.qml index 624e965..42b1d2e 100644 --- a/qml/ApplicationFlow.qml +++ b/qml/ApplicationFlow.qml @@ -25,6 +25,7 @@ ApplicationFlowForm { state = "snapshot" collageMenu.collageImage.imageModel = modelFactory.getCollageImageModel(mainMenu.selectedCollageName) collageMenu.collageImage.imageModel.clearImagePathes() + collageMenu.collageIsPrintable = mainMenu.collageIsPrintable mainMenu.selectedCollageName = "" } diff --git a/qml/CollageMenu.qml b/qml/CollageMenu.qml index cc87c72..c3141c2 100644 --- a/qml/CollageMenu.qml +++ b/qml/CollageMenu.qml @@ -1,10 +1,13 @@ import QtQuick 2.4 import Printer 1.0 +import "content" CollageMenuForm { id: form property alias collageImage: form.collageRenderer property Printer printer + property bool multiplePrints: false + property alias collageIsPrintable: form.showPrintButton signal next signal exit @@ -21,6 +24,9 @@ CollageMenuForm { printButton.onClicked: { + printButton.enabled = false + printerPopup.isPrinting = collageIsPrintable + printerPopup.visible = true console.log("Print button pressed") var path = applicationSettings.foldername.toString() path = path.replace(/^(file:\/{2})/,""); @@ -35,9 +41,21 @@ CollageMenuForm { { if(collageRenderer.saving === false) { + printerPopup.visible = false if(collageRenderer.savedFilename.length > 0) { - printer.printImage(collageRenderer.savedFilename) + if(collageIsPrintable) + { + if(!multiplePrints) + { + printer.printImage(collageRenderer.savedFilename, 1) + } + else + { + printer.printImage(collageRenderer.savedFilename, printCountTumbler.currentIndex + 1) + printCountTumbler.currentIndex = 0 + } + } exit() } } @@ -52,7 +70,7 @@ CollageMenuForm { } else { - printButton.enabled = true + printButton.enabled = !printer.busy || !collageIsPrintable } } @@ -61,14 +79,34 @@ CollageMenuForm { exit() } + // selector for multiple prints should only show if multiple prints are enabled in settings menu and collage is finished + plusButton.visible: multiplePrints && (form.state == "CollageFull") + minusButton.visible: multiplePrints && (form.state == "CollageFull") + printCountTumbler.visible: multiplePrints && collageIsPrintable && (form.state == "CollageFull") + + minusButton.onClicked: + { + if(printCountTumbler.currentIndex > 0) + { + printCountTumbler.currentIndex = printCountTumbler.currentIndex - 1 + } + } + + plusButton.onClicked: + { + if(printCountTumbler.currentIndex < (printCountTumbler.count - 1)) + { + printCountTumbler.currentIndex = printCountTumbler.currentIndex + 1 + } + } + onStateChanged: { if(state == "CollageFull") { if(printer.busy) { - printerBusyPopup.modal = 1 - printerBusyPopup.open() + printerPopup.visible = true } } } @@ -87,9 +125,13 @@ CollageMenuForm { target: printer onBusyChanged: { - if(!printer.busy && printerBusyPopup.opened) + if(!printer.busy) { - printerBusyPopup.close() + printerPopup.visible = false + if(collageRenderer.imagesLoading == 0) + { + printButton.enabled = true + } } } } diff --git a/qml/CollageMenuForm.ui.qml b/qml/CollageMenuForm.ui.qml index 99e12b8..efbd402 100644 --- a/qml/CollageMenuForm.ui.qml +++ b/qml/CollageMenuForm.ui.qml @@ -11,7 +11,11 @@ Item { property alias nextButton: nextButton property alias exitButton: exitButton property real printerRatio: 3 / 4 - property alias printerBusyPopup: printerBusyPopup + property alias printerPopup: printerPopup + property alias plusButton: plusButton + property alias minusButton: minusButton + property alias printCountTumbler: printCountTumbler + property bool showPrintButton : true Rectangle { color: "white" @@ -70,7 +74,7 @@ Item { Text { id: textLabel color: "#ffffff" - text: qsTr("Print") + text: showPrintButton ? qsTr("Print") : qsTr("Save") font.family: "DejaVu Serif" wrapMode: Text.WrapAnywhere font.pixelSize: 64 @@ -79,7 +83,7 @@ Item { ToolButton { id: printButton - text: "\uE802" // icon-print + text: showPrintButton ? "\uE802" : "\uE803" // icon-print or icon-floppy font.family: "fontello" font.pixelSize: 64 enabled: true @@ -124,9 +128,71 @@ Item { forward: false } - PrinterBusyPopup { - id: printerBusyPopup + ToolButton { + id: minusButton + text: "\uE814" // icon-minus + font.family: "fontello" + font.pixelSize: 48 + enabled: true + + anchors.bottom: parent.bottom + anchors.bottomMargin: 25 + anchors.rightMargin: 15 + anchors.right: printCountTumbler.left + + scale: hovered ? 1.1 : 1 + + layer.enabled: true + layer.effect: Glow { + color: "black" + samples: 20 + spread: 0.3 + } + } + + Tumbler { + height: 80 + id: printCountTumbler + model: ["1", "2", "3", "4"] + anchors.bottom: parent.bottom + anchors.bottomMargin: 20 + anchors.horizontalCenter: parent.horizontalCenter + wrap: false + visibleItemCount: 1 + delegate: Text { + text: modelData + font.pixelSize: 64 + color: "#ffffff" + font.family: "DejaVu Serif" + opacity: 0.4 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.6 + } + } + + ToolButton { + id: plusButton + text: "\uE813" // icon-plus + font.family: "fontello" + font.pixelSize: 48 + enabled: true + + anchors.bottom: parent.bottom + anchors.bottomMargin: 25 + anchors.left: printCountTumbler.right + + scale: hovered ? 1.1 : 1 + + layer.enabled: true + layer.effect: Glow { + color: "black" + samples: 20 + spread: 0.3 + } + } + + PrinterPopup { + id: printerPopup anchors.centerIn: parent + visible: false } states: [ diff --git a/qml/CollageSelector.qml b/qml/CollageSelector.qml index e9b8777..00e7474 100644 --- a/qml/CollageSelector.qml +++ b/qml/CollageSelector.qml @@ -6,6 +6,7 @@ CollageSelectorForm { id: form property alias iconModel: form.iconModel property string iconName: "" + property bool isPrintable: true signal selected upButton.onClicked: @@ -39,6 +40,7 @@ CollageSelectorForm { iconListView.currentIndex = index console.log("Selected index: " + Number(index).toString()) iconName = name + isPrintable = printable selected() } } diff --git a/qml/MainMenu.qml b/qml/MainMenu.qml index 2bd2b7b..ed4a441 100644 --- a/qml/MainMenu.qml +++ b/qml/MainMenu.qml @@ -11,6 +11,7 @@ MainMenuForm { property alias settingsPopup: settingsPopup property alias settingsPinCode: settingsPassword.password property string selectedCollageName : "" + property bool collageIsPrintable: true state: "IconNotSelected" signal collageSelected @@ -30,6 +31,7 @@ MainMenuForm { { state = "IconSelected" selectedCollageName = collageSelector.iconName + collageIsPrintable = collageSelector.isPrintable } continueButton.onClicked: diff --git a/qml/MainMenuForm.ui.qml b/qml/MainMenuForm.ui.qml index 83877d4..6082210 100644 --- a/qml/MainMenuForm.ui.qml +++ b/qml/MainMenuForm.ui.qml @@ -7,6 +7,7 @@ Item { id: element width: 640 height: 480 + property alias printerBusy: printerPopup.visible property alias settingsButton: settingsButton property alias collageRenderer: collageRenderer property alias backButton: backButton @@ -66,8 +67,8 @@ Item { x: leftMargin + horzitontalSpacing source: fileURL - sourceSize.height: 1024 - sourceSize.width: 1024 + sourceSize.height: 512 + sourceSize.width: 512 fillMode: Image.PreserveAspectFit transform: Rotation { origin.x: imageSlider.width / 4 @@ -183,6 +184,11 @@ Item { visible: false } + PrinterPopup { + id: printerPopup + anchors.centerIn: parent + } + states: [ State { name: "NoIconSelected" @@ -229,8 +235,7 @@ Item { /*##^## Designer { - D{i:0;height:480;width:640}D{i:14;anchors_x:139;anchors_y:394}D{i:13;anchors_x:139;anchors_y:394} -D{i:7;anchors_height:360;anchors_width:480;anchors_x:57;anchors_y:150} + D{i:0;height:480;width:640}D{i:15}D{i:14}D{i:7} } ##^##*/ diff --git a/qml/PrinterBusyPopup.qml b/qml/PrinterBusyPopup.qml deleted file mode 100644 index 2c82a35..0000000 --- a/qml/PrinterBusyPopup.qml +++ /dev/null @@ -1,8 +0,0 @@ -import QtQuick 2.4 - -PrinterBusyPopupForm { - button.onClicked: - { - close() - } -} diff --git a/qml/PrinterBusyPopupForm.ui.qml b/qml/PrinterBusyPopupForm.ui.qml deleted file mode 100644 index d0abba2..0000000 --- a/qml/PrinterBusyPopupForm.ui.qml +++ /dev/null @@ -1,45 +0,0 @@ -import QtQuick 2.4 -import QtQuick.Controls 2.3 -import "content" - -Popup { - id: popup - width: 600 - height: 400 - property alias button: button - - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent - - Button { - id: button - x: 248 - y: 293 - width: 85 - height: 65 - text: qsTr("OK") - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: 32 - } - - Text { - id: element - text: qsTr("Printer is busy currently. Please stand by.") - font.family: "DejaVu Serif" - anchors.top: parent.top - anchors.topMargin: 16 - horizontalAlignment: Text.AlignHCenter - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.left: parent.left - anchors.leftMargin: 10 - wrapMode: Text.WordWrap - font.pixelSize: 70 - } -} - -/*##^## -Designer { - D{i:2;anchors_x:8;anchors_y:16} -} -##^##*/ - diff --git a/qml/SettingsPassword.qml b/qml/SettingsPassword.qml index da45d86..5b58d83 100644 --- a/qml/SettingsPassword.qml +++ b/qml/SettingsPassword.qml @@ -43,13 +43,15 @@ SettingsPasswordForm { if(passwordInput.text == password) { unlocked = true + console.log("Password accepted: " + passwordInput.text) passwordAccepted() } else { + console.log("Password wrong: " + passwordInput.text) unlocked = false } - console.log("Password accepted: " + unlocked) + } } diff --git a/qml/SettingsPopup.qml b/qml/SettingsPopup.qml index 70434ae..2901cc2 100644 --- a/qml/SettingsPopup.qml +++ b/qml/SettingsPopup.qml @@ -1,10 +1,44 @@ import QtQuick 2.4 +import QtMultimedia 5.5 +import QtQuick.Controls 2.4 SettingsPopupForm { id: form property alias printerEnabled: form.switchPrinter property alias mirrorCamera: form.switchMirrorCamera + Component.onCompleted: + { + versionText = "Version: " + system.getGitHash() + } + + function makeCameraList() + { + var listModel = []; + var i; + var availableCameras = QtMultimedia.availableCameras; + console.log("Available Camera Count: " + Number(availableCameras.length).toString()) + for(i = 0; i < availableCameras.length; i++) + { + listModel.push(availableCameras[i].displayName) + } + return listModel; + } + + function findDeviceId(cameraName) + { + var i; + var availableCameras = QtMultimedia.availableCameras; + for(i = 0; i < availableCameras.length; i++) + { + if(availableCameras[i].displayName === cameraName) + { + return availableCameras[i].deviceId; + } + } + return QtMultimedia.defaultCamera.deviceId + } + onOpened: { if(filesystem.layoutFilesOnRemovableDrive()) @@ -25,6 +59,10 @@ SettingsPopupForm { buttonCopyPhotos.enabled = false } + comboBoxCamera.model = makeCameraList(); + var indexCamera = comboBoxCamera.find(applicationSettings.cameraName) + comboBoxCamera.currentIndex = indexCamera + var index = comboBoxPrinter.find(applicationSettings.printerName) comboBoxPrinter.currentIndex = index console.log("index: " + Number(index).toString()) @@ -65,7 +103,7 @@ SettingsPopupForm { Qt.quit() } - buttonDeletePhotos.onClicked: + buttonDeletePhotos.onActivated: { filesystem.deleteAllImages() } diff --git a/qml/SettingsPopupForm.ui.qml b/qml/SettingsPopupForm.ui.qml index 89660aa..2022220 100644 --- a/qml/SettingsPopupForm.ui.qml +++ b/qml/SettingsPopupForm.ui.qml @@ -6,6 +6,9 @@ Popup { id: popup width: 400 height: 500 + property alias switchHideSnapshotSettings: switchHideSnapshotSettings + property alias switchHideEffectPopup: switchHideEffectPopup + property alias switchMultiplePrints: switchMultiplePrints property alias buttonCloseProgram: buttonCloseProgram property alias comboWindowMode: comboWindowMode property alias labelTime: labelTime @@ -20,6 +23,8 @@ Popup { property alias buttonSetTime: buttonSetTime property alias switchMirrorCamera: switchMirrorCamera property alias comboBoxPrinter: comboBoxPrinter + property alias comboBoxCamera: comboBoxCamera + property alias versionText: labelVersionText.text Button { id: buttonClose @@ -114,8 +119,6 @@ Popup { Column { spacing: 5 - - /* Row { spacing: 5 Label { @@ -125,8 +128,9 @@ Popup { } ComboBox { id: comboBoxCamera + width: 280 } - }*/ + } Row { id: rowMirrorCamera spacing: 5 @@ -142,6 +146,38 @@ Popup { text: qsTr("mirror") } } + + Row { + id: rowHideSnapshotSettings + spacing: 5 + Label { + id: labelHideSnapshotSettings + text: qsTr("Snapshot Settings") + anchors.verticalCenter: switchHideSnapshotSettings.verticalCenter + horizontalAlignment: Text.AlignLeft + } + + Switch { + id: switchHideSnapshotSettings + text: qsTr("hide") + } + } + + Row { + id: rowHideEffectPopup + spacing: 5 + Label { + id: labelHideEffectPopup + text: qsTr("Effect Popup") + anchors.verticalCenter: rowHideEffectPopup.verticalCenter + horizontalAlignment: Text.AlignLeft + } + + Switch { + id: switchHideEffectPopup + text: qsTr("disable") + } + } } } @@ -180,6 +216,22 @@ Popup { width: 280 } } + + Row { + id: rowMultiplePrints + spacing: 5 + Label { + id: labelMultiplePrints + text: qsTr("Allow multiple prints") + anchors.verticalCenter: switchMultiplePrints.verticalCenter + horizontalAlignment: Text.AlignLeft + } + + Switch { + id: switchMultiplePrints + text: qsTr("enabled") + } + } } } @@ -253,11 +305,24 @@ Popup { id: buttonRestart text: qsTr("Restart") } + + } + Row + { + spacing: 10 Button { id: buttonCloseProgram text: qsTr("Exit Photobooth") } } + Row + { + Label + { + id: labelVersionText + text: "Version" + } + } } } } diff --git a/qml/SnapshotMenu.qml b/qml/SnapshotMenu.qml index b20ee1e..1cae643 100644 --- a/qml/SnapshotMenu.qml +++ b/qml/SnapshotMenu.qml @@ -56,6 +56,11 @@ SnapshotMenuForm { } } + snapshotSettings.onViewFinderBrightnessChanged: + { + ledBrightnessPin.value = 1.0 - snapshotSettings.viewFinderBrightness + } + shutterButton.onTriggerSnapshot: { cameraRenderer.takePhoto() @@ -73,7 +78,6 @@ SnapshotMenuForm { cameraRenderer.onSavedPhoto: { captured(filename) - shutterButton.reset() } cameraRenderer.onFailed: @@ -89,8 +93,9 @@ SnapshotMenuForm { loops: 1 PropertyAnimation { target: failureText; property: "visible"; to: true} PropertyAnimation { target: failureText; property: "opacity"; to: 1.0} - NumberAnimation { target: failureText; property: "opacity"; to: 0.0; duration: 1000} + NumberAnimation { target: failureText; property: "opacity"; to: 0.0; duration: 2000} PropertyAnimation { target: failureText; property: "visible"; to: false} + PropertyAnimation { target: form; property: "state"; to: "activated"} } Behavior on snapshotSettings.opacity { diff --git a/qml/SnapshotMenuForm.ui.qml b/qml/SnapshotMenuForm.ui.qml index 82ca443..8041a16 100644 --- a/qml/SnapshotMenuForm.ui.qml +++ b/qml/SnapshotMenuForm.ui.qml @@ -13,6 +13,7 @@ Item { property alias cameraRenderer: cameraRenderer property alias shutterButton: shutterButton property alias countdown: shutterButton.countDownTime + property bool hideSnapshotSettingsPane: false CameraRenderer { id: cameraRenderer @@ -72,7 +73,7 @@ Item { PropertyChanges { target: snapshotSettings - opacity: 1.0 + opacity: hideSnapshotSettingsPane ? 0.0 : 1.0 } PropertyChanges { diff --git a/qml/content/CameraRenderer.qml b/qml/content/CameraRenderer.qml index 3178ed4..5d789c6 100644 --- a/qml/content/CameraRenderer.qml +++ b/qml/content/CameraRenderer.qml @@ -11,6 +11,7 @@ Item { property bool photoProcessing: (state == "snapshot") property bool mirrored: true + property string deviceId: camera.deviceId function printDevicesToConsole(devices) { diff --git a/qml/content/CollageRenderer.qml b/qml/content/CollageRenderer.qml index f3534f4..d53bf4d 100644 --- a/qml/content/CollageRenderer.qml +++ b/qml/content/CollageRenderer.qml @@ -101,10 +101,21 @@ Item { } } + Image + { + id: foreground + sourceSize.height: 2048 + sourceSize.width: 2048 + + anchors.fill: background + fillMode: Image.PreserveAspectFit + } + onImageModelChanged: { imagesLoading = 0 background.source = filesystem.findFile(imageModel.backgroundImage, StandardPaths.standardLocations(StandardPaths.AppDataLocation), true) + foreground.source = filesystem.findFile(imageModel.foregroundImage, StandardPaths.standardLocations(StandardPaths.AppDataLocation), true) console.log("model chnaged. Size: " + Number(imageModel.rowCount()).toString()) } diff --git a/qml/content/PrinterPopup.qml b/qml/content/PrinterPopup.qml new file mode 100644 index 0000000..bc23815 --- /dev/null +++ b/qml/content/PrinterPopup.qml @@ -0,0 +1,43 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.5 +import QtGraphicalEffects 1.0 + +Item { + id: container + property bool isPrinting : true + + width: 300 + height: 300 + + Rectangle { + anchors.fill: parent + radius: 20 + color: "black" + opacity: 0.5 + } + + BusyIndicator + { + anchors.centerIn: parent + + width: 200 + height: 200 + running: true + } + + Text { + anchors.centerIn: parent + + color: "white" + text: isPrinting ? "\uE802" : "\uE803" // icon-print or icon-floppy + font.family: "fontello" + font.pixelSize: 80 + + layer.enabled: true + layer.effect: Glow { + color: "black" + samples: 20 + spread: 0.3 + } + } +} diff --git a/qtbooth.pro b/qtbooth.pro index 6881206..0215a40 100644 --- a/qtbooth.pro +++ b/qtbooth.pro @@ -63,3 +63,5 @@ contains(ANDROID_TARGET_ARCH,x86) { ANDROID_PACKAGE_SOURCE_DIR = \ $$PWD/android } + +DEFINES += GIT_CURRENT_SHA1="$(shell git -C \""$$_PRO_FILE_PWD_"\" describe)" diff --git a/src/abstractprinter.h b/src/abstractprinter.h index f406117..85e7b75 100644 --- a/src/abstractprinter.h +++ b/src/abstractprinter.h @@ -28,7 +28,7 @@ class AbstractPrinter : public QObject void failed(); public slots: - Q_INVOKABLE virtual int printImage(const QString &filename) = 0; + Q_INVOKABLE virtual int printImage(const QString &filename, int copyCount) = 0; }; Q_DECLARE_INTERFACE(AbstractPrinter, "AbstractPrinter") diff --git a/src/collageiconmodel.cpp b/src/collageiconmodel.cpp index 7afa6c7..dd6c7df 100644 --- a/src/collageiconmodel.cpp +++ b/src/collageiconmodel.cpp @@ -117,6 +117,8 @@ QVariant CollageIconModel::data(const QModelIndex &index, int role) const return icon.name(); else if (role == IconRole) return icon.icon(); + else if (role == PrintableRole) + return icon.printable(); return QVariant(); } @@ -125,6 +127,7 @@ QHash CollageIconModel::roleNames() const QHash roles; roles[NameRole] = "name"; roles[IconRole] = "icon"; + roles[PrintableRole] = "printable"; return roles; } diff --git a/src/collageimagemodel.cpp b/src/collageimagemodel.cpp index 4d13f1d..7085dce 100644 --- a/src/collageimagemodel.cpp +++ b/src/collageimagemodel.cpp @@ -38,6 +38,18 @@ bool CollageImageModel::parseXml(const QDomNode& node) return false; } + QDomNodeList foregroundNode = element.elementsByTagName("foreground"); + if(foregroundNode.count() == 1) + { + mForegroundImage = foregroundNode.item(0).toElement().text(); + } + else if(foregroundNode.count() > 1) + { + mErrorMsg = "multiple foreground nodes"; + mLine = element.lineNumber(); + return false; + } + QDomNodeList imagesNode = element.elementsByTagName("images"); if(imagesNode.length() != 1) { @@ -121,6 +133,11 @@ QUrl CollageImageModel::backgroundImage() const return mBackgroundImage; } +QUrl CollageImageModel::foregroundImage() const +{ + return mForegroundImage; +} + bool CollageImageModel::addImagePath(QUrl source, QString effect) { if(!collageFull()) { diff --git a/src/collageimagemodel.h b/src/collageimagemodel.h index 1077b47..c038818 100644 --- a/src/collageimagemodel.h +++ b/src/collageimagemodel.h @@ -53,6 +53,7 @@ class CollageImageModel : public QAbstractListModel, public ModelParser { Q_OBJECT Q_PROPERTY(QUrl backgroundImage READ backgroundImage NOTIFY backgroundImageChanged) + Q_PROPERTY(QUrl foregroundImage READ foregroundImage NOTIFY foregroundImageChanged) Q_PROPERTY(int countImagePathSet READ countImagePathSet NOTIFY countImagePatchSetChanged) Q_PROPERTY(bool collageFull READ collageFull NOTIFY collageFullChanged) public: @@ -73,6 +74,7 @@ class CollageImageModel : public QAbstractListModel, public ModelParser QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; QUrl backgroundImage() const; + QUrl foregroundImage() const; Q_INVOKABLE bool addImagePath(QUrl source, QString effect = ""); Q_INVOKABLE void clearImagePathes(); Q_INVOKABLE bool clearImagePath(int index); @@ -82,11 +84,13 @@ class CollageImageModel : public QAbstractListModel, public ModelParser int countImagePathSet() const; signals: void backgroundImageChanged(const QUrl &image); + void foregroundImageChanged(const QUrl &image); void countImagePatchSetChanged(const int &count); void collageFullChanged(bool full); protected: QList mImages; QUrl mBackgroundImage; + QUrl mForegroundImage; }; #endif // COLLAGEIMAGEMODEL_H diff --git a/src/fakeprinter.cpp b/src/fakeprinter.cpp index 7afa768..58bf1d8 100644 --- a/src/fakeprinter.cpp +++ b/src/fakeprinter.cpp @@ -21,9 +21,9 @@ bool FakePrinter::busy() return mBusyTimer.isActive(); } -int FakePrinter::printImage(const QString &filename) +int FakePrinter::printImage(const QString &filename, int copyCount) { - qDebug() << "Fake printer starts printing " << filename; + qDebug() << "Fake printer starts printing " << copyCount << " copies." << filename; mBusyTimer.start(1000 * 30); // 30 seconds; emit busyChanged(true); return 0; diff --git a/src/fakeprinter.h b/src/fakeprinter.h index bcef64e..029b5ad 100644 --- a/src/fakeprinter.h +++ b/src/fakeprinter.h @@ -13,7 +13,7 @@ class FakePrinter : public AbstractPrinter, public PrinterList QSize getPrintSize() override; bool printerOnline() override; bool busy() override; - int printImage(const QString &filename) override; + int printImage(const QString &filename, int copyCount) override; protected slots: void busyTimeout(); protected: diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 9dd7e0e..2a8c096 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -68,13 +68,28 @@ QUrl FileSystem::findFile(QString filename, QList searchPaths, bool search QString FileSystem::getImagePath() { - QSettings settings("Timmedia", "QML Photo Booth"); + QSettings settings("saeugetier", "qtbooth"); if(settings.contains("Application/foldername")) return settings.value("Application/foldername").value(); else return "file://" + QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } +void FileSystem::checkImageFolders() +{ + QString imagePath = getImagePath(); + imagePath = imagePath.right(imagePath.length() - QString("file://").length()); + if(!QDir(imagePath).exists()) + { + QDir().mkdir(imagePath); + } + QString collagePath = imagePath + "/collage"; + if(!QDir(collagePath).exists()) + { + QDir().mkdir(collagePath); + } +} + bool FileSystem::removableDriveMounted() { if(getRemovableDrivePath().length() > 0) @@ -89,6 +104,8 @@ void FileSystem::unmountRemoveableDrive() QProcess unmountProcess; unmountProcess.setProgram("umount"); unmountProcess.setArguments(QStringList() << this->getRemovableDrivePath()); + unmountProcess.start(); + unmountProcess.waitForFinished(); } void FileSystem::startCopyFilesToRemovableDrive() @@ -113,6 +130,9 @@ void FileSystem::startCopyFilesToRemovableDrive() QStringList filters; filters << "*.jpg" << "*.JPG"; QStringList files = imageDir.entryList(filters, QDir::Files); + filters << "*.png"; + QDir collageDir(imagePath + "/collage"); + files.append(collageDir.entryList(filters, QDir::Files)); int i; for(i = 0; i < files.count() && !this->m_copyFuture.isCanceled(); i++) @@ -123,7 +143,12 @@ void FileSystem::startCopyFilesToRemovableDrive() QFile::remove(removableDrivePath + "/" + files[i]); if(!QFile::copy(imagePath + "/" + files[i], removableDrivePath + "/" + files[i])) - qDebug() << "Copying file: " << files[i] << " was not successfull"; + { + if(!QFile::copy(imagePath + "/collage/" + files[i], removableDrivePath + "/" + files[i])) + { + qDebug() << "Copying file: " << files[i] << " was not successfull"; + } + } emit this->copyProgress(progress); } @@ -183,7 +208,7 @@ void FileSystem::deleteAllImages() imagePath = imagePath.right(imagePath.length() - QString("file://").length()); QDir imageDir(imagePath); QStringList filters; - filters << "*.jpg" << "*.JPG"; + filters << "*.jpg" << "*.JPG" << "*.png" << "*.PNG"; imageDir.setFilter(QDir::Files); imageDir.setNameFilters(filters); if(!imageDir.isEmpty() && imageDir.exists()) @@ -199,6 +224,8 @@ void FileSystem::deleteAllImages() } imageDir.setPath(imagePath + "/collage"); + imageDir.setFilter(QDir::Files); + imageDir.setNameFilters(filters); if(!imageDir.isEmpty() && imageDir.exists()) { QStringList files = imageDir.entryList(filters, QDir::Files); @@ -207,7 +234,7 @@ void FileSystem::deleteAllImages() for(i = 0; i < files.count(); i++) { qDebug() << "removing file: " << files[i]; - QFile::remove(imagePath + "/" + files[i]); + QFile::remove(imagePath + "/collage/" + files[i]); } } diff --git a/src/filesystem.h b/src/filesystem.h index c80af46..1f0ee42 100644 --- a/src/filesystem.h +++ b/src/filesystem.h @@ -12,6 +12,7 @@ class FileSystem : public QObject explicit FileSystem(QObject *parent = nullptr); Q_INVOKABLE QUrl findFile(QString filename, QList searchPaths, bool searchInResource = true); Q_INVOKABLE QString getImagePath(); + Q_INVOKABLE void checkImageFolders(); Q_INVOKABLE bool removableDriveMounted(); Q_INVOKABLE void unmountRemoveableDrive(); Q_INVOKABLE void startCopyFilesToRemovableDrive(); diff --git a/src/noprinter.cpp b/src/noprinter.cpp index 8082d8b..0253ffc 100644 --- a/src/noprinter.cpp +++ b/src/noprinter.cpp @@ -15,7 +15,7 @@ bool NoPrinter::busy() return false; } -int NoPrinter::printImage(const QString &filename) +int NoPrinter::printImage(const QString &filename, int copyCount) { Q_UNUSED(filename); } diff --git a/src/noprinter.h b/src/noprinter.h index 13ca1aa..0cfd6c5 100644 --- a/src/noprinter.h +++ b/src/noprinter.h @@ -12,7 +12,7 @@ class NoPrinter : public AbstractPrinter, public PrinterList QSize getPrintSize() override; bool printerOnline() override; bool busy() override; - int printImage(const QString &filename) override; + int printImage(const QString &filename, int copyCount) override; protected: explicit NoPrinter(QObject *parent = nullptr); static QStringList getAvailablePrintersInternal(); diff --git a/src/selphyprinter.cpp b/src/selphyprinter.cpp index 4c7ce1a..dca7253 100644 --- a/src/selphyprinter.cpp +++ b/src/selphyprinter.cpp @@ -47,15 +47,20 @@ QSize SelphyPrinter::getPrintSize() return QSize(3570,2380); //hard coded pixel size } -int SelphyPrinter::printImage(const QString &filename) +int SelphyPrinter::printImage(const QString &filename, int copyCount) { if(mIp.length() > 0) { QString imageMagickCommand = "convert " + filename + " -quality 100% " + filename + ".jpg"; QString selphyCommand = "selphy -printer_ip=" + mIp + " " + filename + ".jpg"; + QString printCommand = imageMagickCommand; + for(int i = 0; i < copyCount; i++) + { + printCommand = printCommand + " && " + selphyCommand; + } QStringList shParameters; shParameters << "-c"; - shParameters << imageMagickCommand + " && " + selphyCommand; + shParameters << printCommand; if(mPrinterProcess.state() == QProcess::NotRunning) { diff --git a/src/selphyprinter.h b/src/selphyprinter.h index f100001..cb13f45 100644 --- a/src/selphyprinter.h +++ b/src/selphyprinter.h @@ -14,7 +14,7 @@ class SelphyPrinter : public AbstractPrinter, public PrinterList Q_INVOKABLE bool printerOnline(); bool busy(); public slots: - Q_INVOKABLE int printImage(const QString &filename); + Q_INVOKABLE int printImage(const QString &filename, int copyCount); protected slots: void finished(int code, QProcess::ExitStatus status); protected: diff --git a/src/standardprinter.cpp b/src/standardprinter.cpp index a1e0791..4411792 100644 --- a/src/standardprinter.cpp +++ b/src/standardprinter.cpp @@ -27,11 +27,12 @@ bool StandardPrinter::busy() } } -int StandardPrinter::printImage(const QString &filename) +int StandardPrinter::printImage(const QString &filename, int copyCount) { mPrinter.setColorMode(QPrinter::Color); mPrinter.setFullPage(true); mPrinter.setOrientation(QPrinter::Landscape); + mPrinter.setCopyCount(copyCount); QImage img; if(img.load(filename)) { diff --git a/src/standardprinter.h b/src/standardprinter.h index 16837b4..0fd0c1e 100644 --- a/src/standardprinter.h +++ b/src/standardprinter.h @@ -14,7 +14,7 @@ class StandardPrinter : public AbstractPrinter, public PrinterList +#include System::System() { @@ -18,13 +19,35 @@ void System::restart() process.startDetached("reboot"); } -bool System::setTime(QDate date) +bool System::setTime(QDateTime date) { int result = -1; - QDateTime time(date); - time_t t = time.toTime_t(); + + qDebug() << "Set Time " << date.toString("yyyy-MM-dd hh::mm::ss"); #ifdef __linux__ - result = stime(&t); //return zero on success + QProcess process; + QStringList arguments; + arguments.append("-s"); + arguments.append(date.toString("yyyy-MM-dd hh:mm:ss")); + process.start("date", arguments); + process.waitForFinished(); + result = process.exitCode(); + if(result != 0) + { + qDebug() << "Setting time failed. Exit code: " << result; + qDebug() << process.readAllStandardOutput() << "\n" << process.readAllStandardError() << "\n" << process.errorString(); + } + else + { + process.start("hwclock -w"); + process.waitForFinished(); + result = process.exitCode(); + if(result != 0) + { + qDebug() << "Writing clock to hwclock failed with exit code:" << result; + qDebug() << process.readAllStandardOutput() << "\n" << process.readAllStandardError() << "\n" << process.errorString(); + } + } #elif __APPLE__ #pragma message ( "setting the time is not implemented for MacOS" ) #elif _WIN32 @@ -32,3 +55,8 @@ bool System::setTime(QDate date) #endif return (result == 0); } + +QString System::getGitHash() const +{ + return QString(QT_STRINGIFY(GIT_CURRENT_SHA1)); +} diff --git a/src/system.h b/src/system.h index 84b369d..f527604 100644 --- a/src/system.h +++ b/src/system.h @@ -11,7 +11,8 @@ class System : public QObject System(); Q_INVOKABLE void shutdown(); Q_INVOKABLE void restart(); - Q_INVOKABLE bool setTime(QDate date); + Q_INVOKABLE bool setTime(QDateTime date); + Q_INVOKABLE QString getGitHash() const; }; -#endif // SYSTEM_H +#endif diff --git a/tr_de.qm b/tr_de.qm index 5b94a3b..add0145 100644 Binary files a/tr_de.qm and b/tr_de.qm differ diff --git a/tr_de.ts b/tr_de.ts index de2e316..7c72adc 100644 --- a/tr_de.ts +++ b/tr_de.ts @@ -12,17 +12,17 @@ CollageMenuForm.ui - + Print Drucken - + Next Photo Nächstes Foto - + Exit Zurück @@ -114,12 +114,12 @@ MainMenuForm.ui - + Continue Weiter - + Back Zurück @@ -135,14 +135,12 @@ PrinterBusyPopupForm.ui - OK - OK + OK - Printer is busy currently. Please stand by. - Drucker ist noch beschäftigt. Bitte warten. + Drucker ist noch beschäftigt. Bitte warten. @@ -161,8 +159,8 @@ SettingsPopup - - + + Time: Zeit: @@ -170,117 +168,143 @@ SettingsPopupForm.ui - + Close Schließen - + Settings Einstellungen - + Photos Fotos - + Camera Kamera - + Printer Drucker - + System System - + Copy photos to removable disk Kopiere Fotos auf USB Stick - + Delete all photos Alls Fotos löschen - + Mirror Camera Kamera spiegeln - + mirror spiegeln - + + Snapshot Settings + Schnappschusseinstellungen + + + + hide + verstecken + + + + Effect Popup + Effekte Popuo + + + + disable + deaktivieren + + + Enable Printing Druck aktivieren - + + enabled aktiviert - + Printer: Drucker: - + + Allow multiple prints + Erlaube mehrere Ausdrucke + + + Current time Eingestellte Zeit - + Set time Setze Zeit - + Language: Sprache: - + Window Mode: Fenster Modus: - + Window Fenster - + Fullscreen Vollbildschirm - + Copy layout templates from removable disk Kopiere Vorlage von USB Stick - + Shutdown Herunterfahren - + Restart Neustart - + Exit Photobooth Photobooth schließen @@ -288,12 +312,12 @@ SnapshotMenuForm.ui - + Please Try Again Bitte noch einmal probieren - + Abort Beenden @@ -328,7 +352,7 @@ Countdown - Coundown + Verzögerung