Compare commits

...

67 Commits

Author SHA1 Message Date
9f57ba69ac Remove camera reboot, bump version to 1.0.0 2025-04-11 22:49:58 -05:00
5afd22a7bf Update copyright year in About 2025-03-29 14:50:54 -05:00
af07f8853a Fix SettingsDialog to do validity check when OK is pressed 2025-03-29 14:45:32 -05:00
2e9b37f3b4 Fix default Show button 2025-03-27 20:32:53 -05:00
c4e792c9a7 Bump version to 1.0-beta6 2025-03-27 20:29:46 -05:00
01cd6b7ba8 Add icon 2025-03-27 20:26:25 -05:00
e42c3a7dc8 Fix 'make run' and 'make debug' 2024-10-30 00:40:31 -05:00
1d66bcc3bb Remove .idea 2024-10-29 23:51:26 -05:00
efab455a24 Cleanup 2024-10-29 23:45:58 -05:00
834a36c417 Fix make run for new buildcore 2024-10-29 23:45:31 -05:00
59aee596e9 Add icon 2024-10-29 23:44:54 -05:00
b96b75a735 [buildcore] Update buildcore 2024-10-29 23:44:45 -05:00
215f9b4d1d Cleanup 2024-10-29 00:46:30 -05:00
4957bec1c6 Cleanup 2024-10-29 00:45:16 -05:00
ef24f01566 Disable Fusion theme on Mac 2024-10-29 00:26:32 -05:00
d0eaad7dfe Remove debug code 2024-10-28 23:59:54 -05:00
fff098d80e Set app organization name 2024-10-28 20:29:42 -05:00
ec18a0c507 Switch Fusion dark theme 2024-10-28 19:46:06 -05:00
88da18a380 Up version 2024-10-27 14:49:16 -05:00
0db9bff0de Split slide tag across lines 2024-10-25 00:44:58 -05:00
a0a1cd8af1 Upgrade OpenLPClient to target OpenLP 3 2024-10-23 22:22:53 -05:00
f4e0b5ab9f Update year in copyright notices 2024-03-08 22:07:39 -06:00
bb825f947e Update version 2024-03-03 21:25:14 -06:00
b71a64ca33 Add Apply button to settings dialog 2024-03-03 21:21:00 -06:00
048b06db97 Fix Image Preview button to use the correct image settings 2024-03-03 21:14:14 -06:00
344cc4f819 Up version in about menu to 1.0-beta3 2023-10-15 17:09:04 -05:00
ae580a0d58 Fix Reboot dialog for Qt6 2023-10-15 16:47:18 -05:00
ac7bb9c585 Add camera reboot option 2023-10-15 16:02:24 -05:00
56f98eed60 Update buildcore 2023-08-07 21:55:55 -05:00
9171a080a7 Make project require Qt6 2023-08-05 18:07:43 -05:00
8a4989923c Add preview button to Image tab in settings 2023-07-28 23:46:21 -05:00
77df4257c5 Remove some commented out code 2023-07-24 23:36:56 -05:00
87997e8f18 Change mnemonic in Contrast from 'o' to 't' 2023-07-24 22:41:14 -05:00
8bdad3ed68 Remove qDebug 2023-07-24 22:32:58 -05:00
b7aba1180d Up version to 1.0-beta2 2023-07-24 21:53:39 -05:00
ed01f44b91 Make Image tab in setting the second tab 2023-07-24 21:50:25 -05:00
b119268396 Add video image settings per camera preset 2023-07-24 21:46:41 -05:00
bc342a290f Switch autos to east const 2023-07-21 01:19:07 -05:00
814cef4a2e Add README 2023-07-21 00:11:16 -05:00
a9ad077d55 Change version 1.0.0-beta1 2023-07-20 21:21:48 -05:00
2817d543e9 Add Slides menu, Camera Preset menu, and Help > About 2023-07-20 21:07:49 -05:00
8f3693a1e5 Replace key specifiers in slide buttons with tooltips 2023-07-20 20:31:23 -05:00
877f894511 Make Default view controls use grid layout, make settings accept update view controls 2023-07-20 20:26:23 -05:00
0a28e80455 Slightly extend OBS Slides column in Views table 2023-07-20 20:24:54 -05:00
35bf654246 Change "IP Address" options to "Host" 2023-07-20 19:35:51 -05:00
2bfd466da1 Fix crash that happened after switching from an invalid to valid OpenLP host 2023-07-20 19:33:59 -05:00
160977110e Fix Makefile debugger command 2023-07-20 19:30:46 -05:00
29cdc499e6 Add settings dialog, camera controls 2023-07-20 19:30:08 -05:00
a8327b2b67 Update to new buildcore 2023-07-16 22:50:14 -05:00
6c00d960bd Add LICENSE file 2023-07-16 22:50:01 -05:00
6b81eb4137 Run liccor 2023-07-16 22:43:16 -05:00
7bc42bf01e [sc9k] Fix signal/slot disconnect in SlideView::songListUpdate 2022-07-31 08:48:13 -05:00
9f29b58522 [sc9k] Fix to SlideView to use correct signal from QListWidget 2022-07-24 09:23:07 -05:00
564ed77c9a [sc9k] Swap song view and slide view, fix item lookup in song view 2022-07-24 08:56:17 -05:00
7ca3b810fc [sc9k] Make song selector a QListWidget instead of QComboBox 2022-07-24 00:35:11 -05:00
68d963ab69 [buildcore] Update buildcore 2022-07-24 00:34:52 -05:00
6f6f77f104 Get rid of "in Both" language 2022-01-30 21:45:03 -06:00
e57ba9e8f2 Eliminate now redundant tool tip for Show in Both button 2022-01-30 21:18:03 -06:00
382e09d4b4 Collapse slide controls into one line 2022-01-30 19:43:19 -06:00
181e1b8599 Combine Show in OpenLP and Hide in OBS buttons 2022-01-30 19:22:50 -06:00
3115083267 Fix show/hide button order and naming 2022-01-30 19:10:06 -06:00
2d5af03724 Update button layout and keyboard shortcut 2022-01-30 18:59:11 -06:00
c1cab3e3f3 [buildcore] Update buildcore 2022-01-30 18:56:46 -06:00
8d0b0fb4c5 Fix next song in status bar 2021-10-24 16:01:23 -05:00
f9122c2942 Fix typo n Next Song button 2021-10-24 16:01:23 -05:00
b0eeb81592 Add next song to status line 2021-10-24 16:01:23 -05:00
7999cc486f Fix OBS general GET reply handling 2021-10-24 16:01:23 -05:00
39 changed files with 1960 additions and 287 deletions

4
.gitignore vendored
View File

@ -1,10 +1,14 @@
.cache
.clangd
.conanbuild
.current_build
.idea
__pycache__
CMakeLists.txt.user
Session.vim
build
compile_commands.json
cmake-build-debug
dist
graph_info.json
tags

8
.idea/.gitignore generated vendored
View File

@ -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/

17
.idea/misc.xml generated
View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompDBSettings">
<option name="linkedExternalProjectsSettings">
<CompDBProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</CompDBProjectSettings>
</option>
</component>
<component name="CompDBWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
<component name="ExternalStorageConfigurationManager" enabled="true" />
</project>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -2,7 +2,7 @@
source:
- .
copyright_notice: |-
Copyright 2021 gary@drinkingtea.net
Copyright 2021 - 2024 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

View File

@ -14,6 +14,10 @@ if(NOT MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-conversion")
endif()
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_INSTALL_RPATH "$ORIGIN" "$ORIGIN/../")
if(QTDIR)
set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_RPATH} "${QTDIR}/lib")

373
LICENSE Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
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/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@ -3,15 +3,15 @@ BUILDCORE_PATH=deps/buildcore
VCPKG_PKGS=
include ${BUILDCORE_PATH}/base.mk
ifeq ($(OS),darwin)
PROJECT_EXECUTABLE=./dist/${CURRENT_BUILD}/${PROJECT_NAME}.app/Contents/MacOS/SlideController
ifeq ($(BC_VAR_OS),darwin)
PROJECT_EXECUTABLE=./build/${BC_VAR_CURRENT_BUILD}/bin/${PROJECT_NAME}.app/Contents/MacOS/SlideController
else
PROJECT_EXECUTABLE=./dist/${CURRENT_BUILD}/bin/SlideController
PROJECT_EXECUTABLE=./build/${BC_VAR_CURRENT_BUILD}/bin/${PROJECT_NAME}
endif
.PHONY: run
run: install
run: build
${ENV_RUN} ${PROJECT_EXECUTABLE}
.PHONY: debug
debug: install
${ENV_RUN} gdb --args ${PROJECT_EXECUTABLE}
debug: build
${DEBUGGER} ${PROJECT_EXECUTABLE}

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# Slide Controller 9000
## Build Prerequisites
* Install GCC, Clang, or Visual Studio with C++20 support
* Install Python 3
* Install Ninja, Make, and CMake
* [Qt6](https://www.qt.io/download-qt-installer-oss) (Network and Widgets)
* Consider also installing ccache for faster subsequent build times
## Build
Build options: release, debug, asan
make purge configure-{release,debug,asan} install
## Run
make run

View File

@ -11,7 +11,7 @@ set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/dist/${BUILDCORE_BUILD_CONFIG}")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# enable ccache
@ -26,9 +26,14 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_definitions(-DDEBUG)
else()
add_definitions(-DNDEBUG)
if(APPLE)
set(CMAKE_OSX_ARCHITECTURES arm64;x86_64)
endif()
endif()
if(NOT MSVC)
if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:preprocessor")
else()
# forces colored output when using ninja
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color")
# enable warnings
@ -39,12 +44,13 @@ if(NOT MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wformat=2")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wmissing-field-initializers")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wnon-virtual-dtor")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wnull-dereference")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-null-dereference")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wold-style-cast")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Woverloaded-virtual")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpedantic")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-compare")
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-conversion")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-conversion")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wconversion")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wunused")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wunused-variable")
# release build options

194
deps/buildcore/base.mk vendored
View File

@ -1,5 +1,5 @@
#
# Copyright 2016 - 2021 gary@drinkingtea.net
# Copyright 2016 - 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
@ -9,70 +9,103 @@
ifeq (${OS},Windows_NT)
SHELL := powershell.exe
.SHELLFLAGS := -NoProfile -Command
OS=windows
HOST_ENV=${OS}
BC_VAR_OS=windows
BC_CMD_HOST_PY3=python
else
OS=$(shell uname | tr [:upper:] [:lower:])
HOST_ENV=${OS}-$(shell uname -m)
endif
ifeq ($(shell python -c 'import sys; print(sys.version_info[0])'),3)
PYTHON3=python
else
PYTHON3=python3
endif
SCRIPTS=${BUILDCORE_PATH}/scripts
SETUP_BUILD=${PYTHON3} ${SCRIPTS}/setup-build.py
PYBB=${PYTHON3} ${SCRIPTS}/pybb.py
CMAKE_BUILD=${PYBB} cmake-build
RM_RF=${PYBB} rm
ifdef USE_VCPKG
ifndef VCPKG_DIR_BASE
VCPKG_DIR_BASE=.vcpkg
BC_VAR_OS=$(shell uname | tr [:upper:] [:lower:])
ifneq ($(shell which python3 2> /dev/null),)
BC_CMD_HOST_PY3=python3
else
ifeq ($(shell python -c 'import sys; print(sys.version_info[0])'),3)
BC_CMD_HOST_PY3=python
else
echo 'Please install Python3 on host'
exit 1
endif
ifndef VCPKG_VERSION
VCPKG_VERSION=2020.06
endif
VCPKG_TOOLCHAIN=--toolchain=${VCPKG_DIR}/scripts/buildsystems/vcpkg.cmake
endif
ifeq ($(OS),darwin)
DEBUGGER=lldb
else
DEBUGGER=gdb --args
endif
VCPKG_DIR=$(VCPKG_DIR_BASE)/$(VCPKG_VERSION)-$(HOST_ENV)
DEVENV=devenv$(shell pwd | sed 's/\//-/g')
DEVENV_IMAGE=${PROJECT_NAME}-devenv
ifneq ($(shell which docker 2> /dev/null),)
ifeq ($(shell docker inspect --format="{{.State.Status}}" ${DEVENV} 2>&1),running)
ENV_RUN=docker exec -i -t --user $(shell id -u ${USER}) ${DEVENV}
endif
endif
CURRENT_BUILD=$(HOST_ENV)-$(shell ${PYBB} cat .current_build)
ifdef BC_VAR_USE_DOCKER_DEVENV
ifneq ($(shell which docker 2> /dev/null),)
BC_VAR_DEVENV=devenv$(shell pwd | sed 's/\//-/g')
BC_VAR_DEVENV_IMAGE=${BC_VAR_PROJECT_NAME}-devenv
ifeq ($(shell docker inspect --format="{{.State.Status}}" ${BC_VAR_DEVENV} 2>&1),running)
BC_CMD_ENVRUN=docker exec -i -t --user $(shell id -u ${USER}) ${BC_VAR_DEVENV}
endif
endif
ifneq ($(shell ${BC_CMD_ENVRUN} which python3 2> /dev/null),)
BC_CMD_PY3=${BC_CMD_ENVRUN} python3
else
ifeq ($(shell ${BC_CMD_ENVRUN} python -c 'import sys; print(sys.version_info[0])'),3)
BC_CMD_PY3=${BC_CMD_ENVRUN} python
else
echo 'Please install Python3 in devenv'
exit 1
endif
endif
ifndef BC_VAR_DEVENV_ROOT
BC_VAR_DEVENV_ROOT="."
endif
else
BC_CMD_PY3=${BC_CMD_HOST_PY3}
endif
BC_VAR_SCRIPTS=${BUILDCORE_PATH}/scripts
BC_CMD_SETUP_BUILD=${BC_CMD_PY3} ${BC_VAR_SCRIPTS}/setup-build.py
BC_CMD_PYBB=${BC_CMD_PY3} ${BC_VAR_SCRIPTS}/pybb.py
BC_CMD_HOST_PYBB=${BC_CMD_HOST_PY3} ${BC_VAR_SCRIPTS}/pybb.py
BC_CMD_CMAKE_BUILD=${BC_CMD_PYBB} cmake-build
BC_CMD_GETENV=${BC_CMD_PYBB} getenv
BC_CMD_CTEST=${BC_CMD_PYBB} ctest-all
BC_CMD_RM_RF=${BC_CMD_PYBB} rm
BC_CMD_MKDIR_P=${BC_CMD_PYBB} mkdir
BC_CMD_CAT=${BC_CMD_PYBB} cat
BC_CMD_DEBUGGER=${BC_CMD_PYBB} debug
BC_CMD_HOST_DEBUGGER=${BC_CMD_HOST_PYBB} debug
BC_VAR_HOSTENV=$(shell ${BC_CMD_ENVRUN} ${BC_CMD_PYBB} hostenv)
BC_VAR_BUILD_PATH=build
BC_VAR_CURRENT_BUILD=$(BC_VAR_HOSTENV)-$(shell ${BC_CMD_ENVRUN} ${BC_CMD_CAT} .current_build)
ifdef BC_VAR_USE_VCPKG
ifndef BC_VAR_VCPKG_DIR_BASE
BC_VAR_VCPKG_DIR_BASE=.vcpkg
endif
ifndef BC_VAR_VCPKG_VERSION
BC_VAR_VCPKG_VERSION=2023.08.09
endif
endif
.PHONY: build
build:
${ENV_RUN} ${CMAKE_BUILD} build
${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH}
.PHONY: install
install:
${ENV_RUN} ${CMAKE_BUILD} build install
${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} install
.PHONY: clean
clean:
${ENV_RUN} ${CMAKE_BUILD} build clean
${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} clean
.PHONY: purge
purge:
${ENV_RUN} ${RM_RF} .current_build
${ENV_RUN} ${RM_RF} build
${ENV_RUN} ${RM_RF} dist
${BC_CMD_RM_RF} .current_build
${BC_CMD_RM_RF} ${BC_VAR_BUILD_PATH}
${BC_CMD_RM_RF} dist
${BC_CMD_RM_RF} compile_commands.json
.PHONY: test
test: build
${ENV_RUN} ${CMAKE_BUILD} build test
${BC_CMD_ENVRUN} mypy ${BC_VAR_SCRIPTS}
${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} test
.PHONY: test-verbose
test-verbose: build
${BC_CMD_CTEST} ${BC_VAR_BUILD_PATH} --output-on-failure
.PHONY: test-rerun-verbose
test-rerun-verbose: build
${BC_CMD_CTEST} ${BC_VAR_BUILD_PATH} --rerun-failed --output-on-failure
ifdef BC_VAR_USE_DOCKER_DEVENV
.PHONY: devenv-image
devenv-image:
docker build . -t ${DEVENV_IMAGE}
docker build ${BC_VAR_DEVENV_ROOT} -t ${BC_VAR_DEVENV_IMAGE}
.PHONY: devenv-create
devenv-create:
docker run -d \
@ -84,62 +117,77 @@ devenv-create:
-v $(shell pwd):/usr/src/project \
-v /dev/shm:/dev/shm \
--restart=always \
--name ${DEVENV} \
-t ${DEVENV_IMAGE} bash
--name ${BC_VAR_DEVENV} \
-t ${BC_VAR_DEVENV_IMAGE} bash
.PHONY: devenv-destroy
devenv-destroy:
docker rm -f ${DEVENV}
docker rm -f ${BC_VAR_DEVENV}
ifdef BC_CMD_ENVRUN
.PHONY: devenv-shell
devenv-shell:
${ENV_RUN} bash
${BC_CMD_ENVRUN} bash
endif
endif
ifdef BC_VAR_USE_VCPKG
BC_VAR_VCPKG_TOOLCHAIN=--toolchain=${BC_VAR_VCPKG_DIR}/scripts/buildsystems/vcpkg.cmake
BC_VAR_VCPKG_DIR=$(BC_VAR_VCPKG_DIR_BASE)/$(BC_VAR_VCPKG_VERSION)-$(BC_VAR_HOSTENV)
ifdef USE_VCPKG
.PHONY: vcpkg
vcpkg: ${VCPKG_DIR} vcpkg-install
vcpkg: ${BC_VAR_VCPKG_DIR} vcpkg-install
${VCPKG_DIR}:
${ENV_RUN} ${RM_RF} ${VCPKG_DIR}
${ENV_RUN} mkdir -p ${VCPKG_DIR_BASE}
${ENV_RUN} git clone -b release --depth 1 --branch ${VCPKG_VERSION} https://github.com/microsoft/vcpkg.git ${VCPKG_DIR}
ifneq (${OS},windows)
${ENV_RUN} ${VCPKG_DIR}/bootstrap-vcpkg.sh
${BC_VAR_VCPKG_DIR}:
${BC_CMD_RM_RF} ${BC_VAR_VCPKG_DIR}
${BC_CMD_PYBB} mkdir ${BC_VAR_VCPKG_DIR_BASE}
${BC_CMD_ENVRUN} git clone -b release --depth 1 --branch ${BC_VAR_VCPKG_VERSION} https://github.com/microsoft/vcpkg.git ${BC_VAR_VCPKG_DIR}
ifneq (${BC_VAR_OS},windows)
${BC_CMD_ENVRUN} ${BC_VAR_VCPKG_DIR}/bootstrap-vcpkg.sh
else
${ENV_RUN} ${VCPKG_DIR}/bootstrap-vcpkg.bat
${BC_CMD_ENVRUN} ${BC_VAR_VCPKG_DIR}/bootstrap-vcpkg.bat
endif
.PHONY: vcpkg-install
vcpkg-install:
ifneq (${OS},windows)
${VCPKG_DIR}/vcpkg install ${VCPKG_PKGS}
ifneq (${BC_VAR_OS},windows)
${BC_CMD_ENVRUN} ${BC_VAR_VCPKG_DIR}/vcpkg install ${BC_VAR_VCPKG_PKGS}
else
${VCPKG_DIR}/vcpkg install --triplet x64-windows ${VCPKG_PKGS}
${BC_CMD_ENVRUN} ${BC_VAR_VCPKG_DIR}/vcpkg install --triplet x64-windows ${BC_VAR_VCPKG_PKGS}
endif
else # USE_VCPKG
else ifdef USE_CONAN # USE_VCPKG / USE_CONAN ####################################
.PHONY: setup-conan
conan-config:
conan profile new nostalgia --detect --force
ifeq ($(OS),linux)
conan profile update settings.compiler.libcxx=libstdc++11 ${PROJECT_NAME}
${BC_CMD_ENVRUN} conan profile new ${BC_VAR_PROJECT_NAME} --detect --force
ifeq ($(BC_VAR_OS),linux)
${BC_CMD_ENVRUN} conan profile update settings.compiler.libcxx=libstdc++11 ${BC_VAR_PROJECT_NAME}
else
${BC_CMD_ENVRUN} conan profile update settings.compiler.cppstd=20 ${BC_VAR_PROJECT_NAME}
ifeq ($(BC_VAR_OS),windows)
${BC_CMD_ENVRUN} conan profile update settings.compiler.runtime=static ${BC_VAR_PROJECT_NAME}
endif
endif
.PHONY: conan
conan:
@mkdir -p .conanbuild && cd .conanbuild && conan install ../ --build=missing -pr=${PROJECT_NAME}
endif # USE_VCPKG
${BC_CMD_PYBB} conan-install ${BC_VAR_PROJECT_NAME}
endif # USE_CONAN ###############################################
ifeq (${BC_VAR_OS},darwin)
.PHONY: configure-xcode
configure-xcode:
${ENV_RUN} ${SETUP_BUILD} ${VCPKG_TOOLCHAIN} --build_tool=xcode --current_build=0
${BC_CMD_SETUP_BUILD} ${BC_VAR_VCPKG_TOOLCHAIN} --build_tool=xcode --current_build=0 --build_root=${BC_VAR_BUILD_PATH}
endif
.PHONY: configure-release
configure-release:
${ENV_RUN} ${SETUP_BUILD} ${VCPKG_TOOLCHAIN} --build_type=release
${BC_CMD_SETUP_BUILD} ${BC_VAR_VCPKG_TOOLCHAIN} --build_type=release --build_root=${BC_VAR_BUILD_PATH}
.PHONY: configure-debug
configure-debug:
${ENV_RUN} ${SETUP_BUILD} ${VCPKG_TOOLCHAIN} --build_type=debug
${BC_CMD_SETUP_BUILD} ${BC_VAR_VCPKG_TOOLCHAIN} --build_type=debug --build_root=${BC_VAR_BUILD_PATH}
.PHONY: configure-asan
configure-asan:
${ENV_RUN} ${SETUP_BUILD} ${VCPKG_TOOLCHAIN} --build_type=asan
${BC_CMD_SETUP_BUILD} ${BC_VAR_VCPKG_TOOLCHAIN} --build_type=asan --build_root=${BC_VAR_BUILD_PATH}

28
deps/buildcore/scripts/file_to_c.py vendored Normal file
View File

@ -0,0 +1,28 @@
#! /usr/bin/env python3
#
# Copyright 2016 - 2022 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/.
#
import argparse
import sys
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--out-cpp', help='path to output cpp file')
parser.add_argument('--out-hpp', help='path to output hpp file')
args = parser.parse_args()
return 0
if __name__ == '__main__':
try:
err = main()
sys.exit(err)
except KeyboardInterrupt:
sys.exit(1)

View File

@ -8,68 +8,156 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# "Python Busy Box" - adds cross platform equivalents to Unix commands that
# "Python Busy Box" - adds cross-platform equivalents to Unix commands that
# don't translate well to that other operating system
import os
import platform
import shutil
import subprocess
import sys
from typing import List, Optional
import util
def cat(path):
def mkdir(path: str) -> int:
try:
with open(path) as f:
data = f.read()
print(data)
return 0
except FileNotFoundError:
sys.stderr.write('cat: {}: no such file or directory\n'.format(path))
util.mkdir_p(path)
except Exception:
return 1
return 0
def mkdir(path):
if not os.path.exists(path) and os.path.isdir(path):
os.mkdir(path)
def rm_multi(paths: List[str]):
for path in paths:
util.rm(path)
# this exists because Windows is utterly incapable of providing a proper rm -rf
def rm(path):
if (os.path.exists(path) or os.path.islink(path)) and not os.path.isdir(path):
os.remove(path)
elif os.path.isdir(path):
shutil.rmtree(path)
def ctest_all() -> int:
base_path = sys.argv[2]
if not os.path.isdir(base_path):
# no generated projects
return 0
args = ['ctest'] + sys.argv[3:]
orig_dir = os.getcwd()
for d in os.listdir(base_path):
os.chdir(os.path.join(orig_dir, base_path, d))
err = subprocess.run(args).returncode
if err != 0:
return err
return 0
def cmake_build(base_path, target):
def cmake_build(base_path: str, target: Optional[str]) -> int:
if not os.path.isdir(base_path):
# nothing to build
return 0
for d in os.listdir(base_path):
args = ['cmake', '--build', os.path.join(base_path, d)]
path = os.path.join(base_path, d)
if not os.path.isdir(path):
continue
args = ['cmake', '--build', path]
if target is not None:
args.extend(['--target', target])
err = subprocess.run(args).returncode
if err != 0:
return err
return 0
def main():
def conan() -> int:
project_name = sys.argv[2]
conan_dir = '.conanbuild'
err = 0
try:
mkdir(conan_dir)
except Exception:
return 1
if err != 0:
return err
args = ['conan', 'install', '../', '--build=missing', '-pr', project_name]
os.chdir(conan_dir)
return subprocess.run(args).returncode
def cat(paths: List[str]) -> int:
for path in paths:
try:
with open(path) as f:
data = f.read()
print(data)
except FileNotFoundError:
sys.stderr.write(f'cat: {path}: no such file or directory\n')
return 1
return 0
def debug(paths: List[str]) -> int:
if shutil.which('gdb') is not None:
args = ['gdb', '--args']
elif shutil.which('lldb') is not None:
args = ['lldb', '--']
else:
sys.stderr.write('debug: could not find a supported debugger\n')
return 1
args.extend(paths)
return subprocess.run(args).returncode
def get_env(var_name: str) -> int:
if var_name not in os.environ:
return 1
print(os.environ[var_name])
return 0
def hostname() -> int:
print(platform.node())
return 0
def host_env() -> int:
os_name = platform.system().lower()
arch = util.get_arch()
print(f'{os_name}-{arch}')
return 0
def clarg(idx: int) -> Optional[str]:
return sys.argv[idx] if len(sys.argv) > idx else None
def main() -> int:
err = 0
if sys.argv[1] == 'mkdir':
mkdir(sys.argv[2])
err = mkdir(sys.argv[2])
elif sys.argv[1] == 'rm':
for i in range(2, len(sys.argv)):
rm(sys.argv[i])
rm_multi(sys.argv[2:])
elif sys.argv[1] == 'conan-install':
err = conan()
elif sys.argv[1] == 'ctest-all':
err = ctest_all()
elif sys.argv[1] == 'cmake-build':
err = cmake_build(sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else None)
sys.exit(err)
err = cmake_build(sys.argv[2], clarg(3))
elif sys.argv[1] == 'cat':
err = cat(sys.argv[2])
sys.exit(err)
err = cat(sys.argv[2:])
elif sys.argv[1] == 'debug':
err = debug(sys.argv[2:])
elif sys.argv[1] == 'getenv':
err = get_env(sys.argv[2])
elif sys.argv[1] == 'hostname':
err = hostname()
elif sys.argv[1] == 'hostenv':
err = host_env()
else:
sys.stderr.write('Command not found\n')
err = 1
return err
if __name__ == '__main__':
try:
main()
sys.exit(main())
except KeyboardInterrupt:
sys.exit(1)

View File

@ -15,17 +15,35 @@ import shutil
import subprocess
import sys
from pybb import mkdir, rm
import util
def main():
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--target', help='Platform target',
default='{:s}-{:s}'.format(sys.platform, platform.machine()))
parser.add_argument('--build_type', help='Build type (asan,debug,release)', default='release')
parser.add_argument('--build_tool', help='Build tool (default,xcode)', default='')
parser.add_argument('--toolchain', help='Path to CMake toolchain file', default='')
parser.add_argument('--current_build', help='Indicates whether or not to make this the active build', default=1)
parser.add_argument(
'--target',
help='Platform target',
default=f'{util.get_os()}-{util.get_arch()}')
parser.add_argument(
'--build_type',
help='Build type (asan,debug,release)',
default='release')
parser.add_argument(
'--build_tool',
help='Build tool (default,xcode)',
default='')
parser.add_argument(
'--build_root',
help='Path to the root build directory (must be in project dir)',
default='build')
parser.add_argument(
'--toolchain',
help='Path to CMake toolchain file',
default='')
parser.add_argument(
'--current_build',
help='Indicates whether or not to make this the active build',
default=1)
args = parser.parse_args()
if args.build_type == 'asan':
@ -39,7 +57,7 @@ def main():
sanitizer_status = 'OFF'
else:
print('Error: Invalid build tool')
sys.exit(1)
return 1
if args.build_tool == 'xcode':
build_config = '{:s}-{:s}'.format(args.target, args.build_tool)
@ -60,35 +78,46 @@ def main():
build_tool = '-GXcode'
else:
print('Error: Invalid build tool')
sys.exit(1)
return 1
project_dir = os.getcwd()
build_dir = '{:s}/build/{:s}'.format(project_dir, build_config)
rm(build_dir)
mkdir(build_dir)
subprocess.run(['cmake', '-S', project_dir, '-B', build_dir, build_tool,
build_dir = f'{project_dir}/{args.build_root}/{build_config}'
util.rm(build_dir)
cmake_cmd = [
'cmake', '-S', project_dir, '-B', build_dir,
'-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
'-DCMAKE_TOOLCHAIN_FILE={:s}'.format(args.toolchain),
'-DCMAKE_BUILD_TYPE={:s}'.format(build_type_arg),
'-DUSE_ASAN={:s}'.format(sanitizer_status),
'-DBUILDCORE_BUILD_CONFIG={:s}'.format(build_config),
'-DBUILDCORE_TARGET={:s}'.format(args.target),
qt_path,
])
]
if build_tool != '':
cmake_cmd.append(build_tool)
if qt_path != '':
cmake_cmd.append(qt_path)
if platform.system() == 'Windows' and platform.system() == 'AMD64':
cmake_cmd.append('-A x64')
mkdir('dist')
cmake_err = subprocess.run(cmake_cmd).returncode
if cmake_err != 0:
return cmake_err
util.mkdir_p('dist')
if int(args.current_build) != 0:
cb = open('.current_build', 'w')
cb.write(args.build_type)
cb.close()
rm('compile_commands.json')
util.rm('compile_commands.json')
if platform.system() != 'Windows':
os.symlink('build/{:s}/compile_commands.json'.format(build_config), 'compile_commands.json')
os.symlink(f'{build_dir}/compile_commands.json',
'compile_commands.json')
return 0
if __name__ == '__main__':
try:
main()
sys.exit(main())
except KeyboardInterrupt:
sys.exit(1)

38
deps/buildcore/scripts/util.py vendored Normal file
View File

@ -0,0 +1,38 @@
#
# Copyright 2016 - 2021 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/.
#
import os
import platform
import shutil
def mkdir_p(path: str):
if not os.path.exists(path):
os.mkdir(path)
# this exists because Windows is utterly incapable of providing a proper rm -rf
def rm(path: str):
file_exists = os.path.exists(path)
is_link = os.path.islink(path)
is_dir = os.path.isdir(path)
if (file_exists or is_link) and not is_dir:
os.remove(path)
elif os.path.isdir(path):
shutil.rmtree(path)
def get_os() -> str:
return platform.system().lower()
def get_arch() -> str:
arch = platform.machine().lower()
if arch == 'amd64':
arch = 'x86_64'
return arch

BIN
iconsrc/icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
iconsrc/icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
iconsrc/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -3,6 +3,7 @@ from urllib.parse import urlparse, parse_qs
import threading
import obspython as obs
def set_current_scene(scene_name):
scenes = obs.obs_frontend_get_scenes()
for scene in scenes:
@ -12,6 +13,7 @@ def set_current_scene(scene_name):
return 0
return 1
class RqstHandler(BaseHTTPRequestHandler):
def do_GET(self):
@ -25,12 +27,15 @@ class RqstHandler(BaseHTTPRequestHandler):
self.send_response(200)
self.end_headers()
def log_message(self, format, *args):
pass
def run(name):
httpd = HTTPServer(('127.0.0.1', 9302), RqstHandler)
httpd = HTTPServer(('0.0.0.0', 9302), RqstHandler)
httpd.serve_forever()
t = threading.Thread(target=run, args=(1,), daemon=True)
t.start()

View File

@ -3,16 +3,20 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Network Widgets REQUIRED)
find_package(QT NAMES Qt6 COMPONENTS Network Widgets REQUIRED)
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
sc9k.rc
)
target_link_libraries(

113
src/cameraclient.cpp Normal file
View File

@ -0,0 +1,113 @@
/*
* Copyright 2021 - 2024 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 <QNetworkReply>
#include <QSettings>
#include "consts.hpp"
#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::setPresetVC(int preset, VideoConfig const&vc) {
if (preset > 0 && preset < MaxCameraPresets) {
get(QString("/cgi-bin/ptzctrl.cgi?ptzcmd&poscall&%1").arg(preset));
setBrightness(vc.brightness);
setSaturation(vc.saturation);
setContrast(vc.contrast);
setSharpness(vc.sharpness);
setHue(vc.hue);
}
}
void CameraClient::setPreset(int preset) {
if (preset > 0 && preset < MaxCameraPresets) {
get(QString("/cgi-bin/ptzctrl.cgi?ptzcmd&poscall&%1").arg(preset));
auto const vc = getVideoConfig()[preset - 1];
setBrightness(vc.brightness);
setSaturation(vc.saturation);
setContrast(vc.contrast);
setSharpness(vc.sharpness);
setHue(vc.hue);
}
}
void CameraClient::reboot() {
post("/cgi-bin/param.cgi?post_reboot");
emit pollFailed();
}
void CameraClient::setBaseUrl() {
auto const [host, port] = getCameraConnectionData();
m_baseUrl = QString("http://%1:%2").arg(host, QString::number(port));
}
void CameraClient::setBrightness(int val) {
if (val > -1) {
get(QString("/cgi-bin/ptzctrl.cgi?post_image_value&bright&%1").arg(val));
}
}
void CameraClient::setSaturation(int val) {
if (val > -1) {
get(QString("/cgi-bin/ptzctrl.cgi?post_image_value&saturation&%1").arg(val));
}
}
void CameraClient::setContrast(int val) {
if (val > -1) {
get(QString("/cgi-bin/ptzctrl.cgi?post_image_value&contrast&%1").arg(val));
}
}
void CameraClient::setSharpness(int val) {
if (val > -1) {
get(QString("/cgi-bin/ptzctrl.cgi?post_image_value&sharpness&%1").arg(val));
}
}
void CameraClient::setHue(int val) {
if (val > -1) {
get(QString("/cgi-bin/ptzctrl.cgi?post_image_value&hue&%1").arg(val));
}
}
void CameraClient::get(QString const&urlExt) {
QUrl const url{QString{m_baseUrl} + urlExt};
QNetworkRequest rqst{url};
auto const reply = m_nam->get(rqst);
connect(reply, &QIODevice::readyRead, reply, &QObject::deleteLater);
}
void CameraClient::post(QString const&urlExt) {
QNetworkRequest const rqst{QUrl{QString{m_baseUrl} + urlExt}};
auto const reply = m_nam->post(rqst, QByteArray{});
connect(reply, &QIODevice::readyRead, reply, &QObject::deleteLater);
}
void CameraClient::poll() {
QUrl const url{QString{m_baseUrl} + "/cgi-bin/param.cgi?get_device_conf"};
QNetworkRequest const 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();
}

60
src/cameraclient.hpp Normal file
View File

@ -0,0 +1,60 @@
/*
* Copyright 2021 - 2024 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 <QNetworkAccessManager>
#include <QObject>
#include <QTimer>
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 setPresetVC(int preset, struct VideoConfig const&vc);
void setPreset(int preset);
void reboot();
public slots:
void setBaseUrl();
private:
void setBrightness(int val);
void setSaturation(int val);
void setContrast(int val);
void setSharpness(int val);
void setHue(int val);
void get(QString const&url);
void post(QString const&url);
void poll();
void handlePollResponse(QNetworkReply *reply);
signals:
void pollUpdate();
void pollFailed();
};

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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
@ -8,4 +8,7 @@
#pragma once
constexpr auto SlideHost = "127.0.0.1";
constexpr auto MaxCameraPresets = 9;
constexpr auto MaxViews = 9;
constexpr auto Version = "1.0.0";

BIN
src/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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
@ -7,12 +7,18 @@
*/
#include <QApplication>
#include <QSettings>
#include "mainwindow.hpp"
int main(int argc, char *argv[]) {
QSettings::setDefaultFormat(QSettings::Format::IniFormat);
#ifndef __APPLE__
QApplication::setStyle("Fusion");
#endif
QApplication a(argc, argv);
QApplication::setApplicationName(QObject::tr("Slide Controller 9000"));
QApplication::setOrganizationName("DrinkingTea");
QApplication::setApplicationName("Slide Controller 9000");
MainWindow w;
w.show();
return QApplication::exec();

View File

@ -1,81 +1,225 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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 <QFormLayout>
#include <QApplication>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QMenuBar>
#include <QMessageBox>
#include <QPushButton>
#include <QStatusBar>
#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"));
const auto mainWidget = new QWidget(this);
const auto rootLyt = new QVBoxLayout;
const auto controlsLayout = new QGridLayout;
const auto slideView = new SlideView(this);
setupMenu();
auto const mainWidget = new QWidget(this);
m_rootLyt = new QVBoxLayout;
auto const controlsLayout = new QGridLayout;
m_slideView = new SlideView(this);
setCentralWidget(mainWidget);
mainWidget->setLayout(rootLyt);
rootLyt->addWidget(slideView);
rootLyt->addLayout(controlsLayout);
mainWidget->setLayout(m_rootLyt);
m_rootLyt->addWidget(m_slideView);
m_rootLyt->addLayout(controlsLayout);
// setup slide controls
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 btnBlankSlides = new QPushButton(tr("Blank Slides (,)"), this);
const auto btnShowSlides = new QPushButton(tr("Show Slides (.)"), this);
controlsLayout->addWidget(btnPrevSlide, 0, 0);
controlsLayout->addWidget(btnNextSlide, 0, 1);
controlsLayout->addWidget(btnPrevSong, 1, 0);
controlsLayout->addWidget(btnNextSong, 1, 1);
controlsLayout->addWidget(btnBlankSlides, 2, 0);
controlsLayout->addWidget(btnShowSlides, 2, 1);
auto const btnPrevSong = new QPushButton(tr("Previous Song"), this);
auto const btnPrevSlide = new QPushButton(tr("Previous Slide"), this);
auto const btnNextSlide = new QPushButton(tr("Next Slide"), this);
auto const btnNextSong = new QPushButton(tr("Next Song"), this);
btnPrevSong->setToolTip(tr("Change to previous song (left arrow key)"));
btnPrevSlide->setToolTip(tr("Change to previous slide (up arrow key)"));
btnNextSong->setToolTip(tr("Change to next song (right arrow key)"));
btnNextSlide->setToolTip(tr("Change to next slide (down arrow key)"));
controlsLayout->addWidget(btnPrevSlide, 0, 1);
controlsLayout->addWidget(btnNextSlide, 0, 2);
controlsLayout->addWidget(btnPrevSong, 0, 0);
controlsLayout->addWidget(btnNextSong, 0, 3);
controlsLayout->setSpacing(2);
btnNextSong->setShortcut(Qt::Key_Right);
btnPrevSong->setShortcut(Qt::Key_Left);
btnNextSlide->setShortcut(Qt::Key_Down);
btnPrevSlide->setShortcut(Qt::Key_Up);
btnBlankSlides->setShortcut(Qt::Key_Comma);
btnShowSlides->setShortcut(Qt::Key_Period);
btnBlankSlides->setToolTip(tr("Also hides slides in OBS"));
btnNextSong->setFixedWidth(135);
btnPrevSong->setFixedWidth(135);
btnNextSlide->setFixedWidth(135);
btnPrevSlide->setFixedWidth(135);
setupViewControls(m_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(btnBlankSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::blankScreen);
connect(btnBlankSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::hideSlides);
connect(btnShowSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::showSlides);
connect(&m_openlpClient, &OpenLPClient::pollUpdate, slideView, &SlideView::pollUpdate);
connect(&m_openlpClient, &OpenLPClient::songListUpdate, slideView, &SlideView::songListUpdate);
connect(&m_openlpClient, &OpenLPClient::slideListUpdate, slideView, &SlideView::slideListUpdate);
connect(&m_openlpClient, &OpenLPClient::pollFailed, slideView, &SlideView::reset);
connect(slideView, &SlideView::songChanged, &m_openlpClient, &OpenLPClient::changeSong);
connect(slideView, &SlideView::slideChanged, &m_openlpClient, &OpenLPClient::changeSlide);
// setup scene selector
const auto btnObsHideSlides = new QPushButton(tr("Hide Slides in OBS (;)"), mainWidget);
const auto btnObsShowSlides = new QPushButton(tr("Show Slides in OBS (')"), mainWidget);
controlsLayout->addWidget(btnObsHideSlides, 3, 0);
controlsLayout->addWidget(btnObsShowSlides, 3, 1);
btnObsHideSlides->setShortcut(Qt::Key_Semicolon);
btnObsShowSlides->setShortcut(Qt::Key_Apostrophe);
btnObsShowSlides->setToolTip(tr("Also shows slides in OpenLP"));
connect(btnObsHideSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::hideSlides);
connect(btnObsShowSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::showSlides);
connect(btnObsShowSlides, &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 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 menu = 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);
menu->addAction(settingsAct);
menu->addAction(quitAct);
}
// slides menu
{
auto const menu = menuBar()->addMenu(tr("&Slides"));
auto const hideSlidesAct = new QAction(tr("&Hide Slides"), this);
hideSlidesAct->setShortcut(Qt::CTRL | Qt::Key_1);
connect(hideSlidesAct, &QAction::triggered, &m_openlpClient, &OpenLPClient::blankScreen);
connect(hideSlidesAct, &QAction::triggered, &m_obsClient, &OBSClient::hideSlides);
menu->addAction(hideSlidesAct);
auto const showSlidesInOpenLpAct = new QAction(tr("Show in &OpenLP Only"), this);
showSlidesInOpenLpAct->setShortcut(Qt::CTRL | Qt::Key_2);
connect(showSlidesInOpenLpAct, &QAction::triggered, &m_openlpClient, &OpenLPClient::showSlides);
connect(showSlidesInOpenLpAct, &QAction::triggered, &m_obsClient, &OBSClient::hideSlides);
menu->addAction(showSlidesInOpenLpAct);
auto const showSlidesAct = new QAction(tr("&Show Slides"), this);
showSlidesAct->setShortcut(Qt::CTRL | Qt::Key_3);
connect(showSlidesAct, &QAction::triggered, &m_obsClient, &OBSClient::showSlides);
connect(showSlidesAct, &QAction::triggered, &m_openlpClient, &OpenLPClient::showSlides);
menu->addAction(showSlidesAct);
}
// camera preset menu
{
auto const menu = menuBar()->addMenu(tr("&Camera"));
for (auto i = 0; i < std::min(9, MaxCameraPresets); ++i) {
auto const cameraPresetAct = new QAction(tr("Camera Preset &%1").arg(i + 1), this);
cameraPresetAct->setShortcut(Qt::ALT | static_cast<Qt::Key>(Qt::Key_1 + i));
connect(cameraPresetAct, &QAction::triggered, &m_cameraClient, [this, i] {
m_cameraClient.setPreset(i + 1);
});
menu->addAction(cameraPresetAct);
}
}
// help menu
{
auto const menu = menuBar()->addMenu(tr("&Help"));
auto const aboutAct = new QAction(tr("&About"), this);
connect(aboutAct, &QAction::triggered, &m_cameraClient, [this] {
QMessageBox about(this);
about.setText(tr(
R"(Slide Controller 9000 - %1
Build date: %2
Copyright 2021 - 2025 Gary Talent (gary@drinkingtea.net)
Slide Controller 9000 is released under the MPL 2.0
Built on Qt library under LGPL 2.0)").arg(Version, __DATE__));
about.exec();
});
menu->addAction(aboutAct);
}
}
void MainWindow::setupViewControlButtons(QVector<View> 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("Show in OpenLP Only"),
.slides = true,
.obsSlides = false,
});
views.emplace_back(View{
.name = tr("Show"),
.slides = true,
.obsSlides = true,
});
}
setupViewControlButtons(views, viewCtlLyt);
}
void MainWindow::openSettings() {
SettingsDialog d(this);
connect(&d, &SettingsDialog::previewPreset, &m_cameraClient, &CameraClient::setPresetVC);
auto const result = d.exec();
if (result == QDialog::Accepted) {
m_cameraClient.setBaseUrl();
m_obsClient.setBaseUrl();
m_openlpClient.setBaseUrl();
setupViewControls(m_rootLyt);
}
}
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() {
@ -107,7 +251,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");
statusBar()->showMessage(openLpStatus + " | " + obsStatus);
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);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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
@ -8,27 +8,46 @@
#pragma once
#include <cstdint>
#include <QMainWindow>
#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 QVBoxLayout *m_rootLyt = nullptr;
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 setupViewControlButtons(QVector<View> const&views, class QGridLayout *rootLyt);
void setupViewControls(class QVBoxLayout *rootLyt);
void openSettings();
void cameraConnectionInit();
void cameraConnectionLost();
void openLpConnectionInit();
void openLpConnectionLost();

View File

@ -1,23 +1,27 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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 <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#include <QUrl>
#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));
}
@ -29,7 +33,7 @@ void OBSClient::hideSlides() {
setScene("SpeakerScene");
}
void OBSClient::setSlidesVisible(int state) {
void OBSClient::setSlidesVisible(bool state) {
if (state) {
setScene("MusicScene");
} else {
@ -37,15 +41,20 @@ void OBSClient::setSlidesVisible(int state) {
}
}
void OBSClient::get(QString urlExt) {
QUrl url(QString(BaseUrl) + urlExt);
void OBSClient::setBaseUrl() {
auto const [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);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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
@ -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();

View File

@ -1,20 +1,24 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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 <QHttpPart>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValueRef>
#include <QNetworkReply>
#include <QSettings>
#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);
@ -24,20 +28,29 @@ OpenLPClient::OpenLPClient(QObject *parent): QObject(parent) {
connect(m_pollingNam, &QNetworkAccessManager::finished, this, &OpenLPClient::handlePollResponse);
}
QString OpenLPClient::getNextSong() {
auto const currentSong = m_songNameMap[m_currentSongId];
auto const songIdx = m_songList.indexOf(currentSong) + 1;
if (songIdx < m_songList.size()) {
return m_songList[songIdx];
}
return "";
}
void OpenLPClient::nextSlide() {
get("/api/controller/live/next");
post("/api/v2/controller/progress", R"({"action":"next"})");
}
void OpenLPClient::prevSlide() {
get("/api/controller/live/previous");
post("/api/v2/controller/progress", R"({"action":"previous"})");
}
void OpenLPClient::nextSong() {
get("/api/service/next");
post("/api/v2/service/progress", R"({"action":"next"})");
}
void OpenLPClient::prevSong() {
get("/api/service/previous");
post("/api/v2/service/progress", R"({"action":"previous"})");
}
void OpenLPClient::blankScreen() {
@ -48,6 +61,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";
@ -60,26 +81,37 @@ void OpenLPClient::changeSlide(int slide) {
get(url);
}
void OpenLPClient::get(QString urlExt) {
QUrl url(QString(BaseUrl) + urlExt);
void OpenLPClient::setBaseUrl() {
auto const [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::post(QString const&url, QString const&data) {
QNetworkRequest rqst(QUrl(m_baseUrl + url));
rqst.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
m_nam->post(rqst, data.toUtf8());
}
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);
}
@ -117,6 +149,7 @@ void OpenLPClient::handlePollResponse(QNetworkReply *reply) {
if (m_currentSongId != songId) {
requestSlideList();
m_currentSongId = songId;
emit songChanged(songId);
}
emit pollUpdate(m_songNameMap[songId], slide);
}
@ -130,18 +163,18 @@ void OpenLPClient::handleSongListResponse(QNetworkReply *reply) {
if (data.isEmpty()) {
return;
}
QStringList songList;
auto doc = QJsonDocument::fromJson(data);
auto items = doc.object()["results"].toObject()["items"].toArray();
m_songNameMap.clear();
for (const auto &item : items) {
m_songList.clear();
for (auto const &item : items) {
auto song = item.toObject();
auto name = song["title"].toString();
auto id = song["id"].toString();
m_songNameMap[id] = name;
songList.push_back(name);
m_songList.push_back(name);
}
emit songListUpdate(songList);
emit songListUpdate(m_songList);
}
void OpenLPClient::handleSlideListResponse(QNetworkReply *reply) {
@ -157,12 +190,12 @@ void OpenLPClient::handleSlideListResponse(QNetworkReply *reply) {
QStringList tagList;
auto doc = QJsonDocument::fromJson(data);
auto items = doc.object()["results"].toObject()["slides"].toArray();
for (const auto &item : items) {
auto slide = item.toObject();
for (auto const&item : items) {
auto const slide = item.toObject();
auto text = slide["text"].toString();
auto tag = slide["tag"].toString();
slideList.push_back(text);
tagList.push_back(tag);
slideList.push_back(std::move(text));
tagList.push_back(std::move(tag));
}
emit slideListUpdate(tagList, slideList);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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
@ -19,23 +19,23 @@ 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);
QNetworkAccessManager *m_slideListNam = new QNetworkAccessManager(this);
QTimer m_pollTimer;
QHash<QString, QString> m_songNameMap;
QStringList m_songList;
int m_currentServiceId = -1;
QString m_currentSongId;
public:
explicit OpenLPClient(QObject *parent = nullptr);
[[nodiscard]]
QString getNextSong();
public slots:
void nextSlide();
@ -49,12 +49,18 @@ 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 post(QString const&url, QString const&data);
void requestSongList();
@ -80,5 +86,6 @@ class OpenLPClient: public QObject {
void slideListUpdate(QStringList, QStringList);
void songChanged(QString);
};

BIN
src/sc9k.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

1
src/sc9k.rc Normal file
View File

@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "sc9k.ico"

188
src/settingsdata.cpp Normal file
View File

@ -0,0 +1,188 @@
/*
* Copyright 2021 - 2024 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 <QSettings>
#include "consts.hpp"
#include "settingsdata.hpp"
void setVideoConfig(QSettings &settings, QVector<VideoConfig> const&vcList) {
settings.beginGroup("Camera");
settings.beginWriteArray("VideoImageConfig");
for (auto i = 0; auto const&vc : vcList) {
settings.setArrayIndex(i);
settings.setValue("brightness", vc.brightness);
settings.setValue("saturation", vc.saturation);
settings.setValue("contrast", vc.contrast);
settings.setValue("sharpness", vc.sharpness);
settings.setValue("hue", vc.hue);
++i;
}
settings.endArray();
settings.endGroup();
}
void setVideoConfig(QVector<VideoConfig> const&vcList) {
QSettings s;
setVideoConfig(s, vcList);
}
QVector<VideoConfig> getVideoConfig(QSettings &settings) {
QVector<VideoConfig> vc(MaxCameraPresets);
settings.beginGroup("Camera");
auto const size = std::min(settings.beginReadArray("VideoImageConfig"), MaxCameraPresets);
for (auto i = 0; i < size; ++i) {
settings.setArrayIndex(i);
vc[i] = {
.brightness = settings.value("brightness").toInt(),
.saturation = settings.value("saturation").toInt(),
.contrast = settings.value("contrast").toInt(),
.sharpness = settings.value("sharpness").toInt(),
.hue = settings.value("hue").toInt(),
};
}
settings.endArray();
settings.endGroup();
return vc;
}
QVector<VideoConfig> getVideoConfig() {
QSettings s;
return getVideoConfig(s);
}
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<uint16_t>(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<uint16_t>(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<uint16_t>(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<View> 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<View> const&views) {
QSettings s;
return setViews(s, views);
}
QVector<View> getViews(QSettings &settings) {
QVector<View> out;
settings.beginGroup("Views");
auto const 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<View> getViews() {
QSettings s;
return getViews(s);
}

76
src/settingsdata.hpp Normal file
View File

@ -0,0 +1,76 @@
/*
* Copyright 2021 - 2024 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 <cstdint>
#include <QString>
#include <QVector>
struct VideoConfig {
int brightness = 6;
int saturation = 4;
int contrast = 8;
int sharpness = 3;
int hue = 7;
};
void setVideoConfig(class QSettings &settings, QVector<VideoConfig> const&vc);
void setVideoConfig(QVector<VideoConfig> const&vc);
QVector<VideoConfig> getVideoConfig(class QSettings &settings);
QVector<VideoConfig> getVideoConfig();
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<View> const&views);
void setViews(QVector<View> const&views);
[[nodiscard]]
QVector<View> getViews(class QSettings &settings);
[[nodiscard]]
QVector<View> getViews();

313
src/settingsdialog.cpp Normal file
View File

@ -0,0 +1,313 @@
/*
* Copyright 2021 - 2024 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 <QCheckBox>
#include <QComboBox>
#include <QFormLayout>
#include <QHeaderView>
#include <QIntValidator>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSettings>
#include <QSpacerItem>
#include <QSpinBox>
#include <QTabWidget>
#include <QTableWidget>
#include <QVBoxLayout>
#include "consts.hpp"
#include "settingsdialog.hpp"
enum ViewColumn {
Name = 0,
Slides,
ObsSlides,
CameraPreset,
Count
};
SettingsDialog::SettingsDialog(QWidget *parent): QDialog(parent) {
auto const lyt = new QVBoxLayout(this);
auto const tabs = new QTabWidget(this);
lyt->addWidget(tabs);
tabs->addTab(setupViewConfig(tabs), tr("&Views"));
tabs->addTab(setupImageConfig(tabs), tr("&Image"));
tabs->addTab(setupNetworkInputs(tabs), tr("&Network"));
lyt->addWidget(setupButtons(this));
setFixedSize(440, 440);
}
QWidget *SettingsDialog::setupNetworkInputs(QWidget *parent) {
auto const root = new QWidget(parent);
auto const lyt = new QFormLayout(root);
auto const portValidator = new QIntValidator(1, 65536, this);
QSettings settings;
// camera settings
{
auto const c = getCameraConnectionData(settings);
m_cameraHostLe = new QLineEdit(root);
m_cameraPortLe = new QLineEdit(root);
m_cameraHostLe->setText(c.host);
m_cameraPortLe->setText(QString::number(c.port));
m_cameraPortLe->setValidator(portValidator);
lyt->addRow(tr("Camera &Host:"), m_cameraHostLe);
lyt->addRow(tr("Ca&mera Port:"), m_cameraPortLe);
}
// OpenLP settings
{
auto const c = getOpenLPConnectionData(settings);
m_openLpHostLe = new QLineEdit(root);
m_openLpPortLe = new QLineEdit(root);
m_openLpHostLe->setText(c.host);
m_openLpPortLe->setText(QString::number(c.port));
m_openLpPortLe->setValidator(portValidator);
lyt->addRow(tr("Op&enLP Host:"), m_openLpHostLe);
lyt->addRow(tr("Open&LP Port:"), m_openLpPortLe);
}
// OBS settings
{
auto const c = getOBSConnectionData(settings);
m_obsHostLe = new QLineEdit(root);
m_obsPortLe = new QLineEdit(root);
m_obsHostLe->setText(c.host);
m_obsPortLe->setText(QString::number(c.port));
m_obsPortLe->setValidator(portValidator);
lyt->addRow(tr("O&BS Host:"), m_obsHostLe);
lyt->addRow(tr("OB&S Port:"), m_obsPortLe);
}
return root;
}
QWidget *SettingsDialog::setupImageConfig(QWidget *parent) {
auto const root = new QWidget(parent);
auto const lyt = new QVBoxLayout(root);
{
auto const formRoot = new QWidget(parent);
auto const formLyt = new QFormLayout(formRoot);
lyt->addWidget(formRoot);
m_videoConfig = getVideoConfig();
auto const mkSb = [parent, formLyt](QString const&lbl) {
auto const s = new QSpinBox(parent);
s->setAlignment(Qt::AlignRight);
s->setRange(0, 14);
formLyt->addRow(lbl, s);
return s;
};
auto const presetNo = new QComboBox(parent);
connect(presetNo, &QComboBox::currentIndexChanged, this, &SettingsDialog::updateVidConfigPreset);
for (auto i = 0; i < MaxCameraPresets; ++i) {
presetNo->addItem(tr("Camera Preset %1").arg(i + 1));
}
formLyt->addRow(presetNo);
m_vidBrightness = mkSb(tr("&Brightness:"));
m_vidSaturation = mkSb(tr("&Saturation:"));
m_vidContrast = mkSb(tr("Con&trast:"));
m_vidSharpness = mkSb(tr("Sharpn&ess:"));
m_vidHue = mkSb(tr("&Hue:"));
updateVidConfigPreset(0);
}
{
auto const btnRoot = new QWidget(parent);
auto const btnLyt = new QHBoxLayout(btnRoot);
lyt->addWidget(btnRoot);
btnLyt->setAlignment(Qt::AlignRight);
auto const previewBtn = new QPushButton(tr("&Preview"), btnRoot);
btnLyt->addWidget(previewBtn);
connect(previewBtn, &QPushButton::clicked, this, [this] {
this->collectVideoConfig();
auto const &vc = m_videoConfig[m_vidCurrentPreset];
emit previewPreset(m_vidCurrentPreset + 1, vc);
});
}
return root;
}
QWidget *SettingsDialog::setupViewConfig(QWidget *parent) {
auto const root = new QWidget(parent);
auto const lyt = new QVBoxLayout(root);
auto const btnsRoot = new QWidget(root);
m_viewTable = new QTableWidget(root);
lyt->addWidget(btnsRoot);
lyt->addWidget(m_viewTable);
{ // table
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<int>(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, 75);
m_viewTable->setColumnWidth(3, 70);
hdr->setStretchLastSection(true);
}
{ // add/removes buttons
auto const btnsLyt = new QHBoxLayout(btnsRoot);
auto const addBtn = new QPushButton("A&dd", btnsRoot);
auto const rmBtn = new QPushButton("&Remove", btnsRoot);
addBtn->setFixedWidth(70);
rmBtn->setFixedWidth(70);
rmBtn->setDisabled(true);
btnsLyt->addWidget(addBtn);
btnsLyt->addWidget(rmBtn);
btnsLyt->setAlignment(Qt::AlignLeft);
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);
});
auto const views = getViews();
m_viewTable->setRowCount(static_cast<int>(views.size()));
for (auto row = 0; auto const&view : views) {
setupViewRow(row, view);
++row;
}
}
return root;
}
QWidget *SettingsDialog::setupButtons(QWidget *parent) {
auto const root = new QWidget(parent);
auto const lyt = new QHBoxLayout(root);
m_errLbl = new QLabel(root);
auto const okBtn = new QPushButton(tr("&OK"), root);
auto const applyBtn = new QPushButton(tr("&Apply"), root);
auto const 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(applyBtn);
lyt->addWidget(cancelBtn);
connect(okBtn, &QPushButton::clicked, this, &SettingsDialog::handleOK);
connect(applyBtn, &QPushButton::clicked, this, &SettingsDialog::handleApply);
connect(cancelBtn, &QPushButton::clicked, this, &SettingsDialog::reject);
return root;
}
int SettingsDialog::handleApply() {
QSettings settings;
QVector<View> views;
auto const viewsErr = collectViews(views);
if (viewsErr) {
return -1;
}
setViews(settings, views);
setCameraConnectionData(settings, {
.host = m_cameraHostLe->text(),
.port = m_cameraPortLe->text().toUShort(),
});
setOpenLPConnectionData(settings, {
.host = m_openLpHostLe->text(),
.port = m_openLpPortLe->text().toUShort(),
});
setOBSConnectionData(settings, {
.host = m_obsHostLe->text(),
.port = m_obsPortLe->text().toUShort(),
});
collectVideoConfig();
setVideoConfig(settings, m_videoConfig);
return 0;
}
void SettingsDialog::handleOK() {
if (handleApply() == 0) {
accept();
}
}
void SettingsDialog::setupViewRow(int row, View const&view) {
// name
auto const nameItem = new QTableWidgetItem(view.name);
m_viewTable->setItem(row, ViewColumn::Name, nameItem);
// slides
auto const slidesCb = new QCheckBox(m_viewTable);
slidesCb->setChecked(view.slides);
m_viewTable->setCellWidget(row, ViewColumn::Slides, slidesCb);
// obs slides
auto const obsSlidesCb = new QCheckBox(m_viewTable);
obsSlidesCb->setChecked(view.obsSlides);
m_viewTable->setCellWidget(row, ViewColumn::ObsSlides, obsSlidesCb);
// camera preset
auto const presetItem = new QTableWidgetItem(QString::number(view.cameraPreset));
m_viewTable->setItem(row, ViewColumn::CameraPreset, presetItem);
}
int SettingsDialog::collectViews(QVector<View> &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;
}
auto const cameraPreset = m_viewTable->item(row, ViewColumn::CameraPreset)->text().toInt(&ok);
if (!ok || cameraPreset < 1 || cameraPreset > MaxCameraPresets) {
m_errLbl->setText(tr("View %1 has invalid preset (1-%2)").arg(viewNo).arg(MaxCameraPresets));
return 2;
}
views.emplace_back(View{
.name = name,
.slides = dynamic_cast<QCheckBox*>(m_viewTable->cellWidget(row, ViewColumn::Slides))->isChecked(),
.obsSlides = dynamic_cast<QCheckBox*>(m_viewTable->cellWidget(row, ViewColumn::ObsSlides))->isChecked(),
.cameraPreset = cameraPreset,
});
}
return 0;
}
void SettingsDialog::collectVideoConfig() {
auto &vc = m_videoConfig[m_vidCurrentPreset];
auto constexpr getVal = [](int &val, QSpinBox *src) {
if (src) {
val = src->value();
}
};
getVal(vc.brightness, m_vidBrightness);
getVal(vc.saturation, m_vidSaturation);
getVal(vc.contrast, m_vidContrast);
getVal(vc.sharpness, m_vidSharpness);
getVal(vc.hue, m_vidHue);
}
void SettingsDialog::updateVidConfigPreset(int preset) {
// update to new value
auto constexpr setVal = [](int val, QSpinBox *dst) {
if (dst) {
dst->setValue(val);
}
};
auto const&vc = m_videoConfig[preset];
setVal(vc.brightness, m_vidBrightness);
setVal(vc.saturation, m_vidSaturation);
setVal(vc.contrast, m_vidContrast);
setVal(vc.sharpness, m_vidSharpness);
setVal(vc.hue, m_vidHue);
m_vidCurrentPreset = preset;
}
void SettingsDialog::updateVidConfigPresetCollect(int preset) {
collectVideoConfig();
updateVidConfigPreset(preset);
}

56
src/settingsdialog.hpp Normal file
View File

@ -0,0 +1,56 @@
/*
* Copyright 2021 - 2024 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 <QDialog>
#include "consts.hpp"
#include "settingsdata.hpp"
class SettingsDialog: public QDialog {
Q_OBJECT
private:
QVector<VideoConfig> m_videoConfig = QVector<VideoConfig>(MaxCameraPresets);
class QLabel *m_errLbl = nullptr;
class QLineEdit *m_cameraHostLe = nullptr;
class QLineEdit *m_cameraPortLe = nullptr;
class QLineEdit *m_openLpHostLe = nullptr;
class QLineEdit *m_openLpPortLe = nullptr;
class QLineEdit *m_obsHostLe = nullptr;
class QLineEdit *m_obsPortLe = nullptr;
class QSpinBox *m_vidBrightness = nullptr;
class QSpinBox *m_vidSaturation = nullptr;
class QSpinBox *m_vidContrast = nullptr;
class QSpinBox *m_vidSharpness = nullptr;
class QSpinBox *m_vidHue = nullptr;
int m_vidCurrentPreset = 0;
class QTableWidget *m_viewTable = nullptr;
public:
explicit SettingsDialog(QWidget *parent);
private:
QWidget *setupNetworkInputs(QWidget *parent);
QWidget *setupViewConfig(QWidget *parent);
QWidget *setupImageConfig(QWidget *parent);
QWidget *setupButtons(QWidget *parent);
[[nodiscard]]
int handleApply();
void handleOK();
void setupViewRow(int row, View const&view = {});
/**
* Gets views from table.
* @return error code
*/
[[nodiscard("Must check error code")]]
int collectViews(QVector<View> &views) const;
void collectVideoConfig();
void updateVidConfigPreset(int preset);
void updateVidConfigPresetCollect(int preset);
signals:
void previewPreset(int, VideoConfig const&);
};

View File

@ -1,22 +1,22 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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 <QComboBox>
#include <QDebug>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QListWidget>
#include <QTableWidget>
#include <QVBoxLayout>
#include "slideview.hpp"
SlideView::SlideView(QWidget *parent): QWidget(parent) {
auto lyt = new QVBoxLayout(this);
m_songSelector = new QComboBox(this);
auto lyt = new QHBoxLayout(this);
m_songSelector = new QListWidget(this);
m_slideTable = new QTableWidget(this);
auto header = m_slideTable->horizontalHeader();
header->setVisible(false);
@ -28,15 +28,29 @@ SlideView::SlideView(QWidget *parent): QWidget(parent) {
#ifndef _WIN32
m_slideTable->setAlternatingRowColors(true);
#endif
lyt->addWidget(m_songSelector);
lyt->addWidget(m_slideTable);
lyt->addWidget(m_songSelector);
connect(m_slideTable, &QTableWidget::currentCellChanged, this, &SlideView::slideChanged);
}
void SlideView::pollUpdate(QString songName, int slide) {
if (songName != m_currentSong) {
QString SlideView::getNextSong() const {
auto const cnt = m_songSelector->count();
auto const idx = m_songSelector->currentRow() + 1;
if (idx < cnt) {
return m_songSelector->currentItem()->text();
}
return "";
}
void SlideView::pollUpdate(QString const&songName, int slide) {
auto const songItems = m_songSelector->findItems(songName, Qt::MatchFixedString);
if (songItems.empty()) {
return;
}
auto songItem = songItems.first();
if (songItem != m_songSelector->currentItem()) {
m_currentSong = songName;
m_songSelector->setCurrentText(songName);
m_songSelector->setCurrentItem(songItem);
}
if (slide != m_currentSlide) {
m_currentSlide = slide;
@ -45,16 +59,23 @@ void SlideView::pollUpdate(QString songName, int slide) {
}
void SlideView::changeSong(int song) {
if (m_songSelector->currentText() != m_currentSong) {
if (song < 0) {
return;
}
auto const songItem = m_songSelector->item(song);
if (songItem && songItem->text() != m_currentSong) {
emit songChanged(song);
}
}
void SlideView::slideListUpdate(QStringList tagList, QStringList slideList) {
void SlideView::slideListUpdate(QStringList tagList, QStringList const&slideList) {
for (auto &tag : tagList) {
tag = tag.split("").join("\n");
}
m_currentSlide = 0;
m_slideTable->setRowCount(slideList.size());
m_slideTable->setRowCount(static_cast<int>(slideList.size()));
for (int i = 0; i < slideList.size(); ++i) {
auto txt = slideList[i];
auto const& txt = slideList[i];
auto item = new QTableWidgetItem(txt);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
m_slideTable->setItem(i, 0, item);
@ -70,16 +91,16 @@ 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.
auto isReplacement = m_songSelector->count() > 0;
disconnect(m_songSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(changeSong(int)));
disconnect(m_songSelector, &QListWidget::currentRowChanged, this, &SlideView::changeSong);
m_songSelector->clear();
m_songSelector->addItems(songList);
if (isReplacement) {
changeSong(0);
}
connect(m_songSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(changeSong(int)));
connect(m_songSelector, &QListWidget::currentRowChanged, this, &SlideView::changeSong);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 gary@drinkingtea.net
* Copyright 2021 - 2024 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
@ -13,18 +13,22 @@ class SlideView: public QWidget {
Q_OBJECT
private:
class QTableWidget *m_slideTable = nullptr;
class QComboBox *m_songSelector = nullptr;
class QListWidget *m_songSelector = nullptr;
QString m_currentSong;
int m_currentSlide = -1;
public:
explicit SlideView(QWidget *parent = nullptr);
[[nodiscard]]
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 tagList, QStringList const&songList);
void reset();