From 29cdc499e6d3ca5ec3e9f4f4c2d4751a89a6de36 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Thu, 20 Jul 2023 02:13:48 -0500 Subject: [PATCH] Add settings dialog, camera controls --- .gitignore | 3 + .idea/.gitignore | 8 -- .idea/misc.xml | 16 +-- .idea/vcs.xml | 2 +- .liccor.yml | 2 +- Makefile | 2 +- obs_scene_switcher.py | 2 +- src/CMakeLists.txt | 3 + src/cameraclient.cpp | 54 ++++++++++ src/cameraclient.hpp | 46 +++++++++ src/consts.hpp | 1 - src/main.cpp | 2 + src/mainwindow.cpp | 161 ++++++++++++++++++++++++----- src/mainwindow.hpp | 23 ++++- src/obsclient.cpp | 18 +++- src/obsclient.hpp | 10 +- src/openlpclient.cpp | 26 ++++- src/openlpclient.hpp | 12 +-- src/settingsdata.cpp | 142 ++++++++++++++++++++++++++ src/settingsdata.hpp | 59 +++++++++++ src/settingsdialog.cpp | 224 +++++++++++++++++++++++++++++++++++++++++ src/settingsdialog.hpp | 40 ++++++++ src/slideview.cpp | 13 ++- src/slideview.hpp | 6 +- 24 files changed, 793 insertions(+), 82 deletions(-) delete mode 100644 .idea/.gitignore create mode 100644 src/cameraclient.cpp create mode 100644 src/cameraclient.hpp create mode 100644 src/settingsdata.cpp create mode 100644 src/settingsdata.hpp create mode 100644 src/settingsdialog.cpp create mode 100644 src/settingsdialog.hpp diff --git a/.gitignore b/.gitignore index 5473a27..9369574 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ +.cache .clangd .conanbuild .current_build +__pycache__ CMakeLists.txt.user Session.vim build compile_commands.json +cmake-build-debug dist graph_info.json tags diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 73f69e0..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/misc.xml b/.idea/misc.xml index 8067168..7f5d14a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,17 +1,7 @@ - - + + + - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.liccor.yml b/.liccor.yml index cca497c..9efd06c 100644 --- a/.liccor.yml +++ b/.liccor.yml @@ -2,7 +2,7 @@ source: - . copyright_notice: |- - Copyright 2021 gary@drinkingtea.net + Copyright 2021 - 2023 gary@drinkingtea.net This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/Makefile b/Makefile index 6d7cf18..b707ab4 100644 --- a/Makefile +++ b/Makefile @@ -14,4 +14,4 @@ run: install ${ENV_RUN} ${PROJECT_EXECUTABLE} .PHONY: debug debug: install - ${ENV_RUN} gdb --args ${PROJECT_EXECUTABLE} + ${DEBUGGER} lldb -- ${PROJECT_EXECUTABLE} diff --git a/obs_scene_switcher.py b/obs_scene_switcher.py index 8830506..058a8f2 100644 --- a/obs_scene_switcher.py +++ b/obs_scene_switcher.py @@ -33,7 +33,7 @@ class RqstHandler(BaseHTTPRequestHandler): def run(name): - httpd = HTTPServer(('127.0.0.1', 9302), RqstHandler) + httpd = HTTPServer(('0.0.0.0', 9302), RqstHandler) httpd.serve_forever() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c729b7c..b85b551 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,10 +8,13 @@ find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network Widgets REQUIRED) add_executable( SlideController MACOSX_BUNDLE WIN32 + cameraclient.cpp main.cpp mainwindow.cpp obsclient.cpp openlpclient.cpp + settingsdata.cpp + settingsdialog.cpp slideview.cpp ) diff --git a/src/cameraclient.cpp b/src/cameraclient.cpp new file mode 100644 index 0000000..f820060 --- /dev/null +++ b/src/cameraclient.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2021 - 2023 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include + +#include "settingsdata.hpp" +#include "cameraclient.hpp" + +CameraClient::CameraClient(QObject *parent): QObject(parent) { + setBaseUrl(); + m_pollTimer.start(1000); + connect(&m_pollTimer, &QTimer::timeout, this, &CameraClient::poll); + connect(m_pollingNam, &QNetworkAccessManager::finished, this, &CameraClient::handlePollResponse); +} + +void CameraClient::setPreset(int preset) { + if (preset > -1) { + get(QString("/cgi-bin/ptzctrl.cgi?ptzcmd&poscall&%1").arg(preset)); + } +} + +void CameraClient::setBaseUrl() { + const auto [host, port] = getCameraConnectionData(); + m_baseUrl = QString("http://%1:%2").arg(host, QString::number(port)); +} + +void CameraClient::get(QString const&urlExt) { + QUrl url(QString(m_baseUrl) + urlExt); + QNetworkRequest rqst(url); + auto reply = m_nam->get(rqst); + connect(reply, &QIODevice::readyRead, reply, &QObject::deleteLater); +} + +void CameraClient::poll() { + QUrl url(QString(m_baseUrl) + "/cgi-bin/param.cgi?get_device_conf"); + QNetworkRequest rqst(url); + m_pollingNam->get(rqst); +} + +void CameraClient::handlePollResponse(QNetworkReply *reply) { + reply->deleteLater(); + if (reply->error()) { + qDebug() << "CameraClient error response:" << reply->errorString(); + emit pollFailed(); + return; + } + emit pollUpdate(); +} diff --git a/src/cameraclient.hpp b/src/cameraclient.hpp new file mode 100644 index 0000000..9f90d09 --- /dev/null +++ b/src/cameraclient.hpp @@ -0,0 +1,46 @@ +/* + * Copyright 2021 - 2023 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include +#include + +#include "consts.hpp" + +class CameraClient: public QObject { + Q_OBJECT + private: + QString m_baseUrl; + QNetworkAccessManager *const m_nam = new QNetworkAccessManager(this); + QNetworkAccessManager *const m_pollingNam = new QNetworkAccessManager(this); + QTimer m_pollTimer; + + public: + explicit CameraClient(QObject *parent = nullptr); + + void setPreset(int preset); + + public slots: + void setBaseUrl(); + + private: + void get(QString const&url); + + void poll(); + + void handlePollResponse(QNetworkReply *reply); + + signals: + void pollUpdate(); + + void pollFailed(); + +}; + diff --git a/src/consts.hpp b/src/consts.hpp index 1a52fb6..5f360eb 100644 --- a/src/consts.hpp +++ b/src/consts.hpp @@ -8,4 +8,3 @@ #pragma once -constexpr auto SlideHost = "192.168.254.61"; diff --git a/src/main.cpp b/src/main.cpp index 7d49280..59e7250 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,10 +7,12 @@ */ #include +#include #include "mainwindow.hpp" int main(int argc, char *argv[]) { + QSettings::setDefaultFormat(QSettings::Format::IniFormat); QApplication a(argc, argv); QApplication::setApplicationName(QObject::tr("Slide Controller 9000")); MainWindow w; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6ab2236..4f4c4c8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -6,17 +6,21 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include #include +#include #include #include +#include "settingsdialog.hpp" #include "slideview.hpp" #include "mainwindow.hpp" MainWindow::MainWindow(QWidget *parent): QMainWindow(parent) { move(0, 0); - setFixedSize(590, 555); + setFixedSize(610, 555); setWindowTitle(tr("Slide Controller 9000")); + setupMenu(); const auto mainWidget = new QWidget(this); const auto rootLyt = new QVBoxLayout; const auto controlsLayout = new QGridLayout; @@ -26,53 +30,159 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent) { rootLyt->addWidget(m_slideView); rootLyt->addLayout(controlsLayout); // setup slide controls - const auto showHideLyt = new QHBoxLayout; - rootLyt->addLayout(showHideLyt); const auto btnPrevSong = new QPushButton(tr("Previous Song (Left)"), this); const auto btnPrevSlide = new QPushButton(tr("Previous Slide (Up)"), this); const auto btnNextSlide = new QPushButton(tr("Next Slide (Down)"), this); const auto btnNextSong = new QPushButton(tr("Next Song (Right)"), this); - const auto btnHideSlides = new QPushButton(tr("Hide (1)"), this); - const auto btnOpenLpShowSlides = new QPushButton(tr("Show in OpenLP Only (2)"), this); - const auto btnShowSlides = new QPushButton(tr("Show (3)"), mainWidget); controlsLayout->addWidget(btnPrevSlide, 0, 1); controlsLayout->addWidget(btnNextSlide, 0, 2); controlsLayout->addWidget(btnPrevSong, 0, 0); controlsLayout->addWidget(btnNextSong, 0, 3); - showHideLyt->addWidget(btnHideSlides); - showHideLyt->addWidget(btnOpenLpShowSlides); - showHideLyt->addWidget(btnShowSlides); + controlsLayout->setSpacing(2); btnNextSong->setShortcut(Qt::Key_Right); btnPrevSong->setShortcut(Qt::Key_Left); btnNextSlide->setShortcut(Qt::Key_Down); btnPrevSlide->setShortcut(Qt::Key_Up); - btnHideSlides->setShortcut(Qt::Key_1); - btnOpenLpShowSlides->setShortcut(Qt::Key_2); - btnHideSlides->setToolTip(tr("Also hides slides in OBS")); - btnShowSlides->setShortcut(Qt::Key_3); + btnNextSong->setFixedWidth(135); + btnPrevSong->setFixedWidth(135); + btnNextSlide->setFixedWidth(135); + btnPrevSlide->setFixedWidth(135); + setupViewControls(rootLyt); connect(btnNextSlide, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::nextSlide); connect(btnPrevSlide, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::prevSlide); connect(btnNextSong, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::nextSong); connect(btnPrevSong, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::prevSong); - connect(btnHideSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::blankScreen); - connect(btnHideSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::hideSlides); - connect(btnOpenLpShowSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::showSlides); - connect(btnShowSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::showSlides); - connect(btnShowSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::showSlides); connect(&m_openlpClient, &OpenLPClient::pollUpdate, m_slideView, &SlideView::pollUpdate); connect(&m_openlpClient, &OpenLPClient::songListUpdate, m_slideView, &SlideView::songListUpdate); connect(&m_openlpClient, &OpenLPClient::slideListUpdate, m_slideView, &SlideView::slideListUpdate); connect(&m_openlpClient, &OpenLPClient::pollFailed, m_slideView, &SlideView::reset); connect(m_slideView, &SlideView::songChanged, &m_openlpClient, &OpenLPClient::changeSong); connect(m_slideView, &SlideView::slideChanged, &m_openlpClient, &OpenLPClient::changeSlide); - // setup scene selector - connect(btnOpenLpShowSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::hideSlides); // setup status bar setStatusBar(new QStatusBar(this)); + connect(&m_cameraClient, &CameraClient::pollUpdate, this, &MainWindow::cameraConnectionInit); connect(&m_openlpClient, &OpenLPClient::songChanged, this, &MainWindow::refreshStatusBar); connect(&m_openlpClient, &OpenLPClient::pollUpdate, this, &MainWindow::openLpConnectionInit); connect(&m_obsClient, &OBSClient::pollUpdate, this, &MainWindow::obsConnectionInit); refreshStatusBar(); + connect(statusBar(), &QStatusBar::messageChanged, this, [this](QStringView const&msg) { + if (msg.empty()) { + refreshStatusBar(); + } + }); +} + +void MainWindow::setupMenu() { + // file menu + auto const fileMenu = menuBar()->addMenu(tr("&File")); + auto const settingsAct = new QAction(tr("&Settings"), this); + auto const quitAct = new QAction(tr("E&xit"), this); + settingsAct->setShortcuts(QKeySequence::Preferences); + connect(settingsAct, &QAction::triggered, this, &MainWindow::openSettings); + quitAct->setShortcuts(QKeySequence::Quit); + quitAct->setStatusTip(tr("Exit application")); + connect(quitAct, &QAction::triggered, &QApplication::quit); + fileMenu->addAction(settingsAct); + fileMenu->addAction(quitAct); +} + +void MainWindow::setupDefaultViewControls(QHBoxLayout *viewCtlLyt) { + auto const mainWidget = viewCtlLyt->parentWidget(); + auto const btnHideSlides = new QPushButton(tr("1. Hide"), mainWidget); + auto const btnOpenLpShowSlides = new QPushButton(tr("2. Show in OpenLP Only"), mainWidget); + auto const btnShowSlides = new QPushButton(tr("3. Show"), mainWidget); + viewCtlLyt->addWidget(btnHideSlides); + viewCtlLyt->addWidget(btnOpenLpShowSlides); + viewCtlLyt->addWidget(btnShowSlides); + btnHideSlides->setShortcut(Qt::Key_1); + btnOpenLpShowSlides->setShortcut(Qt::Key_2); + btnHideSlides->setToolTip(tr("Also hides slides in OBS")); + btnShowSlides->setShortcut(Qt::Key_3); + connect(btnHideSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::blankScreen); + connect(btnHideSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::hideSlides); + connect(btnOpenLpShowSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::showSlides); + connect(btnOpenLpShowSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::hideSlides); + connect(btnShowSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::showSlides); + connect(btnShowSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::showSlides); +} + +void MainWindow::setupCustomViewControls(QVector const&views, QGridLayout *viewCtlLyt) { + constexpr auto columns = 3; + auto const parent = viewCtlLyt->parentWidget(); + for (auto i = 0; auto const&view : views) { + auto const x = i % columns; + auto const y = i / columns; + auto const name = QString("%1. %2").arg(i + 1).arg(view.name); + auto const btn = new QPushButton(name, parent); + btn->setShortcut(Qt::Key_1 + i); + viewCtlLyt->addWidget(btn, y, x); + auto const slides = view.slides; + auto const obsSlides = view.obsSlides; + auto const cameraPreset = view.cameraPreset; + connect(btn, &QPushButton::clicked, this, [this, slides, obsSlides, cameraPreset] { + m_cameraClient.setPreset(cameraPreset); + m_openlpClient.setSlidesVisible(slides); + m_obsClient.setSlidesVisible(obsSlides); + }); + ++i; + } +} + +void MainWindow::setupViewControls(QVBoxLayout *rootLyt) { + auto views = getViews(); + if (!m_viewControlsParent) { + m_viewControlsParent = new QWidget(rootLyt->parentWidget()); + m_viewControlsParentLyt = new QHBoxLayout(m_viewControlsParent); + m_viewControlsParentLyt->setContentsMargins(0, 0, 0, 0); + rootLyt->addWidget(m_viewControlsParent); + } + delete m_viewControls; + m_viewControls = new QWidget(m_viewControlsParent); + m_viewControlsParentLyt->addWidget(m_viewControls); + auto const viewCtlLyt = new QGridLayout(m_viewControls); + viewCtlLyt->setSpacing(5); + if (views.empty()) { + views.emplace_back(View{ + .name = tr("Hide"), + .slides = false, + .obsSlides = false, + }); + views.emplace_back(View{ + .name = tr("Hide in OBS"), + .slides = false, + .obsSlides = false, + }); + views.emplace_back(View{ + .name = tr("Show"), + .slides = false, + .obsSlides = false, + }); + } + setupCustomViewControls(views, viewCtlLyt); +} + +void MainWindow::openSettings() { + SettingsDialog d(this); + auto const result = d.exec(); + if (result == QDialog::Accepted) { + m_cameraClient.setBaseUrl(); + m_obsClient.setBaseUrl(); + m_openlpClient.setBaseUrl(); + } +} + +void MainWindow::cameraConnectionInit() { + disconnect(&m_cameraClient, &CameraClient::pollUpdate, this, &MainWindow::cameraConnectionInit); + connect(&m_cameraClient, &CameraClient::pollFailed, this, &MainWindow::cameraConnectionLost); + m_cameraConnected = true; + refreshStatusBar(); +} + +void MainWindow::cameraConnectionLost() { + disconnect(&m_cameraClient, &CameraClient::pollFailed, this, &MainWindow::cameraConnectionLost); + connect(&m_cameraClient, &CameraClient::pollUpdate, this, &MainWindow::cameraConnectionInit); + m_cameraConnected = false; + refreshStatusBar(); } void MainWindow::openLpConnectionInit() { @@ -104,9 +214,10 @@ void MainWindow::obsConnectionLost() { } void MainWindow::refreshStatusBar() { - const auto openLpStatus = m_openLpConnected ? tr("OpenLP: Connected") : tr("OpenLP: Not Connected"); - const auto obsStatus = m_obsConnected ? tr("OBS: Connected") : tr("OBS: Not Connected"); - const auto nextSong = m_openlpClient.getNextSong(); - const auto nextSongTxt = m_openLpConnected ? " | Next Song: " + nextSong : ""; - statusBar()->showMessage(openLpStatus + " | " + obsStatus + nextSongTxt); + auto const cameraStatus = m_cameraConnected ? tr("Camera: Connected") : tr("Camera: Not Connected"); + auto const openLpStatus = m_openLpConnected ? tr("OpenLP: Connected") : tr("OpenLP: Not Connected"); + auto const obsStatus = m_obsConnected ? tr("OBS: Connected") : tr("OBS: Not Connected"); + auto const nextSong = m_openlpClient.getNextSong(); + auto const nextSongTxt = m_openLpConnected ? " | Next Song: " + nextSong : ""; + statusBar()->showMessage(cameraStatus + " | " + openLpStatus + " | " + obsStatus + nextSongTxt); } diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp index e963ccc..48e5b80 100644 --- a/src/mainwindow.hpp +++ b/src/mainwindow.hpp @@ -12,24 +12,45 @@ #include +#include "cameraclient.hpp" #include "obsclient.hpp" #include "openlpclient.hpp" +#include "settingsdata.hpp" class MainWindow: public QMainWindow { Q_OBJECT private: + CameraClient m_cameraClient; OBSClient m_obsClient; OpenLPClient m_openlpClient; class SlideView *m_slideView = nullptr; + bool m_cameraConnected = false; bool m_openLpConnected = false; bool m_obsConnected = false; + class QHBoxLayout *m_viewControlsParentLyt = nullptr; + class QWidget *m_viewControlsParent = nullptr; + class QWidget *m_viewControls = nullptr; public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override = default; - private slots: + private: + void setupMenu(); + + void setupDefaultViewControls(class QHBoxLayout *rootLyt); + + void setupCustomViewControls(QVector const&views, class QGridLayout *rootLyt); + + void setupViewControls(class QVBoxLayout *rootLyt); + + void openSettings(); + + void cameraConnectionInit(); + + void cameraConnectionLost(); + void openLpConnectionInit(); void openLpConnectionLost(); diff --git a/src/obsclient.cpp b/src/obsclient.cpp index 28ff756..f54f76c 100644 --- a/src/obsclient.cpp +++ b/src/obsclient.cpp @@ -8,17 +8,20 @@ #include #include +#include #include +#include "settingsdata.hpp" #include "obsclient.hpp" OBSClient::OBSClient(QObject *parent): QObject(parent) { + setBaseUrl(); m_pollTimer.start(1000); connect(&m_pollTimer, &QTimer::timeout, this, &OBSClient::poll); connect(m_pollingNam, &QNetworkAccessManager::finished, this, &OBSClient::handlePollResponse); } -void OBSClient::setScene(QString scene) { +void OBSClient::setScene(QString const&scene) { get(QString("/Scene?name=%1").arg(scene)); } @@ -30,7 +33,7 @@ void OBSClient::hideSlides() { setScene("SpeakerScene"); } -void OBSClient::setSlidesVisible(int state) { +void OBSClient::setSlidesVisible(bool state) { if (state) { setScene("MusicScene"); } else { @@ -38,15 +41,20 @@ void OBSClient::setSlidesVisible(int state) { } } -void OBSClient::get(QString urlExt) { - QUrl url(QString(BaseUrl) + urlExt); +void OBSClient::setBaseUrl() { + const auto [host, port] = getOBSConnectionData(); + m_baseUrl = QString("http://%1:%2").arg(host, QString::number(port)); +} + +void OBSClient::get(QString const&urlExt) { + QUrl url(QString(m_baseUrl) + urlExt); QNetworkRequest rqst(url); auto reply = m_nam->get(rqst); connect(reply, &QIODevice::readyRead, reply, &QObject::deleteLater); } void OBSClient::poll() { - QUrl url(QString(BaseUrl) + "/ping"); + QUrl url(QString(m_baseUrl) + "/ping"); QNetworkRequest rqst(url); m_pollingNam->get(rqst); } diff --git a/src/obsclient.hpp b/src/obsclient.hpp index 2e09860..d8c9329 100644 --- a/src/obsclient.hpp +++ b/src/obsclient.hpp @@ -17,7 +17,7 @@ class OBSClient: public QObject { Q_OBJECT private: - const QString BaseUrl = QString("http://") + SlideHost + ":9302"; + QString m_baseUrl; QNetworkAccessManager *m_nam = new QNetworkAccessManager(this); QNetworkAccessManager *m_pollingNam = new QNetworkAccessManager(this); QTimer m_pollTimer; @@ -26,16 +26,18 @@ class OBSClient: public QObject { explicit OBSClient(QObject *parent = nullptr); public slots: - void setScene(QString scene); + void setScene(QString const&scene); void showSlides(); void hideSlides(); - void setSlidesVisible(int state); + void setSlidesVisible(bool state); + + void setBaseUrl(); private: - void get(QString url); + void get(QString const&url); void poll(); diff --git a/src/openlpclient.cpp b/src/openlpclient.cpp index e3f7da2..93600c6 100644 --- a/src/openlpclient.cpp +++ b/src/openlpclient.cpp @@ -11,10 +11,13 @@ #include #include #include +#include +#include "settingsdata.hpp" #include "openlpclient.hpp" OpenLPClient::OpenLPClient(QObject *parent): QObject(parent) { + setBaseUrl(); poll(); m_pollTimer.start(250); connect(&m_pollTimer, &QTimer::timeout, this, &OpenLPClient::poll); @@ -57,6 +60,14 @@ void OpenLPClient::showSlides() { get("/api/display/show"); } +void OpenLPClient::setSlidesVisible(bool value) { + if (value) { + showSlides(); + } else { + blankScreen(); + } +} + void OpenLPClient::changeSong(int it) { auto n = QString::number(it); auto url = "/api/service/set?data=%7B%22request%22%3A+%7B%22id%22%3A+" + n + "%7D%7D&_=1627181837297"; @@ -69,26 +80,31 @@ void OpenLPClient::changeSlide(int slide) { get(url); } -void OpenLPClient::get(QString urlExt) { - QUrl url(QString(BaseUrl) + urlExt); +void OpenLPClient::setBaseUrl() { + const auto [host, port] = getOpenLPConnectionData(); + m_baseUrl = QString("http://%1:%2").arg(host, QString::number(port)); +} + +void OpenLPClient::get(QString const&urlExt) { + QUrl url(m_baseUrl + urlExt); QNetworkRequest rqst(url); m_nam->get(rqst); } void OpenLPClient::requestSongList() { - QUrl url(QString(BaseUrl) + "/api/service/list?_=1626628079579"); + QUrl url(m_baseUrl + "/api/service/list?_=1626628079579"); QNetworkRequest rqst(url); m_songListNam->get(rqst); } void OpenLPClient::requestSlideList() { - QUrl url(QString(BaseUrl) + "/api/controller/live/text?_=1626628079579"); + QUrl url(m_baseUrl + "/api/controller/live/text?_=1626628079579"); QNetworkRequest rqst(url); m_slideListNam->get(rqst); } void OpenLPClient::poll() { - QUrl url(QString(BaseUrl) + "/api/poll?_=1626628079579"); + QUrl url(m_baseUrl + "/api/poll?_=1626628079579"); QNetworkRequest rqst(url); m_pollingNam->get(rqst); } diff --git a/src/openlpclient.hpp b/src/openlpclient.hpp index d9192c5..e6b7c28 100644 --- a/src/openlpclient.hpp +++ b/src/openlpclient.hpp @@ -19,11 +19,7 @@ class OpenLPClient: public QObject { Q_OBJECT private: - struct Song { - QString name; - QString id; - }; - const QString BaseUrl = QString("http://") + SlideHost + ":4316"; + QString m_baseUrl; QNetworkAccessManager *m_nam = new QNetworkAccessManager(this); QNetworkAccessManager *m_pollingNam = new QNetworkAccessManager(this); QNetworkAccessManager *m_songListNam = new QNetworkAccessManager(this); @@ -53,12 +49,16 @@ class OpenLPClient: public QObject { void showSlides(); + void setSlidesVisible(bool value); + void changeSong(int it); void changeSlide(int slide); + void setBaseUrl(); + private: - void get(QString url); + void get(QString const&url); void requestSongList(); diff --git a/src/settingsdata.cpp b/src/settingsdata.cpp new file mode 100644 index 0000000..e7039c3 --- /dev/null +++ b/src/settingsdata.cpp @@ -0,0 +1,142 @@ +/* + * Copyright 2021 - 2023 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include "settingsdata.hpp" + +void setCameraConnectionData(QSettings &settings, ConnectionData const&cd) { + settings.beginGroup("CameraClient"); + settings.setValue("Host", cd.host); + settings.setValue("Port", cd.port); + settings.endGroup(); +} + +void setOpenLPConnectionData(QSettings &settings, ConnectionData const&cd) { + settings.beginGroup("OpenLPClient"); + settings.setValue("Host", cd.host); + settings.setValue("Port", cd.port); + settings.endGroup(); +} + +void setOBSConnectionData(QSettings &settings, ConnectionData const&cd) { + settings.beginGroup("OBSClient"); + settings.setValue("Host", cd.host); + settings.setValue("Port", cd.port); + settings.endGroup(); +} + +ConnectionData getCameraConnectionData(QSettings &settings) { + ConnectionData out; + settings.beginGroup("CameraClient"); + out.host = settings.value("Host", "192.168.100.88").toString(); + out.port = static_cast(settings.value("Port", 80).toInt()); + settings.endGroup(); + return out; +} + +ConnectionData getOpenLPConnectionData(QSettings &settings) { + ConnectionData out; + settings.beginGroup("OpenLPClient"); + out.host = settings.value("Host", "127.0.0.1").toString(); + out.port = static_cast(settings.value("Port", 4316).toInt()); + settings.endGroup(); + return out; +} + +ConnectionData getOBSConnectionData(QSettings &settings) { + ConnectionData out; + settings.beginGroup("OBSClient"); + out.host = settings.value("Host", "127.0.0.1").toString(); + out.port = static_cast(settings.value("Port", 9302).toInt()); + settings.endGroup(); + return out; +} + +void setCameraConnectionData(ConnectionData const&cd) { + QSettings settings; + settings.beginGroup("CameraClient"); + settings.setValue("Host", cd.host); + settings.setValue("Port", cd.port); + settings.endGroup(); +} + +void setOpenLPConnectionData(ConnectionData const&cd) { + QSettings settings; + settings.beginGroup("OpenLPClient"); + settings.setValue("Host", cd.host); + settings.setValue("Port", cd.port); + settings.endGroup(); +} + +void setOBSConnectionData(ConnectionData const&cd) { + QSettings settings; + settings.beginGroup("OBSClient"); + settings.setValue("Host", cd.host); + settings.setValue("Port", cd.port); + settings.endGroup(); +} + +ConnectionData getCameraConnectionData() { + QSettings s; + return getCameraConnectionData(s); +} + +ConnectionData getOpenLPConnectionData() { + QSettings s; + return getOpenLPConnectionData(s); +} + +ConnectionData getOBSConnectionData() { + QSettings s; + return getOBSConnectionData(s); +} + + +void setViews(QSettings &settings, QVector const&views) { + settings.beginGroup("Views"); + settings.beginWriteArray("Views"); + for (auto i = 0; auto const&view : views) { + settings.setArrayIndex(i); + settings.setValue("Name", view.name); + settings.setValue("Slides", view.slides); + settings.setValue("ObsSlides", view.obsSlides); + settings.setValue("Preset", view.cameraPreset); + ++i; + } + settings.endArray(); + settings.endGroup(); +} + +void setViews(QVector const&views) { + QSettings s; + return setViews(s, views); +} + +QVector getViews(QSettings &settings) { + QVector out; + settings.beginGroup("Views"); + const auto size = settings.beginReadArray("Views"); + for (auto i = 0; i < size; ++i) { + settings.setArrayIndex(i); + out.emplace_back(View{ + .name = settings.value("Name").toString(), + .slides = settings.value("Slides").toBool(), + .obsSlides = settings.value("ObsSlides").toBool(), + .cameraPreset = settings.value("Preset").toInt(), + }); + } + settings.endArray(); + settings.endGroup(); + return out; +} + +QVector getViews() { + QSettings s; + return getViews(s); +} diff --git a/src/settingsdata.hpp b/src/settingsdata.hpp new file mode 100644 index 0000000..4a39622 --- /dev/null +++ b/src/settingsdata.hpp @@ -0,0 +1,59 @@ +/* + * Copyright 2021 - 2023 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include + +struct ConnectionData { + QString host; + uint16_t port = 0; +}; + +void setCameraConnectionData(class QSettings &settings, ConnectionData const&cd); + +void setOpenLPConnectionData(class QSettings &settings, ConnectionData const&cd); + +void setOBSConnectionData(class QSettings &settings, ConnectionData const&cd); + +[[nodiscard]] +ConnectionData getCameraConnectionData(class QSettings &settings); + +[[nodiscard]] +ConnectionData getOpenLPConnectionData(class QSettings &settings); + +[[nodiscard]] +ConnectionData getOBSConnectionData(class QSettings &settings); + +[[nodiscard]] +ConnectionData getCameraConnectionData(); + +[[nodiscard]] +ConnectionData getOpenLPConnectionData(); + +[[nodiscard]] +ConnectionData getOBSConnectionData(); + + +struct View { + QString name; + bool slides = false; + bool obsSlides = false; + int cameraPreset = -1; +}; + +void setViews(class QSettings &settings, QVector const&views); + +void setViews(QVector const&views); + +[[nodiscard]] +QVector getViews(class QSettings &settings); + +[[nodiscard]] +QVector getViews(); \ No newline at end of file diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp new file mode 100644 index 0000000..ba76801 --- /dev/null +++ b/src/settingsdialog.cpp @@ -0,0 +1,224 @@ +/* + * Copyright 2021 - 2023 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "settingsdialog.hpp" + +constexpr auto MaxPresets = 9; +constexpr auto MaxViews = 9; + +enum ViewColumn { + Name = 0, + Slides, + ObsSlides, + CameraPreset, + Count +}; + +SettingsDialog::SettingsDialog(QWidget *parent): QDialog(parent) { + const auto lyt = new QVBoxLayout(this); + const auto tabs = new QTabWidget(this); + lyt->addWidget(tabs); + tabs->addTab(setupViewConfig(tabs), tr("&Views")); + tabs->addTab(setupNetworkInputs(tabs), tr("&Network")); + lyt->addWidget(setupButtons(this)); + setFixedSize(440, 440); +} + +QWidget *SettingsDialog::setupNetworkInputs(QWidget *parent) { + const auto root = new QWidget(parent); + const auto lyt = new QFormLayout(root); + const auto portValidator = new QIntValidator(1, 65536, this); + QSettings settings; + // camera settings + { + const auto c = getCameraConnectionData(settings); + m_cameraIpLe = new QLineEdit(root); + m_cameraPortLe = new QLineEdit(root); + m_cameraIpLe->setText(c.host); + m_cameraPortLe->setText(QString::number(c.port)); + m_cameraPortLe->setValidator(portValidator); + lyt->addRow(tr("C&amera IP Address:"), m_cameraIpLe); + lyt->addRow(tr("Ca&mera Port:"), m_cameraPortLe); + } + // OpenLP settings + { + const auto c = getOpenLPConnectionData(settings); + m_openLpIpLe = new QLineEdit(root); + m_openLpPortLe = new QLineEdit(root); + m_openLpIpLe->setText(c.host); + m_openLpPortLe->setText(QString::number(c.port)); + m_openLpPortLe->setValidator(portValidator); + lyt->addRow(tr("Op&enLP IP Address:"), m_openLpIpLe); + lyt->addRow(tr("Open&LP Port:"), m_openLpPortLe); + } + // OBS settings + { + const auto c = getOBSConnectionData(settings); + m_obsIpLe = new QLineEdit(root); + m_obsPortLe = new QLineEdit(root); + m_obsIpLe->setText(c.host); + m_obsPortLe->setText(QString::number(c.port)); + m_obsPortLe->setValidator(portValidator); + lyt->addRow(tr("O&BS IP Address:"), m_obsIpLe); + lyt->addRow(tr("OB&S Port:"), m_obsPortLe); + } + return root; +} + +QWidget *SettingsDialog::setupViewConfig(QWidget *parent) { + auto const root = new QWidget(parent); + auto const lyt = new QVBoxLayout(root); + // table + m_viewTable = new QTableWidget(parent); + { + lyt->addWidget(m_viewTable); + QStringList columns; + columns.resize(ViewColumn::Count); + columns[ViewColumn::Name] = tr("Name"); + columns[ViewColumn::Slides] = tr("Slides"); + columns[ViewColumn::ObsSlides] = tr("OBS Slides"); + columns[ViewColumn::CameraPreset] = tr("Camera Preset"); + m_viewTable->setColumnCount(static_cast(columns.size())); + m_viewTable->setHorizontalHeaderLabels(columns); + m_viewTable->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + m_viewTable->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows); + auto const hdr = m_viewTable->horizontalHeader(); + m_viewTable->setColumnWidth(1, 70); + m_viewTable->setColumnWidth(2, 70); + m_viewTable->setColumnWidth(3, 70); + hdr->setStretchLastSection(true); + } + // add/removes buttons + { + auto const btnsRoot = new QWidget(root); + auto const btnsLyt = new QHBoxLayout(btnsRoot); + auto const addBtn = new QPushButton("+", btnsRoot); + auto const rmBtn = new QPushButton("-", btnsRoot); + addBtn->setFixedWidth(20); + rmBtn->setFixedWidth(20); + rmBtn->setDisabled(true); + lyt->addWidget(btnsRoot); + btnsLyt->addWidget(addBtn); + btnsLyt->addWidget(rmBtn); + btnsLyt->addSpacerItem(new QSpacerItem(1000, 0, QSizePolicy::Expanding, QSizePolicy::Ignored)); + connect(addBtn, &QPushButton::clicked, this, [this, addBtn] { + auto const row = m_viewTable->rowCount(); + m_viewTable->setRowCount(row + 1); + setupViewRow(row); + addBtn->setEnabled(m_viewTable->rowCount() < MaxViews); + }); + connect(rmBtn, &QPushButton::clicked, this, [this, addBtn] { + auto const row = m_viewTable->currentRow(); + m_viewTable->removeRow(row); + addBtn->setEnabled(m_viewTable->rowCount() < MaxViews); + }); + connect(m_viewTable, &QTableWidget::currentCellChanged, rmBtn, [this, addBtn, rmBtn] (int row) { + rmBtn->setEnabled(row > -1 && row < m_viewTable->rowCount()); + addBtn->setEnabled(m_viewTable->rowCount() < MaxViews); + }); + const auto views = getViews(); + m_viewTable->setRowCount(static_cast(views.size())); + for (auto row = 0; auto const&view : views) { + setupViewRow(row, view); + ++row; + } + } + return root; +} + +QWidget *SettingsDialog::setupButtons(QWidget *parent) { + const auto root = new QWidget(parent); + const auto lyt = new QHBoxLayout(root); + m_errLbl = new QLabel(root); + const auto okBtn = new QPushButton(tr("&OK"), root); + const auto cancelBtn = new QPushButton(tr("&Cancel"), root); + lyt->addWidget(m_errLbl); + lyt->addSpacerItem(new QSpacerItem(1000, 0, QSizePolicy::Expanding, QSizePolicy::Ignored)); + lyt->addWidget(okBtn); + lyt->addWidget(cancelBtn); + connect(okBtn, &QPushButton::clicked, this, &SettingsDialog::handleOK); + connect(cancelBtn, &QPushButton::clicked, this, &SettingsDialog::reject); + return root; +} + +void SettingsDialog::handleOK() { + QSettings settings; + QVector views; + auto const viewsErr = collectViews(views); + if (viewsErr) { + return; + } + setViews(settings, views); + setCameraConnectionData(settings, { + .host = m_cameraIpLe->text(), + .port = m_cameraPortLe->text().toUShort(), + }); + setOpenLPConnectionData(settings, { + .host = m_openLpIpLe->text(), + .port = m_openLpPortLe->text().toUShort(), + }); + setOBSConnectionData(settings, { + .host = m_obsIpLe->text(), + .port = m_obsPortLe->text().toUShort(), + }); + accept(); +} + +void SettingsDialog::setupViewRow(int row, View const&view) { + // name + const auto nameItem = new QTableWidgetItem(view.name); + m_viewTable->setItem(row, ViewColumn::Name, nameItem); + // slides + const auto slidesCb = new QCheckBox(m_viewTable); + slidesCb->setChecked(view.slides); + m_viewTable->setCellWidget(row, ViewColumn::Slides, slidesCb); + // obs slides + const auto obsSlidesCb = new QCheckBox(m_viewTable); + obsSlidesCb->setChecked(view.obsSlides); + m_viewTable->setCellWidget(row, ViewColumn::ObsSlides, obsSlidesCb); + // camera preset + const auto presetItem = new QTableWidgetItem(QString::number(view.cameraPreset)); + m_viewTable->setItem(row, ViewColumn::CameraPreset, presetItem); +} + +int SettingsDialog::collectViews(QVector &views) const { + for (auto row = 0; row < m_viewTable->rowCount(); ++row) { + auto const viewNo = row + 1; + bool ok = false; + auto const name = m_viewTable->item(row, ViewColumn::Name)->text(); + if (name.trimmed() == "") { + m_errLbl->setText(tr("View %1 has no name.").arg(viewNo)); + return 1; + } + const auto cameraPreset = m_viewTable->item(row, ViewColumn::CameraPreset)->text().toInt(&ok); + if (!ok || cameraPreset < 1 || cameraPreset > MaxPresets) { + m_errLbl->setText(tr("View %1 has invalid preset (1-%2)").arg(viewNo).arg(MaxPresets)); + return 2; + } + views.emplace_back(View{ + .name = name, + .slides = dynamic_cast(m_viewTable->cellWidget(row, ViewColumn::Slides))->isChecked(), + .obsSlides = dynamic_cast(m_viewTable->cellWidget(row, ViewColumn::ObsSlides))->isChecked(), + .cameraPreset = cameraPreset, + }); + } + return 0; +} diff --git a/src/settingsdialog.hpp b/src/settingsdialog.hpp new file mode 100644 index 0000000..633d517 --- /dev/null +++ b/src/settingsdialog.hpp @@ -0,0 +1,40 @@ +/* + * Copyright 2021 - 2023 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include + +#include "settingsdata.hpp" + +class SettingsDialog: public QDialog { + Q_OBJECT + private: + class QLabel *m_errLbl = nullptr; + class QLineEdit *m_cameraIpLe = nullptr; + class QLineEdit *m_cameraPortLe = nullptr; + class QLineEdit *m_openLpIpLe = nullptr; + class QLineEdit *m_openLpPortLe = nullptr; + class QLineEdit *m_obsIpLe = nullptr; + class QLineEdit *m_obsPortLe = nullptr; + class QTableWidget *m_viewTable = nullptr; + public: + explicit SettingsDialog(QWidget *parent); + private: + QWidget *setupNetworkInputs(QWidget *parent); + QWidget *setupViewConfig(QWidget *parent); + QWidget *setupButtons(QWidget *parent); + void handleOK(); + void setupViewRow(int row, View const&view = {}); + /** + * Gets views from table. + * @return error code + */ + [[nodiscard("Must check error code")]] + int collectViews(QVector &views) const; +}; diff --git a/src/slideview.cpp b/src/slideview.cpp index 5b920eb..152f995 100644 --- a/src/slideview.cpp +++ b/src/slideview.cpp @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include #include #include #include @@ -43,9 +42,9 @@ QString SlideView::getNextSong() const { return ""; } -void SlideView::pollUpdate(QString songName, int slide) { +void SlideView::pollUpdate(QString const&songName, int slide) { auto songItems = m_songSelector->findItems(songName, Qt::MatchFixedString); - if (songItems.size() < 1) { + if (songItems.empty()) { return; } auto songItem = songItems.first(); @@ -66,11 +65,11 @@ void SlideView::changeSong(int song) { } } -void SlideView::slideListUpdate(QStringList tagList, QStringList slideList) { +void SlideView::slideListUpdate(QStringList const&tagList, QStringList const&slideList) { m_currentSlide = 0; - m_slideTable->setRowCount(slideList.size()); + m_slideTable->setRowCount(static_cast(slideList.size())); for (int i = 0; i < slideList.size(); ++i) { - auto txt = slideList[i]; + const auto& txt = slideList[i]; auto item = new QTableWidgetItem(txt); item->setFlags(item->flags() & ~Qt::ItemIsEditable); m_slideTable->setItem(i, 0, item); @@ -86,7 +85,7 @@ void SlideView::reset() { m_currentSlide = -1; } -void SlideView::songListUpdate(QStringList songList) { +void SlideView::songListUpdate(QStringList const&songList) { // Is this replacing an existing song list or is it the initial song list? // We want to reset the song to 0 upon replacement, // but leave it alone upon initialization. diff --git a/src/slideview.hpp b/src/slideview.hpp index fb20c32..ff8fdf1 100644 --- a/src/slideview.hpp +++ b/src/slideview.hpp @@ -24,11 +24,11 @@ class SlideView: public QWidget { QString getNextSong() const; public slots: - void pollUpdate(QString songId, int slideNum); + void pollUpdate(const QString& songId, int slideNum); - void songListUpdate(QStringList songList); + void songListUpdate(QStringList const&songList); - void slideListUpdate(QStringList tagList, QStringList songList); + void slideListUpdate(QStringList const&tagList, QStringList const&songList); void reset();