mirror of
				https://github.com/gtalent/sc9k.git
				synced 2025-10-27 05:09:09 -05:00 
			
		
		
		
	Compare commits
	
		
			33 Commits
		
	
	
		
			release-1.
			...
			release-1.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e42c3a7dc8 | |||
| 1d66bcc3bb | |||
| efab455a24 | |||
| 834a36c417 | |||
| 59aee596e9 | |||
| b96b75a735 | |||
| 215f9b4d1d | |||
| 4957bec1c6 | |||
| ef24f01566 | |||
| d0eaad7dfe | |||
| fff098d80e | |||
| ec18a0c507 | |||
| 88da18a380 | |||
| 0db9bff0de | |||
| a0a1cd8af1 | |||
| f4e0b5ab9f | |||
| bb825f947e | |||
| b71a64ca33 | |||
| 048b06db97 | |||
| 344cc4f819 | |||
| ae580a0d58 | |||
| ac7bb9c585 | |||
| 56f98eed60 | |||
| 9171a080a7 | |||
| 8a4989923c | |||
| 77df4257c5 | |||
| 87997e8f18 | |||
| 8bdad3ed68 | |||
| b7aba1180d | |||
| ed01f44b91 | |||
| b119268396 | |||
| bc342a290f | |||
| 814cef4a2e | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -2,6 +2,7 @@ | ||||
| .clangd | ||||
| .conanbuild | ||||
| .current_build | ||||
| .idea | ||||
| __pycache__ | ||||
| CMakeLists.txt.user | ||||
| Session.vim | ||||
|   | ||||
							
								
								
									
										7
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" /> | ||||
|   <component name="CompDBWorkspace"> | ||||
|     <contentRoot DIR="$PROJECT_DIR$" /> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="VcsDirectoryMappings"> | ||||
|     <mapping directory="" vcs="Git" /> | ||||
|   </component> | ||||
| </project> | ||||
| @@ -2,7 +2,7 @@ | ||||
| source: | ||||
| - . | ||||
| copyright_notice: |- | ||||
|   Copyright 2021 - 2023 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 | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
							
								
								
									
										10
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Makefile
									
									
									
									
									
								
							| @@ -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 | ||||
| debug: build | ||||
| 	${DEBUGGER} ${PROJECT_EXECUTABLE} | ||||
|   | ||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								README.md
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										196
									
								
								deps/buildcore/base.mk
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										196
									
								
								deps/buildcore/base.mk
									
									
									
									
										vendored
									
									
								
							| @@ -9,89 +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 | ||||
|  | ||||
| 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} | ||||
| 	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 | ||||
| 	endif | ||||
| endif | ||||
|  | ||||
| ifneq ($(shell ${ENV_RUN} which python3 2> /dev/null),) | ||||
| 	PYTHON3=python3 | ||||
| else | ||||
| 	ifeq ($(shell ${ENV_RUN} python -c 'import sys; print(sys.version_info[0])'),3) | ||||
| 		PYTHON3=python | ||||
|  | ||||
| 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 | ||||
|  | ||||
| SCRIPTS=${BUILDCORE_PATH}/scripts | ||||
| SETUP_BUILD=${PYTHON3} ${SCRIPTS}/setup-build.py | ||||
| PYBB=${PYTHON3} ${SCRIPTS}/pybb.py | ||||
| CMAKE_BUILD=${PYBB} cmake-build | ||||
| GET_ENV=${PYBB} getenv | ||||
| CTEST=${PYBB} ctest-all | ||||
| RM_RF=${PYBB} rm | ||||
| HOST=$(shell ${PYBB} hostname) | ||||
| BUILDCORE_HOST_SPECIFIC_BUILDPATH=$(shell ${GET_ENV} BUILDCORE_HOST_SPECIFIC_BUILDPATH) | ||||
| ifneq (${BUILDCORE_HOST_SPECIFIC_BUILDPATH},) | ||||
| BUILD_PATH=build/${HOST} | ||||
| else | ||||
| BUILD_PATH=build | ||||
| endif | ||||
| ifdef USE_VCPKG | ||||
| 	ifndef VCPKG_DIR_BASE | ||||
| 		VCPKG_DIR_BASE=.vcpkg | ||||
| 	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 | ||||
| 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) | ||||
|  | ||||
| VCPKG_DIR=$(VCPKG_DIR_BASE)/$(VCPKG_VERSION)-$(HOST_ENV) | ||||
| CURRENT_BUILD=$(HOST_ENV)-$(shell ${ENV_RUN} ${PYBB} 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_PATH} | ||||
| 	${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} | ||||
| .PHONY: install | ||||
| install: | ||||
| 	${ENV_RUN} ${CMAKE_BUILD} ${BUILD_PATH} install | ||||
| 	${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} install | ||||
| .PHONY: clean | ||||
| clean: | ||||
| 	${ENV_RUN} ${CMAKE_BUILD} ${BUILD_PATH} clean | ||||
| 	${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} clean | ||||
| .PHONY: purge | ||||
| purge: | ||||
| 	${ENV_RUN} ${RM_RF} .current_build | ||||
| 	${ENV_RUN} ${RM_RF} ${BUILD_PATH} | ||||
| 	${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} mypy ${SCRIPTS} | ||||
| 	${ENV_RUN} ${CMAKE_BUILD} ${BUILD_PATH} test | ||||
| 	${BC_CMD_ENVRUN} mypy ${BC_VAR_SCRIPTS} | ||||
| 	${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} test | ||||
| .PHONY: test-verbose | ||||
| test-verbose: build | ||||
| 	${ENV_RUN} ${CTEST} ${BUILD_PATH} --output-on-failure | ||||
| 	${BC_CMD_CTEST} ${BC_VAR_BUILD_PATH} --output-on-failure | ||||
| .PHONY: test-rerun-verbose | ||||
| test-rerun-verbose: build | ||||
| 	${ENV_RUN} ${CTEST} ${BUILD_PATH} --rerun-failed --output-on-failure | ||||
| 	${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 \ | ||||
| @@ -103,73 +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} | ||||
| ifdef ENV_RUN | ||||
| 	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 USE_VCPKG | ||||
| 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) | ||||
|  | ||||
| .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 ifdef USE_CONAN # USE_VCPKG ################################################ | ||||
|  | ||||
| else ifdef USE_CONAN # USE_VCPKG / USE_CONAN #################################### | ||||
| .PHONY: setup-conan | ||||
| conan-config: | ||||
| 	${ENV_RUN} conan profile new ${PROJECT_NAME} --detect --force | ||||
| ifeq ($(OS),linux) | ||||
| 	${ENV_RUN} 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 | ||||
| 	${ENV_RUN} conan profile update settings.compiler.cppstd=20 ${PROJECT_NAME} | ||||
| ifeq ($(OS),windows) | ||||
| 	${ENV_RUN} conan profile update settings.compiler.runtime=static ${PROJECT_NAME} | ||||
| 	${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: | ||||
| 	${ENV_RUN} ${PYBB} conan-install ${PROJECT_NAME} | ||||
| endif # USE_VCPKG ############################################### | ||||
| 	${BC_CMD_PYBB} conan-install ${BC_VAR_PROJECT_NAME} | ||||
| endif # USE_CONAN ############################################### | ||||
|  | ||||
| ifeq (${OS},darwin) | ||||
| ifeq (${BC_VAR_OS},darwin) | ||||
| .PHONY: configure-xcode | ||||
| configure-xcode: | ||||
| 	${ENV_RUN} ${SETUP_BUILD} ${VCPKG_TOOLCHAIN} --build_tool=xcode --current_build=0 --build_root=${BUILD_PATH} | ||||
| 	${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 --build_root=${BUILD_PATH} | ||||
| 	${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 --build_root=${BUILD_PATH} | ||||
| 	${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 --build_root=${BUILD_PATH} | ||||
| 	${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
									
								
							
							
						
						
									
										28
									
								
								deps/buildcore/scripts/file_to_c.py
									
									
									
									
										vendored
									
									
										Normal 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) | ||||
							
								
								
									
										75
									
								
								deps/buildcore/scripts/pybb.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										75
									
								
								deps/buildcore/scripts/pybb.py
									
									
									
									
										vendored
									
									
								
							| @@ -18,18 +18,20 @@ import subprocess | ||||
| import sys | ||||
| from typing import List, Optional | ||||
|  | ||||
|  | ||||
| def mkdir(path: str): | ||||
|     if not os.path.exists(path): | ||||
|         os.mkdir(path) | ||||
| import util | ||||
|  | ||||
|  | ||||
| # this exists because Windows is utterly incapable of providing a proper rm -rf | ||||
| def rm(path: str): | ||||
|     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 mkdir(path: str) -> int: | ||||
|     try: | ||||
|         util.mkdir_p(path) | ||||
|     except Exception: | ||||
|         return 1 | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def rm_multi(paths: List[str]): | ||||
|     for path in paths: | ||||
|         util.rm(path) | ||||
|  | ||||
|  | ||||
| def ctest_all() -> int: | ||||
| @@ -70,16 +72,13 @@ def conan() -> int: | ||||
|     err = 0 | ||||
|     try: | ||||
|         mkdir(conan_dir) | ||||
|     except: | ||||
|     except Exception: | ||||
|         return 1 | ||||
|     if err != 0: | ||||
|         return err | ||||
|     args = ['conan', 'install', '../', '--build=missing', '-pr', project_name] | ||||
|     os.chdir(conan_dir) | ||||
|     err = subprocess.run(args).returncode | ||||
|     if err != 0: | ||||
|         return err | ||||
|     return 0 | ||||
|     return subprocess.run(args).returncode | ||||
|  | ||||
|  | ||||
| def cat(paths: List[str]) -> int: | ||||
| @@ -87,48 +86,70 @@ def cat(paths: List[str]) -> int: | ||||
|         try: | ||||
|             with open(path) as f: | ||||
|                 data = f.read() | ||||
|                 sys.stdout.write(data) | ||||
|                 print(data) | ||||
|         except FileNotFoundError: | ||||
|             sys.stderr.write('cat: {}: no such file or directory\n'.format(path)) | ||||
|             sys.stderr.write(f'cat: {path}: no such file or directory\n') | ||||
|             return 1 | ||||
|     sys.stdout.write('\n') | ||||
|     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 | ||||
|     sys.stdout.write(os.environ[var_name]) | ||||
|     print(os.environ[var_name]) | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def hostname() -> int: | ||||
|     sys.stdout.write(platform.node()) | ||||
|     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': | ||||
|         try: | ||||
|             mkdir(sys.argv[2]) | ||||
|         except: | ||||
|             err = 1 | ||||
|         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) | ||||
|         err = cmake_build(sys.argv[2], clarg(3)) | ||||
|     elif sys.argv[1] == 'cat': | ||||
|         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 | ||||
|   | ||||
							
								
								
									
										54
									
								
								deps/buildcore/scripts/setup-build.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										54
									
								
								deps/buildcore/scripts/setup-build.py
									
									
									
									
										vendored
									
									
								
							| @@ -15,18 +15,35 @@ import shutil | ||||
| import subprocess | ||||
| import sys | ||||
|  | ||||
| from pybb import mkdir, rm | ||||
| import util | ||||
|  | ||||
|  | ||||
| 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('--build_root', help='Path to the root of build directories (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) | ||||
|     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': | ||||
| @@ -64,10 +81,10 @@ def main() -> int: | ||||
|         return 1 | ||||
|  | ||||
|     project_dir = os.getcwd() | ||||
|     build_dir = '{:s}/{:s}/{:s}'.format(project_dir, args.build_root, build_config) | ||||
|     rm(build_dir) | ||||
|     build_dir = f'{project_dir}/{args.build_root}/{build_config}' | ||||
|     util.rm(build_dir) | ||||
|     cmake_cmd = [ | ||||
|         'cmake', '-S', project_dir, '-B', build_dir, build_tool, | ||||
|         '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), | ||||
| @@ -75,22 +92,27 @@ def main() -> int: | ||||
|         '-DBUILDCORE_BUILD_CONFIG={:s}'.format(build_config), | ||||
|         '-DBUILDCORE_TARGET={:s}'.format(args.target), | ||||
|     ] | ||||
|     if build_tool != '': | ||||
|         cmake_cmd.append(build_tool) | ||||
|     if qt_path != '': | ||||
|         cmake_cmd.append(qt_path) | ||||
|     if platform.system() == 'Windows': | ||||
|     if platform.system() == 'Windows' and platform.system() == 'AMD64': | ||||
|         cmake_cmd.append('-A x64') | ||||
|  | ||||
|     subprocess.run(cmake_cmd) | ||||
|     cmake_err = subprocess.run(cmake_cmd).returncode | ||||
|     if cmake_err != 0: | ||||
|         return cmake_err | ||||
|  | ||||
|     mkdir('dist') | ||||
|     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('{:s}/compile_commands.json'.format(build_dir), 'compile_commands.json') | ||||
|         os.symlink(f'{build_dir}/compile_commands.json', | ||||
|                    'compile_commands.json') | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										38
									
								
								deps/buildcore/scripts/util.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								deps/buildcore/scripts/util.py
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										
											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
									
								
							
							
						
						
									
										
											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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								iconsrc/icon-32.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.1 KiB | 
| @@ -3,7 +3,7 @@ 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( | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -9,6 +9,7 @@ | ||||
| #include <QNetworkReply> | ||||
| #include <QSettings> | ||||
|  | ||||
| #include "consts.hpp" | ||||
| #include "settingsdata.hpp" | ||||
| #include "cameraclient.hpp" | ||||
|  | ||||
| @@ -19,27 +20,85 @@ CameraClient::CameraClient(QObject *parent): QObject(parent) { | ||||
| 	connect(m_pollingNam, &QNetworkAccessManager::finished, this, &CameraClient::handlePollResponse); | ||||
| } | ||||
|  | ||||
| void CameraClient::setPreset(int preset) { | ||||
| 	if (preset > -1) { | ||||
| 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() { | ||||
| 	const auto [host, port] = getCameraConnectionData(); | ||||
| 	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 url(QString(m_baseUrl) + urlExt); | ||||
| 	QNetworkRequest rqst(url); | ||||
| 	auto reply = m_nam->get(rqst); | ||||
| 	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 url(QString(m_baseUrl) + "/cgi-bin/param.cgi?get_device_conf"); | ||||
| 	QNetworkRequest rqst(url); | ||||
| 	QUrl const url{QString{m_baseUrl} + "/cgi-bin/param.cgi?get_device_conf"}; | ||||
| 	QNetworkRequest const rqst{url}; | ||||
| 	m_pollingNam->get(rqst); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -12,8 +12,6 @@ | ||||
| #include <QObject> | ||||
| #include <QTimer> | ||||
|  | ||||
| #include "consts.hpp" | ||||
|  | ||||
| class CameraClient: public QObject { | ||||
| 	Q_OBJECT | ||||
| 	private: | ||||
| @@ -25,14 +23,30 @@ class CameraClient: public QObject { | ||||
| 	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); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -10,4 +10,5 @@ | ||||
|  | ||||
| constexpr auto MaxCameraPresets = 9; | ||||
| constexpr auto MaxViews = 9; | ||||
| constexpr auto Version = "1.0-beta5"; | ||||
|  | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								src/icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/icon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.5 KiB | 
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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,8 +13,12 @@ | ||||
|  | ||||
| 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(); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -22,19 +22,19 @@ MainWindow::MainWindow(QWidget *parent): QMainWindow(parent) { | ||||
| 	setFixedSize(610, 555); | ||||
| 	setWindowTitle(tr("Slide Controller 9000")); | ||||
| 	setupMenu(); | ||||
| 	const auto mainWidget = new QWidget(this); | ||||
| 	auto const mainWidget = new QWidget(this); | ||||
| 	m_rootLyt = new QVBoxLayout; | ||||
| 	const auto controlsLayout = new QGridLayout; | ||||
| 	auto const controlsLayout = new QGridLayout; | ||||
| 	m_slideView = new SlideView(this); | ||||
| 	setCentralWidget(mainWidget); | ||||
| 	mainWidget->setLayout(m_rootLyt); | ||||
| 	m_rootLyt->addWidget(m_slideView); | ||||
| 	m_rootLyt->addLayout(controlsLayout); | ||||
| 	// setup slide controls | ||||
| 	const auto btnPrevSong = new QPushButton(tr("Previous Song"), this); | ||||
| 	const auto btnPrevSlide = new QPushButton(tr("Previous Slide"), this); | ||||
| 	const auto btnNextSlide = new QPushButton(tr("Next Slide"), this); | ||||
| 	const auto btnNextSong = new QPushButton(tr("Next Song"), this); | ||||
| 	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)")); | ||||
| @@ -112,8 +112,8 @@ void MainWindow::setupMenu() { | ||||
| 	} | ||||
| 	// camera preset menu | ||||
| 	{ | ||||
| 		auto const menu = menuBar()->addMenu(tr("&Camera Preset")); | ||||
| 		for (auto i = 0; i < MaxCameraPresets; ++i) { | ||||
| 		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] { | ||||
| @@ -121,6 +121,18 @@ void MainWindow::setupMenu() { | ||||
| 			}); | ||||
| 			menu->addAction(cameraPresetAct); | ||||
| 		} | ||||
| 		menu->addSeparator(); | ||||
| 		auto const rebootAct = new QAction(tr("&Reboot"), this); | ||||
| 		connect(rebootAct, &QAction::triggered, &m_cameraClient, [this] { | ||||
| 			QMessageBox confirm(this); | ||||
| 			confirm.setText(tr("Are you sure you want to reboot the camera? This will take about 20 seconds.")); | ||||
| 			confirm.addButton(tr("&No"), QMessageBox::ButtonRole::NoRole); | ||||
| 			confirm.addButton(tr("&Yes"), QMessageBox::ButtonRole::YesRole); | ||||
| 			if (confirm.exec()) { | ||||
| 				m_cameraClient.reboot(); | ||||
| 			} | ||||
| 		}); | ||||
| 		menu->addAction(rebootAct); | ||||
| 	} | ||||
| 	// help menu | ||||
| 	{ | ||||
| @@ -129,39 +141,19 @@ void MainWindow::setupMenu() { | ||||
| 		connect(aboutAct, &QAction::triggered, &m_cameraClient, [this] { | ||||
| 			QMessageBox about(this); | ||||
| 			about.setText(tr( | ||||
| R"(Slide Controller 9000 - 1.0-beta1 | ||||
| Build date: %1 | ||||
| R"(Slide Controller 9000 - %1 | ||||
| Build date: %2 | ||||
|  | ||||
| Copyright 2021 - 2023 Gary Talent (gary@drinkingtea.net) | ||||
| Copyright 2021 - 2024 Gary Talent (gary@drinkingtea.net) | ||||
| Slide Controller 9000 is released under the MPL 2.0 | ||||
| Built on Qt library under LGPL 2.0)").arg(__DATE__)); | ||||
| Built on Qt library under LGPL 2.0)").arg(Version, __DATE__)); | ||||
| 			about.exec(); | ||||
| 		}); | ||||
| 		menu->addAction(aboutAct); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MainWindow::setupDefaultViewControls(QGridLayout *viewCtlLyt) { | ||||
| 	auto const mainWidget = viewCtlLyt->parentWidget(); | ||||
| 	auto const btnHideSlides = new QPushButton(tr("1. Hide"), mainWidget); | ||||
| 	auto const btnOpenLpShowSlides = new QPushButton(tr("2. Show in OpenLP Only"), mainWidget); | ||||
| 	auto const btnShowSlides = new QPushButton(tr("3. Show"), mainWidget); | ||||
| 	viewCtlLyt->addWidget(btnHideSlides, 0, 0); | ||||
| 	viewCtlLyt->addWidget(btnOpenLpShowSlides, 0, 1); | ||||
| 	viewCtlLyt->addWidget(btnShowSlides, 0, 2); | ||||
| 	btnHideSlides->setShortcut(Qt::Key_1); | ||||
| 	btnOpenLpShowSlides->setShortcut(Qt::Key_2); | ||||
| 	btnHideSlides->setToolTip(tr("Also hides slides in OBS")); | ||||
| 	btnShowSlides->setShortcut(Qt::Key_3); | ||||
| 	connect(btnHideSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::blankScreen); | ||||
| 	connect(btnHideSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::hideSlides); | ||||
| 	connect(btnOpenLpShowSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::showSlides); | ||||
| 	connect(btnOpenLpShowSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::hideSlides); | ||||
| 	connect(btnShowSlides, &QPushButton::clicked, &m_obsClient, &OBSClient::showSlides); | ||||
| 	connect(btnShowSlides, &QPushButton::clicked, &m_openlpClient, &OpenLPClient::showSlides); | ||||
| } | ||||
|  | ||||
| void MainWindow::setupCustomViewControls(QVector<View> const&views, QGridLayout *viewCtlLyt) { | ||||
| 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) { | ||||
| @@ -203,8 +195,8 @@ void MainWindow::setupViewControls(QVBoxLayout *rootLyt) { | ||||
| 				.obsSlides = false, | ||||
| 		}); | ||||
| 		views.emplace_back(View{ | ||||
| 				.name = tr("Hide in OBS"), | ||||
| 				.slides = false, | ||||
| 				.name = tr("Show in OpenLP Only"), | ||||
| 				.slides = true, | ||||
| 				.obsSlides = false, | ||||
| 		}); | ||||
| 		views.emplace_back(View{ | ||||
| @@ -213,15 +205,12 @@ void MainWindow::setupViewControls(QVBoxLayout *rootLyt) { | ||||
| 			.obsSlides = false, | ||||
| 		}); | ||||
| 	} | ||||
| 	if (views.empty()) { | ||||
| 		setupDefaultViewControls(viewCtlLyt); | ||||
| 	} else { | ||||
| 		setupCustomViewControls(views, viewCtlLyt); | ||||
| 	} | ||||
| 	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(); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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,8 +8,6 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| #include <QMainWindow> | ||||
|  | ||||
| #include "cameraclient.hpp" | ||||
| @@ -40,9 +38,7 @@ class MainWindow: public QMainWindow { | ||||
| 	private: | ||||
| 		void setupMenu(); | ||||
|  | ||||
| 		void setupDefaultViewControls(class QGridLayout *rootLyt); | ||||
|  | ||||
| 		void setupCustomViewControls(QVector<View> const&views, class QGridLayout *rootLyt); | ||||
| 		void setupViewControlButtons(QVector<View> const&views, class QGridLayout *rootLyt); | ||||
|  | ||||
| 		void setupViewControls(class QVBoxLayout *rootLyt); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -42,7 +42,7 @@ void OBSClient::setSlidesVisible(bool state) { | ||||
| } | ||||
|  | ||||
| void OBSClient::setBaseUrl() { | ||||
| 	const auto [host, port] = getOBSConnectionData(); | ||||
| 	auto const [host, port] = getOBSConnectionData(); | ||||
| 	m_baseUrl = QString("http://%1:%2").arg(host, QString::number(port)); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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> | ||||
| @@ -28,8 +29,8 @@ OpenLPClient::OpenLPClient(QObject *parent): QObject(parent) { | ||||
| } | ||||
|  | ||||
| QString OpenLPClient::getNextSong() { | ||||
| 	const auto currentSong = m_songNameMap[m_currentSongId]; | ||||
| 	const auto songIdx = m_songList.indexOf(currentSong) + 1; | ||||
| 	auto const currentSong = m_songNameMap[m_currentSongId]; | ||||
| 	auto const songIdx = m_songList.indexOf(currentSong) + 1; | ||||
| 	if (songIdx < m_songList.size()) { | ||||
| 		return m_songList[songIdx]; | ||||
| 	} | ||||
| @@ -37,19 +38,19 @@ QString OpenLPClient::getNextSong() { | ||||
| } | ||||
|  | ||||
| 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() { | ||||
| @@ -81,7 +82,7 @@ void OpenLPClient::changeSlide(int slide) { | ||||
| } | ||||
|  | ||||
| void OpenLPClient::setBaseUrl() { | ||||
| 	const auto [host, port] = getOpenLPConnectionData(); | ||||
| 	auto const [host, port] = getOpenLPConnectionData(); | ||||
| 	m_baseUrl = QString("http://%1:%2").arg(host, QString::number(port)); | ||||
| } | ||||
|  | ||||
| @@ -91,6 +92,12 @@ void OpenLPClient::get(QString const&urlExt) { | ||||
| 	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(m_baseUrl + "/api/service/list?_=1626628079579"); | ||||
| 	QNetworkRequest rqst(url); | ||||
| @@ -160,7 +167,7 @@ void OpenLPClient::handleSongListResponse(QNetworkReply *reply) { | ||||
| 	auto items = doc.object()["results"].toObject()["items"].toArray(); | ||||
| 	m_songNameMap.clear(); | ||||
| 	m_songList.clear(); | ||||
| 	for (const auto &item : items) { | ||||
| 	for (auto const &item : items) { | ||||
| 		auto song = item.toObject(); | ||||
| 		auto name = song["title"].toString(); | ||||
| 		auto id = song["id"].toString(); | ||||
| @@ -183,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); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -60,6 +60,8 @@ class OpenLPClient: public QObject { | ||||
| 	private: | ||||
| 		void get(QString const&url); | ||||
|  | ||||
| 		void post(QString const&url, QString const&data); | ||||
|  | ||||
| 		void requestSongList(); | ||||
|  | ||||
| 		void requestSlideList(); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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,8 +8,54 @@ | ||||
|  | ||||
| #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); | ||||
| @@ -121,7 +167,7 @@ void setViews(QVector<View> const&views) { | ||||
| QVector<View> getViews(QSettings &settings) { | ||||
| 	QVector<View> out; | ||||
| 	settings.beginGroup("Views"); | ||||
| 	const auto size = settings.beginReadArray("Views"); | ||||
| 	auto const size = settings.beginReadArray("Views"); | ||||
| 	for (auto i = 0; i < size; ++i) { | ||||
| 		settings.setArrayIndex(i); | ||||
| 		out.emplace_back(View{ | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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,9 +8,26 @@ | ||||
|  | ||||
| #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; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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,6 +7,7 @@ | ||||
|  */ | ||||
|  | ||||
| #include <QCheckBox> | ||||
| #include <QComboBox> | ||||
| #include <QFormLayout> | ||||
| #include <QHeaderView> | ||||
| #include <QIntValidator> | ||||
| @@ -15,6 +16,7 @@ | ||||
| #include <QPushButton> | ||||
| #include <QSettings> | ||||
| #include <QSpacerItem> | ||||
| #include <QSpinBox> | ||||
| #include <QTabWidget> | ||||
| #include <QTableWidget> | ||||
| #include <QVBoxLayout> | ||||
| @@ -31,34 +33,35 @@ enum ViewColumn { | ||||
| }; | ||||
|  | ||||
| SettingsDialog::SettingsDialog(QWidget *parent): QDialog(parent) { | ||||
| 	const auto lyt = new QVBoxLayout(this); | ||||
| 	const auto tabs = new QTabWidget(this); | ||||
| 	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) { | ||||
| 	const auto root = new QWidget(parent); | ||||
| 	const auto lyt = new QFormLayout(root); | ||||
| 	const auto portValidator = new QIntValidator(1, 65536, this); | ||||
| 	auto const root = new QWidget(parent); | ||||
| 	auto const lyt = new QFormLayout(root); | ||||
| 	auto const portValidator = new QIntValidator(1, 65536, this); | ||||
| 	QSettings settings; | ||||
| 	// camera settings | ||||
| 	{ | ||||
| 		const auto c = getCameraConnectionData(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("C&amera Host:"), m_cameraHostLe); | ||||
| 		lyt->addRow(tr("Camera &Host:"), m_cameraHostLe); | ||||
| 		lyt->addRow(tr("Ca&mera Port:"), m_cameraPortLe); | ||||
| 	} | ||||
| 	// OpenLP settings | ||||
| 	{ | ||||
| 		const auto c = getOpenLPConnectionData(settings); | ||||
| 		auto const c = getOpenLPConnectionData(settings); | ||||
| 		m_openLpHostLe = new QLineEdit(root); | ||||
| 		m_openLpPortLe = new QLineEdit(root); | ||||
| 		m_openLpHostLe->setText(c.host); | ||||
| @@ -69,7 +72,7 @@ QWidget *SettingsDialog::setupNetworkInputs(QWidget *parent) { | ||||
| 	} | ||||
| 	// OBS settings | ||||
| 	{ | ||||
| 		const auto c = getOBSConnectionData(settings); | ||||
| 		auto const c = getOBSConnectionData(settings); | ||||
| 		m_obsHostLe = new QLineEdit(root); | ||||
| 		m_obsPortLe = new QLineEdit(root); | ||||
| 		m_obsHostLe->setText(c.host); | ||||
| @@ -81,13 +84,58 @@ QWidget *SettingsDialog::setupNetworkInputs(QWidget *parent) { | ||||
| 	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); | ||||
| 	// table | ||||
| 	m_viewTable = new QTableWidget(parent); | ||||
| 	{ | ||||
| 	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"); | ||||
| @@ -104,19 +152,16 @@ QWidget *SettingsDialog::setupViewConfig(QWidget *parent) { | ||||
| 		m_viewTable->setColumnWidth(3, 70); | ||||
| 		hdr->setStretchLastSection(true); | ||||
| 	} | ||||
| 	// add/removes buttons | ||||
| 	{ | ||||
| 		auto const btnsRoot = new QWidget(root); | ||||
| 	{ // add/removes buttons | ||||
| 		auto const btnsLyt = new QHBoxLayout(btnsRoot); | ||||
| 		auto const addBtn = new QPushButton("+", btnsRoot); | ||||
| 		auto const rmBtn = new QPushButton("-", btnsRoot); | ||||
| 		addBtn->setFixedWidth(20); | ||||
| 		rmBtn->setFixedWidth(20); | ||||
| 		auto const addBtn = new QPushButton("A&dd", btnsRoot); | ||||
| 		auto const rmBtn = new QPushButton("&Remove", btnsRoot); | ||||
| 		addBtn->setFixedWidth(70); | ||||
| 		rmBtn->setFixedWidth(70); | ||||
| 		rmBtn->setDisabled(true); | ||||
| 		lyt->addWidget(btnsRoot); | ||||
| 		btnsLyt->addWidget(addBtn); | ||||
| 		btnsLyt->addWidget(rmBtn); | ||||
| 		btnsLyt->addSpacerItem(new QSpacerItem(1000, 0, QSizePolicy::Expanding, QSizePolicy::Ignored)); | ||||
| 		btnsLyt->setAlignment(Qt::AlignLeft); | ||||
| 		connect(addBtn, &QPushButton::clicked, this, [this, addBtn] { | ||||
| 			auto const row = m_viewTable->rowCount(); | ||||
| 			m_viewTable->setRowCount(row + 1); | ||||
| @@ -132,7 +177,7 @@ QWidget *SettingsDialog::setupViewConfig(QWidget *parent) { | ||||
| 			rmBtn->setEnabled(row > -1 && row < m_viewTable->rowCount()); | ||||
| 			addBtn->setEnabled(m_viewTable->rowCount() < MaxViews); | ||||
| 		}); | ||||
| 		const auto views = getViews(); | ||||
| 		auto const views = getViews(); | ||||
| 		m_viewTable->setRowCount(static_cast<int>(views.size())); | ||||
| 		for (auto row = 0; auto const&view : views) { | ||||
| 			setupViewRow(row, view); | ||||
| @@ -143,21 +188,24 @@ QWidget *SettingsDialog::setupViewConfig(QWidget *parent) { | ||||
| } | ||||
|  | ||||
| QWidget *SettingsDialog::setupButtons(QWidget *parent) { | ||||
| 	const auto root = new QWidget(parent); | ||||
| 	const auto lyt = new QHBoxLayout(root); | ||||
| 	auto const root = new QWidget(parent); | ||||
| 	auto const lyt = new QHBoxLayout(root); | ||||
| 	m_errLbl = new QLabel(root); | ||||
| 	const auto okBtn = new QPushButton(tr("&OK"), root); | ||||
| 	const auto cancelBtn = new QPushButton(tr("&Cancel"), root); | ||||
| 	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; | ||||
| } | ||||
|  | ||||
| void SettingsDialog::handleOK() { | ||||
| void SettingsDialog::handleApply() { | ||||
| 	QSettings settings; | ||||
| 	QVector<View> views; | ||||
| 	auto const viewsErr = collectViews(views); | ||||
| @@ -177,23 +225,29 @@ void SettingsDialog::handleOK() { | ||||
| 		.host = m_obsHostLe->text(), | ||||
| 		.port = m_obsPortLe->text().toUShort(), | ||||
| 	}); | ||||
| 	collectVideoConfig(); | ||||
| 	setVideoConfig(settings, m_videoConfig); | ||||
| } | ||||
|  | ||||
| void SettingsDialog::handleOK() { | ||||
| 	handleApply(); | ||||
| 	accept(); | ||||
| } | ||||
|  | ||||
| void SettingsDialog::setupViewRow(int row, View const&view) { | ||||
| 	// name | ||||
| 	const auto nameItem = new QTableWidgetItem(view.name); | ||||
| 	auto const nameItem = new QTableWidgetItem(view.name); | ||||
| 	m_viewTable->setItem(row, ViewColumn::Name, nameItem); | ||||
| 	// slides | ||||
| 	const auto slidesCb = new QCheckBox(m_viewTable); | ||||
| 	auto const slidesCb = new QCheckBox(m_viewTable); | ||||
| 	slidesCb->setChecked(view.slides); | ||||
| 	m_viewTable->setCellWidget(row, ViewColumn::Slides, slidesCb); | ||||
| 	// obs slides | ||||
| 	const auto obsSlidesCb = new QCheckBox(m_viewTable); | ||||
| 	auto const obsSlidesCb = new QCheckBox(m_viewTable); | ||||
| 	obsSlidesCb->setChecked(view.obsSlides); | ||||
| 	m_viewTable->setCellWidget(row, ViewColumn::ObsSlides, obsSlidesCb); | ||||
| 	// camera preset | ||||
| 	const auto presetItem = new QTableWidgetItem(QString::number(view.cameraPreset)); | ||||
| 	auto const presetItem = new QTableWidgetItem(QString::number(view.cameraPreset)); | ||||
| 	m_viewTable->setItem(row, ViewColumn::CameraPreset, presetItem); | ||||
| } | ||||
|  | ||||
| @@ -206,7 +260,7 @@ int SettingsDialog::collectViews(QVector<View> &views) const { | ||||
| 			m_errLbl->setText(tr("View %1 has no name.").arg(viewNo)); | ||||
| 			return 1; | ||||
| 		} | ||||
| 		const auto cameraPreset = m_viewTable->item(row, ViewColumn::CameraPreset)->text().toInt(&ok); | ||||
| 		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; | ||||
| @@ -220,3 +274,38 @@ int SettingsDialog::collectViews(QVector<View> &views) const { | ||||
| 	} | ||||
| 	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); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -10,11 +10,13 @@ | ||||
|  | ||||
| #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; | ||||
| @@ -22,13 +24,21 @@ class SettingsDialog: public QDialog { | ||||
| 		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); | ||||
| 		void handleApply(); | ||||
| 		void handleOK(); | ||||
| 		void setupViewRow(int row, View const&view = {}); | ||||
| 		/** | ||||
| @@ -37,4 +47,9 @@ class SettingsDialog: public QDialog { | ||||
| 		 */ | ||||
| 		[[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&); | ||||
| }; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -34,8 +34,8 @@ SlideView::SlideView(QWidget *parent): QWidget(parent) { | ||||
| } | ||||
|  | ||||
| QString SlideView::getNextSong() const { | ||||
| 	const auto cnt = m_songSelector->count(); | ||||
| 	const auto idx = m_songSelector->currentRow() + 1; | ||||
| 	auto const cnt = m_songSelector->count(); | ||||
| 	auto const idx = m_songSelector->currentRow() + 1; | ||||
| 	if (idx < cnt) { | ||||
| 		return m_songSelector->currentItem()->text(); | ||||
| 	} | ||||
| @@ -43,7 +43,7 @@ QString SlideView::getNextSong() const { | ||||
| } | ||||
|  | ||||
| void SlideView::pollUpdate(QString const&songName, int slide) { | ||||
| 	auto songItems = m_songSelector->findItems(songName, Qt::MatchFixedString); | ||||
| 	auto const songItems = m_songSelector->findItems(songName, Qt::MatchFixedString); | ||||
| 	if (songItems.empty()) { | ||||
| 		return; | ||||
| 	} | ||||
| @@ -68,11 +68,14 @@ void SlideView::changeSong(int song) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void SlideView::slideListUpdate(QStringList const&tagList, QStringList const&slideList) { | ||||
| void SlideView::slideListUpdate(QStringList tagList, QStringList const&slideList) { | ||||
| 	for (auto &tag : tagList) { | ||||
| 		tag = tag.split("").join("\n"); | ||||
| 	} | ||||
| 	m_currentSlide = 0; | ||||
| 	m_slideTable->setRowCount(static_cast<int>(slideList.size())); | ||||
| 	for (int i = 0; i < slideList.size(); ++i) { | ||||
| 		const 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); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright 2021 - 2023 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 | ||||
| @@ -28,7 +28,7 @@ class SlideView: public QWidget { | ||||
|  | ||||
| 		void songListUpdate(QStringList const&songList); | ||||
|  | ||||
| 		void slideListUpdate(QStringList const&tagList, QStringList const&songList); | ||||
| 		void slideListUpdate(QStringList tagList, QStringList const&songList); | ||||
|  | ||||
| 		void reset(); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user