Commit 3411c717 authored by Leander Schulten's avatar Leander Schulten

New Feature: Added slideshow. The settings are saved per os-user. All settings...

New Feature: Added slideshow. The settings are saved per os-user. All settings can be modified in the settings tab.
symposion/symposion#46
parent 26c9516d
Pipeline #195965 passed with stage
in 4 minutes and 39 seconds
......@@ -51,6 +51,7 @@ SOURCES += \
modules/dmxconsumer.cpp \
modules/ledconsumer.cpp \
scanner.cpp \
slideshow.cpp \
system_error_handler.cpp \
test/testloopprogramm.cpp \
settings.cpp \
......@@ -120,6 +121,7 @@ HEADERS += \
modules/ledconsumer.h \
modules/scanner.hpp \
scanner.h \
slideshow.h \
system_error_handler.h \
updater.h \
usermanagment.h \
......
......@@ -3,6 +3,7 @@
#include "errornotifier.h"
#include "modelmanager.h"
#include "settings.h"
#include "slideshow.h"
#include "sortedmodelview.h"
#include "updater.h"
#include "usermanagment.h"
......@@ -357,7 +358,9 @@ int main(int argc, char *argv[]) {
QQmlEngine::setObjectOwnership(&Driver::dmxValueModel,QQmlEngine::CppOwnership);
engine.rootContext()->setContextProperty(QStringLiteral("dmxOutputValues"),&Driver::dmxValueModel);
engine.rootContext()->setContextProperty(QStringLiteral("AudioManager"), &Audio::AudioCaptureManager::get());
engine.rootContext()->setContextProperty(QStringLiteral("SlideShow"), &SlideShow::get());
engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml")));
engine.load(QUrl(QStringLiteral("qrc:/qml/SlideShowWindow.qml")));
// laden erst nach dem laden des qml ausführen
......
......@@ -84,5 +84,6 @@
<file>qml/ModifyThemePane.qml</file>
<file>qml/components/CenteredPopup.qml</file>
<file>qml/components/ModalPopupBackground.qml</file>
<file>qml/SlideShowWindow.qml</file>
</qresource>
</RCC>
......@@ -11,7 +11,7 @@ Pane{
GridLayout{
anchors.left: parent.left
anchors.right: parent.right
rowSpacing: 16
rowSpacing: 10
columns: 2
Label{
text: "Settings file path:"
......@@ -171,6 +171,67 @@ Pane{
text: "Modify Theme and appearance"
onClicked: modifyThemeWindow.show()
}
Label{
Layout.fillWidth: true
text: "SlideShow:"
}
RowLayout{
Button{
enabled: SlideShow.hasImages
text: SlideShow.windowVisibility !== Window.Hidden ? "Hide" : "Show"
onClicked: {
if (SlideShow.windowVisibility === Window.Hidden){
SlideShow.windowVisibility = Window.Maximized;
} else {
SlideShow.windowVisibility = Window.Minimized;
}
}
ToolTip.visible: hovered
ToolTip.text: text + "s the slideshow window"
}
TextInputField{
text: SlideShow.showTimeInSeconds
onEditingFinished: SlideShow.showTimeInSeconds = text;
Layout.minimumWidth: 20
Layout.rightMargin: 8
validator: IntValidator{
bottom: 1
}
MouseArea{
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
ToolTip.visible: containsMouse
ToolTip.text: "How long a image should be displayed (in seconds)"
}
}
ComboBox{
Layout.preferredWidth: implicitWidth + 20
model: ["Random", "Oldest first", "Newest first", "Name A-Z", "Name Z-A"]
currentIndex: SlideShow.showOrder
onCurrentIndexChanged: SlideShow.showOrder = currentIndex;
ToolTip.visible: hovered
ToolTip.text: "The order in which the images are displayed"
}
TextFieldFileChooser{
Layout.fillWidth: true
folder: true
path: SlideShow.path
onPathChanged: SlideShow.path = path;
fileChooser: fileDialog
MouseArea{
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
ToolTip.visible: containsMouse
ToolTip.text: "The path to the folder with the images"
}
}
}
}
SystemDialog.FileDialog{
property var callback;
......
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Window 2.12
import QtGraphicalEffects 1.0
Window {
color: "black"
visibility: SlideShow.windowVisibility
property int _currentState;
property int _oldState;
property bool showFirst: true;
onVisibilityChanged: {
_oldState = _currentState;
_currentState = visibility;
SlideShow.windowVisibility = visibility;
}
Component.onCompleted: {
SlideShow.onLoadNextImage.connect(path => {
if(path[0] === '/'){
path = path.substring(1);
}
if (showFirst) {
secondBackground.source = "file:///" + path;
secondImage.source = "file:///" + path;
} else {
firstBackground.source = "file:///" + path;
firstImage.source = "file:///" + path;
}
});
SlideShow.showNextImage.connect(() => {
showFirst = !showFirst;
if (showFirst){
secondAni.start();
} else {
firstAni.start();
}
});
screen = Qt.application.screens[Qt.application.screens.length - 1];
}
Shortcut{
sequence: StandardKey.Cancel
onActivated: if(visibility === Window.FullScreen) SlideShow.windowVisibility = Window.Maximized;
}
Item {
anchors.fill: parent
id: background
Image {
anchors.fill: parent
id: firstBackground
asynchronous: true
onStatusChanged: {
if(status === Image.Error){
SlideShow.reportInvalidImage();
}
}
}
Image {
anchors.fill: parent
id: secondBackground
opacity: !showFirst
asynchronous: true
onStatusChanged: {
if(status === Image.Error){
SlideShow.reportInvalidImage();
}
}
Behavior on opacity {
NumberAnimation{
duration: 1000
}
}
}
}
FastBlur{
anchors.fill: parent
radius: 128
source: background
}
RectangularGlow {
anchors.margins: 20
anchors.fill: firstImage
glowRadius: 60
spread: 0.2
color: "black"
opacity: firstImage.opacity
z: 1
}
Image {
height: Math.min(parent.height * (2/3),sourceSize.height)
anchors.centerIn: parent
id: firstImage
opacity: showFirst
fillMode: Image.PreserveAspectFit
z: 1
asynchronous: true
Behavior on opacity {
NumberAnimation{
duration: 500
}
}
NumberAnimation {
id: firstAni
targets: [firstImage, firstBackground]
property: "scale"
easing.type: Easing.InOutQuad
duration: 2000
from: 1
to : 1.5
onFinished: firstBackground.scale = firstImage.scale = 1
}
}
RectangularGlow {
anchors.margins: 20
anchors.fill: secondImage
glowRadius: 60
spread: 0.2
color: "black"
opacity: secondImage.opacity
z: showFirst ? 2 : .5
}
Image {
fillMode: Image.PreserveAspectFit
height: Math.min(parent.height * (2/3),sourceSize.height)
anchors.centerIn: parent
id: secondImage
opacity: 1 - firstImage.opacity
z: showFirst ? 2 : .5
asynchronous: true
NumberAnimation {
id: secondAni
targets: [secondImage, secondBackground]
easing.type: Easing.InOutQuad
property: "scale"
duration: 2000
from: 1
to : 1.5
onFinished: secondBackground.scale = secondImage.scale = 1
}
}
MouseArea{
anchors.fill: parent
hoverEnabled: true
opacity: containsMouse
Behavior on opacity { NumberAnimation{ easing.type: Easing.OutCubic; } }
RoundButton{
text: SlideShow.windowVisibility === Window.FullScreen ? "Exit Fullscreen" : "Enter Fullscreen"
onClicked: {
if (SlideShow.windowVisibility === Window.FullScreen){
SlideShow.windowVisibility = _oldState;
} else {
SlideShow.windowVisibility = Window.FullScreen;
}
}
}
}
Text{
anchors.centerIn: parent
font.pointSize: 30
text: "No images"
color: "white"
visible: !SlideShow.hasImages
}
}
#include "settings.h"
Settings::Settings(QObject *parent) : QObject(parent), settings(QStringLiteral("Turmstraße 1 e.V."),QStringLiteral("Lichtsteuerung"))
{
Settings::Settings(QObject *parent) : QObject(parent), settings(OrganisationName, ApplicationName) {
if(localSettingsFile.exists()){
localSettings.emplace(localSettingsFile.filePath(),QSettings::IniFormat);
localSettings->setIniCodec("UTF-8");
......
......@@ -59,7 +59,10 @@ class Settings : public QObject {
Q_PROPERTY(int theme READ getTheme WRITE setTheme NOTIFY themeChanged)
Q_PROPERTY(unsigned int updatePauseInMs READ getUpdatePauseInMs WRITE setUpdatePauseInMs NOTIFY updatePauseInMsChanged)
static inline QFileInfo localSettingsFile;
public:
static constexpr auto OrganisationName = "Turmstraße 1 e.V.";
static constexpr auto ApplicationName = "Lichtsteuerung";
/**
* @brief setLocalSettingFile sets a localSetting file that is loaded and preferred everytime a Settings object is created
* @param settingFile the filePath to the local setting file in utf8 ini format
......
#include "slideshow.h"
#include "settings.h"
#include "usermanagment.h"
#include <QDateTime>
#include <QDebug>
#include <QDirIterator>
#include <QImageReader>
#include <QRegularExpression>
#include <QSettings>
#include <algorithm>
#include <cmath>
#include <errornotifier.h>
#include <thread>
SlideShow::SlideShow() : randomNumberGenerator(static_cast<unsigned>(rand())) {
QSettings s(QSettings::UserScope, Settings::OrganisationName, Settings::ApplicationName);
showTimeInSeconds = std::max(3, s.value(QStringLiteral("showTimeInSeconds"), 3).toInt());
order = static_cast<ShowOrder>(std::clamp<int>(s.value(QStringLiteral("showOrder"), 0).toInt(), Random, Last));
path = s.value(QStringLiteral("path"), "").toString();
scanForNewFiles();
sort();
emit hasImagesChanged();
loadTimer = startTimer(showTimeInSeconds * 1000);
// if a new path gets added, add the new files to the images deque and sort
QObject::connect(&watcher, &QFileSystemWatcher::directoryChanged, [this](const auto &path) {
scanDirectory(path, false);
sort();
});
// if the window gets visible and no image is loaded, load a image and show it
QObject::connect(this, &SlideShow::windowVisibilityChanged, [this]() {
if (windowVisibility != QWindow::Hidden && currentImage < 0) {
loadNextImage();
emit showNextImage();
}
});
}
void SlideShow::setShowTimeInSeconds(int time) {
if (time != showTimeInSeconds) {
showTimeInSeconds = std::max(3, time);
killTimer(loadTimer);
loadTimer = startTimer(showTimeInSeconds * 1000);
emit showTimeInSecondsChanged();
if (UserManagment::get()->getCurrentUser()->havePermission(UserManagment::Permission::SAVE_SLIDE_SHOW_SETTINGS)) {
QSettings(QSettings::UserScope, Settings::OrganisationName, Settings::ApplicationName).setValue(QStringLiteral("showTimeInSeconds"), showTimeInSeconds);
}
}
}
void SlideShow::setShowOrder(SlideShow::ShowOrder showOrder) {
if (showOrder != order) {
order = showOrder;
emit showOrderChanged();
sort();
if (UserManagment::get()->getCurrentUser()->havePermission(UserManagment::Permission::SAVE_SLIDE_SHOW_SETTINGS)) {
QSettings(QSettings::UserScope, Settings::OrganisationName, Settings::ApplicationName).setValue(QStringLiteral("showOrder"), showOrder);
}
}
}
void SlideShow::setPath(const QString &path) {
if (path != this->path && QFileInfo(path).isDir()) {
this->path = path;
emit pathChanged();
images.clear();
alreadyScanned.clear();
scanForNewFiles();
sort();
emit hasImagesChanged();
if (UserManagment::get()->getCurrentUser()->havePermission(UserManagment::Permission::SAVE_SLIDE_SHOW_SETTINGS)) {
QSettings(QSettings::UserScope, Settings::OrganisationName, Settings::ApplicationName).setValue(QStringLiteral("path"), path);
}
}
}
void SlideShow::reportInvalidImage() {
removeImage(currentImage);
--currentImage; // so that ++currentImage returns the next image
loadNextImage();
killTimer(offsetTimer);
offsetTimer = startTimer(offsetWaitTime);
}
void SlideShow::loadNextImage() {
// if it is locked, than by scanForNewFiles
if (imageMutex.try_lock()) {
if (order == Random) {
while (!images.empty()) {
auto i = dis(randomNumberGenerator);
auto p = images[i];
p.refresh();
if (p.exists()) {
checkCustomShowTime(p.filePath());
emit loadNextImage(p.filePath());
break;
}
removeImage(static_cast<int>(i));
}
} else {
// get index of next image
++currentImage;
while (!images.empty()) {
// the size of the deque can change
currentImage %= images.size();
auto &img = images[static_cast<size_t>(currentImage)];
img.refresh();
if (img.exists()) {
checkCustomShowTime(img.filePath());
emit loadNextImage(img.filePath());
break;
}
// if we remove an image, current index will point to the "next" image
alreadyScanned.erase(img.filePath());
images.erase(images.cbegin() + currentImage);
}
}
imageMutex.unlock();
if (images.empty()) {
currentImage = -1;
emit hasImagesChanged();
}
}
}
// regex to search for things like 10s in the file name for custom show times
const QRegularExpression regex(QStringLiteral("(\\d+)s"));
void SlideShow::checkCustomShowTime(const QString &path) {
auto i = regex.globalMatch(path);
int customTime = -1;
while (i.hasNext()) {
auto match = i.next();
customTime = match.captured(1).toInt();
}
if (customTime > 2) {
wasCustomShowTime = true;
killTimer(loadTimer);
loadTimer = startTimer(customTime * 1000);
} else if (wasCustomShowTime) {
wasCustomShowTime = false;
killTimer(loadTimer);
loadTimer = startTimer(showTimeInSeconds * 1000);
}
}
void SlideShow::removeImage(int index) {
if (index >= 0 && index < images.size() && imageMutex.try_lock()) {
alreadyScanned.erase(images[index].filePath());
images.erase(images.cbegin() + index);
dis = std::uniform_int_distribution<size_t>(0, images.size() - 1);
imageMutex.unlock();
}
}
void SlideShow::scanDirectory(const QString &path, bool recursive) {
std::unique_lock lock(imageMutex);
QDirIterator it(path, QStringList() << QStringLiteral("*.jpg") << QStringLiteral("*.jpeg") << QStringLiteral("*.png") << QStringLiteral("*.gif") << QStringLiteral("*.bmp") << QStringLiteral("*.webp"), QDir::Files | QDir::Readable | QDir::AllDirs | QDir::NoDotAndDotDot,
recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
while (it.hasNext()) {
auto p = it.next();
auto [_, newInserted] = alreadyScanned.insert(p);
if (newInserted) {
QFileInfo f(p);
if (f.isDir()) {
// scan new dirs
if (!recursive) {
scanDirectory(p, true);
}
watcher.addPath(p);
}
// invalid files from macOS ../._abc.png
else if (!p.contains(QLatin1String("._"))) {
images.emplace_back(std::move(p));
}
}
}
dis = std::uniform_int_distribution<size_t>(0, images.size() - 1);
}
void SlideShow::scanForNewFiles() {
watcher.removePaths(watcher.directories());
if (path.isEmpty()) {
return;
}
QFileInfo dir(path);
if (dir.isFile()) {
std::unique_lock lock(imageMutex);
images.emplace_back(path);
dis = std::uniform_int_distribution<size_t>(0);
} else {
std::thread t([this]() {
scanDirectory(path, true);
emit hasImagesChanged();
if (images.empty()) {
ErrorNotifier::showError(QStringLiteral("The selected folder for the slideshow contains no images!"));
}
watcher.addPath(path);
if (shouldSort) {
shouldSort = false;
sort();
}
});
t.detach();
}
}
void SlideShow::sort() {
struct newest {
bool operator()(const QFileInfo &l, const QFileInfo &r) const { return l.lastModified() > r.lastModified(); }
};
struct oldest {
bool operator()(const QFileInfo &l, const QFileInfo &r) const { return l.lastModified() < r.lastModified(); }
};
struct nameAZ {
bool operator()(const QFileInfo &l, const QFileInfo &r) const { return l.filePath() < r.filePath(); }
};
struct nameZA {
bool operator()(const QFileInfo &l, const QFileInfo &r) const { return l.filePath() > r.filePath(); }
};
if (order == Random) {
return;
}
// if it is locked, than by scanForNewFiles
if (imageMutex.try_lock()) {
switch (order) {
case Random: /*No warning*/ break;
case NewestFirst: std::sort(images.begin(), images.end(), newest{}); break;
case OldestFirst: std::sort(images.begin(), images.end(), oldest{}); break;
case NameAZ: std::sort(images.begin(), images.end(), nameAZ{}); break;
case NameZA: std::sort(images.begin(), images.end(), nameZA{}); break;
}
imageMutex.unlock();
} else {
shouldSort = true;
}
}
void SlideShow::timerEvent(QTimerEvent *event) {
if (event->timerId() == loadTimer) {
event->accept();
offsetTimer = startTimer(offsetWaitTime);
loadNextImage();
} else if (event->timerId() == offsetTimer) {
event->accept();
killTimer(offsetTimer);
offsetTimer = -1;
emit showNextImage();
}
}
#ifndef SLIDESHOW_H
#define SLIDESHOW_H
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QObject>
#include <QWindow>
#include <atomic>
#include <deque>
#include <mutex>
#include <random>
#include <set>
class SlideShow : public QObject {
Q_OBJECT
public:
enum ShowOrder { Random, OldestFirst, NewestFirst, NameAZ, NameZA, Last = NameZA };
Q_ENUM(ShowOrder)
private:
/**
* @brief path the path to the folder, in which the images are
*/
QString path;
/**
* @brief showTimeInSeconds how long a image is visible
*/
int showTimeInSeconds;
/**
* @brief order the oder of the images
*/
ShowOrder order;
/**
* if the image deque is locked by the thread that loads the images asnyc, you can
* set this bool to true. If this bool is true, the async loading thread will sort
* the image deque when the loading is finished
*/
std::atomic_bool shouldSort;
// the mutex to protect the image deque
std::recursive_mutex imageMutex;
// all loaded images, sorted if order != Random
std::deque<QFileInfo> images;
// all paths that were already scanned
std::set<QString> alreadyScanned;
// a watcher to check if new files/folders gets added into the folder <path>
QFileSystemWatcher watcher;
// the current displayed image (index in the images deque)
int currentImage = -1;
// the id of the load timer
int loadTimer = -1;
/**
* @brief offsetTimer the id of the offset timer. The offset timer should wait offsetWaitTime after the loadNextImage
* is emitted to emit emit the showNextImage signal. So the Slideshow has some time to load the image
*/
int offsetTimer = -1;
// if a file contains [0-9]+s, the wait time will be this custom time
bool wasCustomShowTime = false;
/**
* @brief windowVisibility the window visibility of the slideshow window
*/
QWindow::Visibility windowVisibility = QWindow::Hidden;
// the used random number generator for <order> = Random
std::mt19937 randomNumberGenerator;
// the distribution to get a random number from the random number generator
std::uniform_int_distribution<size_t> dis;
Q_PROPERTY(int showTimeInSeconds READ getShowTimeInSeconds WRITE setShowTimeInSeconds NOTIFY showTimeInSecondsChanged)
Q_PROPERTY(ShowOrder showOrder READ getShowOrder WRITE setShowOrder NOTIFY showOrderChanged)
Q_PROPERTY(QString path READ getPath WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(QWindow::Visibility windowVisibility MEMBER windowVisibility NOTIFY windowVisibilityChanged)
Q_PROPERTY(bool hasImages READ hasImages NOTIFY hasImagesChanged)
// the time that should be waitet by the offset timer
static constexpr int offsetWaitTime = 100;
public:
static SlideShow &get() {
static SlideShow s;
return s;
}
public:
SlideShow();
void setShowTimeInSeconds(int time);
[[nodiscard]] int getShowTimeInSeconds() const { return showTimeInSeconds; }
void setShowOrder(ShowOrder showOrder);
[[nodiscard]] ShowOrder getShowOrder() const { return order; }
void setPath(const QString &path);
[[nodiscard]] QString getPath() const { return path; }
[[nodiscard]] bool hasImages() const { return images.size(); }
/**
* @brief reportInvalidImage you can report the current image as invalid.
* The image gets removed from a images deque and a new image gets selected
*/
Q_INVOKABLE void reportInvalidImage();
protected:
/**
* @brief scanForNewFiles scans in the path for new files and updates the <watcher>. The scanning is done in a seperate thread.
*/
void scanForNewFiles();
/**
* @brief sort Sorts the images deque. If the imageMutex can not be locked, shouldSort is set to true.
*/
void sort();
void timerEvent(QTimerEvent *event) override;
/**
* @brief loadNextImage determins the next image and emit the signal loadNextImage, if the imageMutex can be locked
*/
void loadNextImage();
private:
/**
* @brief checkCustomShowTime checks if the path contains a custom show time ([0-9]+s) and manages the load timers
* @param path The path that should be checked
*/
void checkCustomShowTime(const QString &path);
/**
* @brief removeImage removes a image from the images deque and from the alreadyScanned set and updates the distribution
* @param index index in the deque, can be invalid
*/
void removeImage(int index);
/**
* @brief scanDirectory scans the directory path for new files. If a new directory is found, scan it recursive, even if recursive is false
* @param path the path of the directory that should be scanned
* @param recursive false, if only the directory should be scanned. If true, subdirectories are also scanned.
*/