diff --git a/.github/workflows/build-all-on-macos.yml b/.github/workflows/build-all-on-macos.yml index dc60a1e..a5b43e5 100644 --- a/.github/workflows/build-all-on-macos.yml +++ b/.github/workflows/build-all-on-macos.yml @@ -75,7 +75,7 @@ jobs: run: | echo "current working directory" && pwd cd EmotiBitSlidePlayer - xcodebuild -project EmotiBitSlidePlayer.xcodeproj -scheme Release + xcodebuild -project "EmotiBit SlidePlayer.xcodeproj" -scheme Release upload-artifact-macos: needs: [build-oscilloscope-macos, build-dataparser-macos, build-firmwareinstaller-macos, build-slideplayer-macos] if: github.ref == 'refs/heads/dev' @@ -98,7 +98,7 @@ jobs: mv EmotiBitOscilloscope/bin/EmotiBitOscilloscope.app stageRelease/EmotiBitSoftware-macos-${{ steps.get_version.outputs.version }} mv EmotiBitDataParser/bin/EmotiBitDataParser.app stageRelease/EmotiBitSoftware-macos-${{ steps.get_version.outputs.version }} mv EmotiBitFirmwareInstaller/bin/EmotiBitFirmwareInstaller.app stageRelease/EmotiBitSoftware-macos-${{ steps.get_version.outputs.version }} - mv EmotiBitSlidePlayer/bin/EmotiBitSlidePlayer.app stageRelease/EmotiBitSoftware-macos-${{ steps.get_version.outputs.version }} + mv "EmotiBitSlidePlayer/bin/EmotiBit SlidePlayer.app" stageRelease/EmotiBitSoftware-macos-${{ steps.get_version.outputs.version }} - name: copy SiLabs drivers working-directory: ${{ vars.ADDONS_DIR }} run: | diff --git a/EmotiBitSlidePlayer/CMakeLists.txt b/EmotiBitSlidePlayer/CMakeLists.txt index 98e1bdf..f54d17a 100644 --- a/EmotiBitSlidePlayer/CMakeLists.txt +++ b/EmotiBitSlidePlayer/CMakeLists.txt @@ -24,16 +24,18 @@ add_executable(${PROJECT_NAME} MACOSX_BUNDLE src/main.cpp ${OF_APP_SOURCES}) of_configure_app(${PROJECT_NAME} 23) target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src ${ADDON_OFXJSON_INCLUDE_DIRS} ) set_target_properties(${PROJECT_NAME} PROPERTIES - MACOSX_BUNDLE_BUNDLE_NAME "EmotiBitSlidePlayer" + MACOSX_BUNDLE_BUNDLE_NAME "EmotiBit SlidePlayer" + OUTPUT_NAME "EmotiBit SlidePlayer" ) # ── Copy settings + slides to app bundle Resources ──────────────────────────── set(BUNDLE_RESOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/bin/${PROJECT_NAME}.app/Contents/Resources") + "${CMAKE_CURRENT_SOURCE_DIR}/bin/EmotiBit SlidePlayer.app/Contents/Resources") add_custom_target(copy_settings ALL COMMAND ${CMAKE_COMMAND} -E make_directory "${BUNDLE_RESOURCES}" COMMAND ${CMAKE_COMMAND} -E copy @@ -73,6 +75,7 @@ set_target_properties(slideShow_tests PROPERTIES ) target_include_directories(slideShow_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src ${ADDON_OFXJSON_INCLUDE_DIRS} ) diff --git a/EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/project.pbxproj b/EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/project.pbxproj similarity index 97% rename from EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/project.pbxproj rename to EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/project.pbxproj index db87a84..d40f703 100644 --- a/EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/project.pbxproj +++ b/EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/project.pbxproj @@ -40,13 +40,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1645F56257269CD0356320BD /* ofxJSON.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; name = ofxJSON.h; path = ../../../addons/ofxJSON/src/ofxJSON.h; sourceTree = SOURCE_ROOT; }; - 21BDE665988474F1B1F4D302 /* jsoncpp.cpp */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.cpp; fileEncoding = 4; name = jsoncpp.cpp; path = ../../../addons/ofxJSON/libs/jsoncpp/src/jsoncpp.cpp; sourceTree = SOURCE_ROOT; }; - 26A541233BC6F736E758F718 /* ofxJSONElement.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; name = ofxJSONElement.h; path = ../../../addons/ofxJSON/src/ofxJSONElement.h; sourceTree = SOURCE_ROOT; }; - 2C7CF000B7B4F782C187C353 /* json.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; name = json.h; path = ../../../addons/ofxJSON/libs/jsoncpp/include/json/json.h; sourceTree = SOURCE_ROOT; }; - 61313493CDB52744E22A604D /* json-forwards.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; name = "json-forwards.h"; path = "../../../addons/ofxJSON/libs/jsoncpp/include/json/json-forwards.h"; sourceTree = SOURCE_ROOT; }; + 1645F56257269CD0356320BD /* ofxJSON.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; name = ofxJSON.h; path = ../../ofxJSON/src/ofxJSON.h; sourceTree = SOURCE_ROOT; }; + 21BDE665988474F1B1F4D302 /* jsoncpp.cpp */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.cpp; fileEncoding = 4; name = jsoncpp.cpp; path = ../../ofxJSON/libs/jsoncpp/src/jsoncpp.cpp; sourceTree = SOURCE_ROOT; }; + 26A541233BC6F736E758F718 /* ofxJSONElement.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; name = ofxJSONElement.h; path = ../../ofxJSON/src/ofxJSONElement.h; sourceTree = SOURCE_ROOT; }; + 2C7CF000B7B4F782C187C353 /* json.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; name = json.h; path = ../../ofxJSON/libs/jsoncpp/include/json/json.h; sourceTree = SOURCE_ROOT; }; + 61313493CDB52744E22A604D /* json-forwards.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; name = "json-forwards.h"; path = "../../ofxJSON/libs/jsoncpp/include/json/json-forwards.h"; sourceTree = SOURCE_ROOT; }; 8EF01BA42F7876C700BF0971 /* emotibitSlidePlayerSettings.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = emotibitSlidePlayerSettings.json; path = bin/data/emotibitSlidePlayerSettings.json; sourceTree = ""; }; 8EF01BA62F7876DD00BF0971 /* example_slides */ = {isa = PBXFileReference; lastKnownFileType = folder; name = example_slides; path = bin/data/example_slides; sourceTree = ""; }; + 8EF01BA9300BFE2B00BF0971 /* ofxEmotiBitVersion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ofxEmotiBitVersion.h; path = ../src/ofxEmotiBitVersion.h; sourceTree = ""; }; E42962AC2163EDD300A6A9E2 /* ofCamera.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = ofCamera.cpp; path = ../../../libs/openFrameworks/3d/ofCamera.cpp; sourceTree = SOURCE_ROOT; }; E42962AD2163EDD300A6A9E2 /* ofMesh.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ofMesh.h; path = ../../../libs/openFrameworks/3d/ofMesh.h; sourceTree = SOURCE_ROOT; }; E42962AE2163EDD300A6A9E2 /* ofNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ofNode.h; path = ../../../libs/openFrameworks/3d/ofNode.h; sourceTree = SOURCE_ROOT; }; @@ -222,13 +223,13 @@ E42963712163EDD300A6A9E2 /* ofFmodSoundPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ofFmodSoundPlayer.h; path = ../../../libs/openFrameworks/sound/ofFmodSoundPlayer.h; sourceTree = SOURCE_ROOT; }; E42963722163EDD300A6A9E2 /* ofSoundBuffer.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = ofSoundBuffer.cpp; path = ../../../libs/openFrameworks/sound/ofSoundBuffer.cpp; sourceTree = SOURCE_ROOT; }; E42963732163EDD300A6A9E2 /* ofSoundBaseTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ofSoundBaseTypes.h; path = ../../../libs/openFrameworks/sound/ofSoundBaseTypes.h; sourceTree = SOURCE_ROOT; }; - E4B69B5B0A3A1756003C02F2 /* EmotiBitSlidePlayerDebug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EmotiBitSlidePlayerDebug.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E4B69B5B0A3A1756003C02F2 /* EmotiBit SlidePlayerDebug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "EmotiBit SlidePlayerDebug.app"; sourceTree = BUILT_PRODUCTS_DIR; }; E4B69E1D0A3A1BDC003C02F2 /* main.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = main.cpp; path = src/main.cpp; sourceTree = SOURCE_ROOT; }; E4B69E1E0A3A1BDC003C02F2 /* ofApp.cpp */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.cpp; fileEncoding = 4; name = ofApp.cpp; path = src/ofApp.cpp; sourceTree = SOURCE_ROOT; }; E4B69E1F0A3A1BDC003C02F2 /* ofApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ofApp.h; path = src/ofApp.h; sourceTree = SOURCE_ROOT; }; E4B6FCAD0C3E899E008CF71C /* openFrameworks-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "openFrameworks-Info.plist"; sourceTree = ""; }; E4EB6923138AFD0F00A09F29 /* Project.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Project.xcconfig; sourceTree = ""; }; - F2B099E6BD1199664C48B177 /* ofxJSONElement.cpp */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.cpp; fileEncoding = 4; name = ofxJSONElement.cpp; path = ../../../addons/ofxJSON/src/ofxJSONElement.cpp; sourceTree = SOURCE_ROOT; }; + F2B099E6BD1199664C48B177 /* ofxJSONElement.cpp */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.cpp; fileEncoding = 4; name = ofxJSONElement.cpp; path = ../../ofxJSON/src/ofxJSONElement.cpp; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -277,6 +278,14 @@ name = local_addons; sourceTree = ""; }; + 8EF01BA8300BFE1400BF0971 /* ofxEmotiBIt */ = { + isa = PBXGroup; + children = ( + 8EF01BA9300BFE2B00BF0971 /* ofxEmotiBitVersion.h */, + ); + name = ofxEmotiBIt; + sourceTree = ""; + }; 977A836DD2C489CCC5E330FF /* jsoncpp */ = { isa = PBXGroup; children = ( @@ -289,6 +298,7 @@ BB4B014C10F69532006C3DED /* addons */ = { isa = PBXGroup; children = ( + 8EF01BA8300BFE1400BF0971 /* ofxEmotiBIt */, 2865DAEF86B1907A704CA70B /* ofxJSON */, ); name = addons; @@ -603,7 +613,7 @@ E42962AA2163EDD300A6A9E2 /* openFrameworks */, BB4B014C10F69532006C3DED /* addons */, 6948EE371B920CB800B5AC1A /* local_addons */, - E4B69B5B0A3A1756003C02F2 /* EmotiBitSlidePlayerDebug.app */, + E4B69B5B0A3A1756003C02F2 /* EmotiBit SlidePlayerDebug.app */, ); sourceTree = ""; }; @@ -628,9 +638,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - E4B69B5A0A3A1756003C02F2 /* EmotiBitSlidePlayer */ = { + E4B69B5A0A3A1756003C02F2 /* EmotiBit SlidePlayer */ = { isa = PBXNativeTarget; - buildConfigurationList = E4B69B5F0A3A1757003C02F2 /* Build configuration list for PBXNativeTarget "EmotiBitSlidePlayer" */; + buildConfigurationList = E4B69B5F0A3A1757003C02F2 /* Build configuration list for PBXNativeTarget "EmotiBit SlidePlayer" */; buildPhases = ( E42962A92163ECCD00A6A9E2 /* ShellScript */, E4B69B580A3A1756003C02F2 /* Sources */, @@ -644,9 +654,9 @@ ); dependencies = ( ); - name = EmotiBitSlidePlayer; + name = "EmotiBit SlidePlayer"; productName = myOFApp; - productReference = E4B69B5B0A3A1756003C02F2 /* EmotiBitSlidePlayerDebug.app */; + productReference = E4B69B5B0A3A1756003C02F2 /* EmotiBit SlidePlayerDebug.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -657,7 +667,7 @@ attributes = { LastUpgradeCheck = 0600; }; - buildConfigurationList = E4B69B4D0A3A1720003C02F2 /* Build configuration list for PBXProject "EmotiBitSlidePlayer" */; + buildConfigurationList = E4B69B4D0A3A1720003C02F2 /* Build configuration list for PBXProject "EmotiBit SlidePlayer" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; @@ -672,7 +682,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - E4B69B5A0A3A1756003C02F2 /* EmotiBitSlidePlayer */, + E4B69B5A0A3A1756003C02F2 /* EmotiBit SlidePlayer */, ); }; /* End PBXProject section */ @@ -964,7 +974,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - E4B69B4D0A3A1720003C02F2 /* Build configuration list for PBXProject "EmotiBitSlidePlayer" */ = { + E4B69B4D0A3A1720003C02F2 /* Build configuration list for PBXProject "EmotiBit SlidePlayer" */ = { isa = XCConfigurationList; buildConfigurations = ( E4B69B4E0A3A1720003C02F2 /* Debug */, @@ -974,7 +984,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - E4B69B5F0A3A1757003C02F2 /* Build configuration list for PBXNativeTarget "EmotiBitSlidePlayer" */ = { + E4B69B5F0A3A1757003C02F2 /* Build configuration list for PBXNativeTarget "EmotiBit SlidePlayer" */ = { isa = XCConfigurationList; buildConfigurations = ( E4B69B600A3A1757003C02F2 /* Debug */, diff --git a/EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/xcshareddata/xcschemes/Debug.xcscheme b/EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/xcshareddata/xcschemes/Debug.xcscheme similarity index 77% rename from EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/xcshareddata/xcschemes/Debug.xcscheme rename to EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/xcshareddata/xcschemes/Debug.xcscheme index 57212aa..91ac876 100644 --- a/EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/xcshareddata/xcschemes/Debug.xcscheme +++ b/EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/xcshareddata/xcschemes/Debug.xcscheme @@ -15,9 +15,9 @@ + BuildableName = "EmotiBit SlidePlayer.app" + BlueprintName = "EmotiBit SlidePlayer" + ReferencedContainer = "container:EmotiBit SlidePlayer.xcodeproj"> @@ -31,9 +31,9 @@ + BuildableName = "EmotiBit SlidePlayer.app" + BlueprintName = "EmotiBit SlidePlayer" + ReferencedContainer = "container:EmotiBit SlidePlayer.xcodeproj"> @@ -54,9 +54,9 @@ + BuildableName = "EmotiBit SlidePlayer.app" + BlueprintName = "EmotiBit SlidePlayer" + ReferencedContainer = "container:EmotiBit SlidePlayer.xcodeproj"> @@ -71,9 +71,9 @@ + BuildableName = "EmotiBit SlidePlayer.app" + BlueprintName = "EmotiBit SlidePlayer" + ReferencedContainer = "container:EmotiBit SlidePlayer.xcodeproj"> diff --git a/EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/xcshareddata/xcschemes/Release.xcscheme b/EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/xcshareddata/xcschemes/Release.xcscheme similarity index 77% rename from EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/xcshareddata/xcschemes/Release.xcscheme rename to EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/xcshareddata/xcschemes/Release.xcscheme index 7f18b3f..79ec123 100644 --- a/EmotiBitSlidePlayer/EmotiBitSlidePlayer.xcodeproj/xcshareddata/xcschemes/Release.xcscheme +++ b/EmotiBitSlidePlayer/EmotiBit SlidePlayer.xcodeproj/xcshareddata/xcschemes/Release.xcscheme @@ -15,9 +15,9 @@ + BuildableName = "EmotiBit SlidePlayer.app" + BlueprintName = "EmotiBit SlidePlayer" + ReferencedContainer = "container:EmotiBit SlidePlayer.xcodeproj"> @@ -31,9 +31,9 @@ + BuildableName = "EmotiBit SlidePlayer.app" + BlueprintName = "EmotiBit SlidePlayer" + ReferencedContainer = "container:EmotiBit SlidePlayer.xcodeproj"> @@ -54,9 +54,9 @@ + BuildableName = "EmotiBit SlidePlayer.app" + BlueprintName = "EmotiBit SlidePlayer" + ReferencedContainer = "container:EmotiBit SlidePlayer.xcodeproj"> @@ -71,9 +71,9 @@ + BuildableName = "EmotiBit SlidePlayer.app" + BlueprintName = "EmotiBit SlidePlayer" + ReferencedContainer = "container:EmotiBit SlidePlayer.xcodeproj"> diff --git a/EmotiBitSlidePlayer/EmotiBitSlidePlayer.vcxproj b/EmotiBitSlidePlayer/EmotiBitSlidePlayer.vcxproj index e1c9a8d..c4ff0c2 100644 --- a/EmotiBitSlidePlayer/EmotiBitSlidePlayer.vcxproj +++ b/EmotiBitSlidePlayer/EmotiBitSlidePlayer.vcxproj @@ -100,7 +100,7 @@ %(PreprocessorDefinitions) MultiThreadedDebugDLL Level3 - %(AdditionalIncludeDirectories);..\..\..\addons\ofxJSON\libs;..\..\..\addons\ofxJSON\libs\jsoncpp;..\..\..\addons\ofxJSON\libs\jsoncpp\include;..\..\..\addons\ofxJSON\libs\jsoncpp\include\json;..\..\..\addons\ofxJSON\libs\jsoncpp\src;..\..\..\addons\ofxJSON\src + %(AdditionalIncludeDirectories);..\src;..\..\..\addons\ofxJSON\libs;..\..\..\addons\ofxJSON\libs\jsoncpp;..\..\..\addons\ofxJSON\libs\jsoncpp\include;..\..\..\addons\ofxJSON\libs\jsoncpp\include\json;..\..\..\addons\ofxJSON\libs\jsoncpp\src;..\..\..\addons\ofxJSON\src CompileAsCpp $(IntDir)%(RelativeDir) @@ -120,7 +120,7 @@ %(PreprocessorDefinitions) MultiThreadedDebugDLL Level3 - %(AdditionalIncludeDirectories);..\..\..\addons\ofxJSON\libs;..\..\..\addons\ofxJSON\libs\jsoncpp;..\..\..\addons\ofxJSON\libs\jsoncpp\include;..\..\..\addons\ofxJSON\libs\jsoncpp\include\json;..\..\..\addons\ofxJSON\libs\jsoncpp\src;..\..\..\addons\ofxJSON\src + %(AdditionalIncludeDirectories);..\src;..\..\..\addons\ofxJSON\libs;..\..\..\addons\ofxJSON\libs\jsoncpp;..\..\..\addons\ofxJSON\libs\jsoncpp\include;..\..\..\addons\ofxJSON\libs\jsoncpp\include\json;..\..\..\addons\ofxJSON\libs\jsoncpp\src;..\..\..\addons\ofxJSON\src CompileAsCpp true $(IntDir)%(RelativeDir) @@ -140,7 +140,7 @@ %(PreprocessorDefinitions) MultiThreadedDLL Level3 - %(AdditionalIncludeDirectories);..\..\..\addons\ofxJSON\libs;..\..\..\addons\ofxJSON\libs\jsoncpp;..\..\..\addons\ofxJSON\libs\jsoncpp\include;..\..\..\addons\ofxJSON\libs\jsoncpp\include\json;..\..\..\addons\ofxJSON\libs\jsoncpp\src;..\..\..\addons\ofxJSON\src + %(AdditionalIncludeDirectories);..\src;..\..\..\addons\ofxJSON\libs;..\..\..\addons\ofxJSON\libs\jsoncpp;..\..\..\addons\ofxJSON\libs\jsoncpp\include;..\..\..\addons\ofxJSON\libs\jsoncpp\include\json;..\..\..\addons\ofxJSON\libs\jsoncpp\src;..\..\..\addons\ofxJSON\src CompileAsCpp true $(IntDir)%(RelativeDir) @@ -163,7 +163,7 @@ %(PreprocessorDefinitions) MultiThreadedDLL Level3 - %(AdditionalIncludeDirectories);..\..\..\addons\ofxJSON\libs;..\..\..\addons\ofxJSON\libs\jsoncpp;..\..\..\addons\ofxJSON\libs\jsoncpp\include;..\..\..\addons\ofxJSON\libs\jsoncpp\include\json;..\..\..\addons\ofxJSON\libs\jsoncpp\src;..\..\..\addons\ofxJSON\src + %(AdditionalIncludeDirectories);..\src;..\..\..\addons\ofxJSON\libs;..\..\..\addons\ofxJSON\libs\jsoncpp;..\..\..\addons\ofxJSON\libs\jsoncpp\include;..\..\..\addons\ofxJSON\libs\jsoncpp\include\json;..\..\..\addons\ofxJSON\libs\jsoncpp\src;..\..\..\addons\ofxJSON\src CompileAsCpp $(IntDir)%(RelativeDir) diff --git a/EmotiBitSlidePlayer/Project.xcconfig b/EmotiBitSlidePlayer/Project.xcconfig index 40adc89..6b7edee 100644 --- a/EmotiBitSlidePlayer/Project.xcconfig +++ b/EmotiBitSlidePlayer/Project.xcconfig @@ -15,4 +15,4 @@ ICON_FILE_PATH = $(OF_PATH)/libs/openFrameworksCompiled/project/osx/ OTHER_CFLAGS = $(OF_CORE_CFLAGS) OTHER_LDFLAGS = $(OF_CORE_LIBS) $(OF_CORE_FRAMEWORKS) -HEADER_SEARCH_PATHS = $(OF_CORE_HEADERS) +HEADER_SEARCH_PATHS = $(OF_CORE_HEADERS) ../src diff --git a/EmotiBitSlidePlayer/README.md b/EmotiBitSlidePlayer/README.md index 42cfe53..6cb5bf8 100644 --- a/EmotiBitSlidePlayer/README.md +++ b/EmotiBitSlidePlayer/README.md @@ -4,7 +4,7 @@ Displays timed sequences of image slide sets and logs all events to a CSV file. ## Settings file -On first launch the app copies `emotibitSlidePlayerSettings.json` to `~/Documents/EmotiBit/` and reads it from there on every subsequent run. Edit that copy to configure the app. +On first launch the app copies `emotibitSlidePlayerSettings.json` to `~/Documents/EmotiBit/EmotiBit SlidePlayer/` and reads it from there on every subsequent run. Edit that copy to configure the app. ### Global slide settings @@ -24,8 +24,8 @@ On first launch the app copies `emotibitSlidePlayerSettings.json` to `~/Document | Key | Description | |-----|-------------| | `startFullScreen` | Start in full-screen mode | -| `startPaused` | Start with the slideshow paused | -| `logFileDirectory` | Directory for CSV log output (leave empty to log to the current directory) | +| `startPaused` | If `true`, pause on the first slide when the show starts (one-shot — does not repeat mid-show; resets when the show is restarted with `R`, `S`, or `L`) | +| `logFileDirectory` | Directory for CSV log output (leave empty to use the default: `~/Documents/EmotiBit/EmotiBit SlidePlayer`) | ### Keyboard controls (all keys re-bindable in settings) @@ -54,20 +54,18 @@ Each entry in `slideSets` points to a directory of images (`.jpg`, `.jpeg`, `.pn ] ``` -Image paths can be relative (resolved from the app's `data/` directory) or absolute. +Image paths can be relative (TODO: State the relative path location for windows and mac) or absolute. ## Event log -A CSV file (`dateTime,event,details`) is written to `logFileDirectory` on each run. Logged events include `SLIDE_ON`, `SLIDE_OFF`, `KEY_PRESS`, `APP_END`, and others. +A CSV file (`dateTime,epochTime(S),event,details`) is written to `logFileDirectory` on each run. `epochTime(S)` is UTC Unix time in seconds with millisecond precision (e.g. `1234567890.123`). To align slide events with physiological signals, correlate this column against the `LocalTimestamp` column in parsed EmotiBit data. Logged events include `SLIDE_ON`, `SLIDE_OFF`, `KEY_RELEASE`, `APP_END`, and others. > **Note:** If `logFileDirectory` does not exist the log file will silently fail to open. Ensure the directory exists before launching. ## Known TODOs -- `startFullScreen` and `startPaused` settings are parsed but not yet implemented. -- `S` (load settings) and `L` (set log directory) keyboard commands are not yet implemented. +- `startFullScreen` setting is parsed but not yet implemented. - Pressing `B` at the start of a set does not cross back to the previous set; it stops at the first slide of the current set. -- No end slide is shown when the last slide set finishes — the app exits immediately. - Supported image extensions (jpg/jpeg/png/bmp) are hardcoded; other formats are not loaded. - Time-elapsed tracking on key press only accounts for the ON phase, not the OFF phase. - Log directory existence is not validated before opening the log file. diff --git a/EmotiBitSlidePlayer/config.make b/EmotiBitSlidePlayer/config.make index 5a719bc..9af9455 100644 --- a/EmotiBitSlidePlayer/config.make +++ b/EmotiBitSlidePlayer/config.make @@ -105,7 +105,7 @@ OF_ROOT = /Users/cfl/dev/emotibit/software/local_dev/of_v0.11.2_osx_release # # Note: Leave a leading space when adding list items with the += operator ################################################################################ -# PROJECT_CFLAGS = +PROJECT_CFLAGS = -I../src ################################################################################ # PROJECT OPTIMIZATION CFLAGS diff --git a/EmotiBitSlidePlayer/openFrameworks-Info.plist b/EmotiBitSlidePlayer/openFrameworks-Info.plist index ba13aec..789b6d0 100644 --- a/EmotiBitSlidePlayer/openFrameworks-Info.plist +++ b/EmotiBitSlidePlayer/openFrameworks-Info.plist @@ -14,6 +14,8 @@ APPL CFBundleSignature ???? + CFBundleName + EmotiBit SlidePlayer CFBundleVersion 1.0 CFBundleIconFile diff --git a/EmotiBitSlidePlayer/src/ofApp.cpp b/EmotiBitSlidePlayer/src/ofApp.cpp index 88c9956..29a8100 100644 --- a/EmotiBitSlidePlayer/src/ofApp.cpp +++ b/EmotiBitSlidePlayer/src/ofApp.cpp @@ -13,6 +13,7 @@ #include "ofFileUtils.h" #include "ofImage.h" #include "ofUtils.h" +#include "ofxEmotiBitVersion.h" // ── Setup // ───────────────────────────────────────────────────────────────────── @@ -23,7 +24,7 @@ void ofApp::ensureSettingsFile() ofFilePath::join( ofFilePath::join(ofFilePath::getUserHomeDir(), "Documents"), "EmotiBit"), - "EmotiBitSlidePlayer"); + "EmotiBit SlidePlayer"); std::string target_path = ofFilePath::join(docs_dir, settings_file_name_); if (!ofFile(target_path).exists()) { @@ -45,6 +46,7 @@ void ofApp::setup() #ifdef TARGET_OSX ofSetDataPathRoot("../Resources"); #endif + ofSetWindowTitle("EmotiBit SlidePlayer v" + ofxEmotiBitVersion); ensureSettingsFile(); ofSetLogLevel(OF_LOG_SILENT); if (!loadAppSettings()) @@ -53,18 +55,18 @@ void ofApp::setup() ofExit(); } startLogToFile(); - logEvent("APP_START", "settings=" + settings_file_name_ + - " log_dir=" + app_settings_.log_file_directory_); - logEvent("SETTINGS_JSON", settings_json_); + logEvent("APP_START", {"settings=" + settings_file_name_, + "log_dir=" + app_settings_.log_file_directory_}); + logEvent("SETTINGS_JSON", {settings_json_}); for (int i = 0; i < (int)app_settings_.slide_sets_.size(); i++) { const auto& ss = app_settings_.slide_sets_[i]; logEvent("SETTINGS_LOAD", - "set_index=" + std::to_string(i) + - " dir=" + ss.slide_directory_ + " max_slides=" + - std::to_string(ss.settings_.max_slides_per_set_) + - " randomize=" + - std::to_string(ss.settings_.slide_order_randomization_)); + {"set_index=" + std::to_string(i), + "dir=" + ss.slide_directory_, + "max_slides=" + std::to_string(ss.settings_.max_slides_per_set_), + "randomize=" + + std::to_string(ss.settings_.slide_order_randomization_)}); } } @@ -122,7 +124,7 @@ bool ofApp::parseSettings(const Json::Value& settings) ofFilePath::join(ofFilePath::getUserHomeDir(), "Documents"), "EmotiBit"), - "EmotiBitSlidePlayer"), + "EmotiBit SlidePlayer"), "log"); std::cerr << "Warning: log file directory not specified. Using default: " @@ -257,6 +259,7 @@ bool ofApp::parseSettings(const Json::Value& settings) if (settings.isMember("slideSets")) { + app_settings_.slide_sets_.clear(); uint16_t num_slide_sets = settings["slideSets"].size(); for (int i = 0; i < num_slide_sets; i++) { @@ -296,21 +299,34 @@ bool ofApp::startLogToFile() std::cerr << "Failed to open log file: " << log_file_name << std::endl; return false; } - event_log_ << "dateTime,event,details\n"; + event_log_ << "dateTime,epochTime(S),event,details\n"; log_stream_ = &event_log_; return true; } -void ofApp::logEvent(const std::string& event, const std::string& details) +void ofApp::logEvent(const std::string& event, + const std::vector& details) { + std::string joined; + for (size_t i = 0; i < details.size(); ++i) + { + if (i > 0) joined += log_details_delimiter_; + joined += details[i]; + } std::string timestamp = get_timestamp_(); + uint64_t epoch_ms = get_epoch_msec_(); + std::string epoch_str = std::to_string(epoch_ms / 1000) + "." + + (epoch_ms % 1000 < 100 ? "0" : "") + + (epoch_ms % 1000 < 10 ? "0" : "") + + std::to_string(epoch_ms % 1000); if (log_stream_ != nullptr) { - *log_stream_ << timestamp << ',' << event << ',' << '"' << details - << '"' << '\n'; + *log_stream_ << timestamp << ',' << epoch_str << ',' << event << ',' + << '"' << joined << '"' << '\n'; log_stream_->flush(); } - std::cout << timestamp << ' ' << event << ' ' << details << '\n'; + std::cout << timestamp << ' ' << epoch_str << ' ' << event << ' ' << joined + << '\n'; } // ── Per-frame @@ -318,11 +334,6 @@ void ofApp::logEvent(const std::string& event, const std::string& details) void ofApp::update() { - if (should_exit_) - { - ofExit(); - return; - } updateCurrentState(); } @@ -332,15 +343,20 @@ void ofApp::updateCurrentState() { return; } + if (show_ended_) + { + return; + } if (current_state_.init_new_set_) { current_state_.slide_set_index_++; if (current_state_.slide_set_index_ >= (int)app_settings_.slide_sets_.size()) { - logEvent("APP_END", "reason=end_of_slide_show"); - // TODO: consider if we want an end slide - should_exit_ = true; + logEvent("APP_END", {"reason=end_of_slide_show"}); + show_ended_ = true; + current_state_.slide_state_ = + CurrentState::SlideState::kSlideOff; return; } current_state_.slide_index_ = @@ -357,10 +373,11 @@ void ofApp::updateCurrentState() if (current_state_.slide_paths_.empty()) { - logEvent( - "WARNING", - "set_index=" + std::to_string(current_state_.slide_set_index_) + - " reason=no_slides_found dir=" + current_state_.slide_dir_); + logEvent("WARNING", + {"set_index=" + + std::to_string(current_state_.slide_set_index_), + "reason=no_slides_found", + "dir=" + current_state_.slide_dir_}); current_state_.init_new_set_ = true; // skip to next set return; } @@ -421,15 +438,16 @@ void ofApp::updateCurrentState() std::string slide_list; for (const auto& path : current_state_.slide_paths_) { - slide_list += path + "|"; + slide_list += path + ";"; } - logEvent( - "SLIDE_SET_INIT", - "set_index=" + std::to_string(current_state_.slide_set_index_) + - " intro_slide=" + intro_path + " background=" + - current_state_.slide_settings_.background_ + " slide_count=" + - std::to_string(current_state_.slide_paths_.size()) + - " slides=" + slide_list); + logEvent("SLIDE_SET_INIT", + {"set_index=" + + std::to_string(current_state_.slide_set_index_), + "intro_slide=" + intro_path, + "background=" + current_state_.slide_settings_.background_, + "slide_count=" + + std::to_string(current_state_.slide_paths_.size()), + "slides=" + slide_list}); // use existing machinery to advance to the next slide changeSlide(1); } @@ -448,34 +466,24 @@ void ofApp::updateCurrentState() current_state_.slide_state_ = CurrentState::SlideState::kSlideOff; current_state_.phase_start_msec_ = get_time_msec_(); - logEvent( - "SLIDE_OFF", - "set_index=" + - std::to_string(current_state_.slide_set_index_) + - " slide_index=" + - std::to_string(current_state_.slide_index_) + - " OFF_TIME=" + - std::to_string(current_state_.state_times_.off_time_)); + logEvent("SLIDE_OFF", + {"set_index=" + std::to_string( + current_state_.slide_set_index_), + "slide_index=" + + std::to_string(current_state_.slide_index_), + "OFF_TIME=" + std::to_string( + current_state_.state_times_.off_time_)}); } } } if (CurrentState::SlideState::kSlideOff == current_state_.slide_state_) { - if (current_state_.slide_index_ == 0) + if ((float)get_time_msec_() - + (float)current_state_.phase_start_msec_ > + current_state_.state_times_.off_time_) { - // increment without wait if this is an intro slide - // no background for intro slide changeSlide(1); } - else - { - if ((float)get_time_msec_() - - (float)current_state_.phase_start_msec_ > - current_state_.state_times_.off_time_) - { - changeSlide(1); - } - } } } @@ -514,24 +522,16 @@ void ofApp::changeSlide(int delta) (int)on_time_max); current_state_.state_times_.on_time_ = (float)dist(rng); } - if (current_state_.slide_index_ == 0) + if (off_time_min == off_time_max) { - // NOTE: intro slide has no off time - current_state_.state_times_.off_time_ = 0; + current_state_.state_times_.off_time_ = off_time_max; } else { - if (off_time_min == off_time_max) - { - current_state_.state_times_.off_time_ = off_time_max; - } - else - { - std::mt19937 rng{std::random_device{}()}; - std::uniform_int_distribution dist((int)off_time_min, - (int)off_time_max); - current_state_.state_times_.off_time_ = (float)dist(rng); - } + std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution dist((int)off_time_min, + (int)off_time_max); + current_state_.state_times_.off_time_ = (float)dist(rng); } current_state_.phase_start_msec_ = get_time_msec_(); if (current_state_.slide_index_ >= (int)current_state_.slide_paths_.size()) @@ -545,12 +545,45 @@ void ofApp::changeSlide(int delta) current_state_.slide_paths_[current_state_.slide_index_]); logEvent( "SLIDE_ON", - "set_index=" + std::to_string(current_state_.slide_set_index_) + - " slide_index=" + std::to_string(current_state_.slide_index_) + - " path=" + - current_state_.slide_paths_[current_state_.slide_index_] + - " ON_TIME=" + - std::to_string(current_state_.state_times_.on_time_)); + {"set_index=" + std::to_string(current_state_.slide_set_index_), + "slide_index=" + std::to_string(current_state_.slide_index_), + "path=" + + current_state_.slide_paths_[current_state_.slide_index_], + "ON_TIME=" + + std::to_string(current_state_.state_times_.on_time_)}); + bool paused_this_slide = false; + if (!beginning_pause_applied_ && app_settings_.start_paused_ && + current_state_.slide_set_index_ == 0 && + current_state_.slide_index_ == 0) + { + beginning_pause_applied_ = true; + paused_this_slide = true; + current_state_.slide_state_before_pause_ = + CurrentState::SlideState::kSlideOn; + current_state_.slide_state_ = CurrentState::SlideState::kSlidePause; + current_state_.time_since_phase_start_on_pause_ = 0; + logEvent("PAUSE", + {"set_index=" + + std::to_string(current_state_.slide_set_index_), + "slide_index=" + + std::to_string(current_state_.slide_index_), + "reason=pause_at_beginning"}); + } + if (!paused_this_slide && current_state_.slide_index_ == 0 && + !current_state_.slide_settings_.slide_set_intro_slide_.empty() && + current_state_.slide_settings_.pause_on_set_intro_slide_) + { + current_state_.slide_state_before_pause_ = + CurrentState::SlideState::kSlideOn; + current_state_.slide_state_ = CurrentState::SlideState::kSlidePause; + current_state_.time_since_phase_start_on_pause_ = 0; + logEvent("PAUSE", + {"set_index=" + + std::to_string(current_state_.slide_set_index_), + "slide_index=" + + std::to_string(current_state_.slide_index_), + "reason=pause_on_intro_slide"}); + } } } @@ -559,7 +592,12 @@ void ofApp::changeSlide(int delta) void ofApp::draw() { - if (CurrentState::SlideState::kSlideOff == current_state_.slide_state_) + const CurrentState::SlideState display_state = + (current_state_.slide_state_ == CurrentState::SlideState::kSlidePause) + ? current_state_.slide_state_before_pause_ + : current_state_.slide_state_; + + if (display_state == CurrentState::SlideState::kSlideOff) { drawImageFitted(background_image_); } @@ -590,16 +628,24 @@ void ofApp::drawImageFitted(const ofImage& img) // ───────────────────────────────────────────────────────────────────── void ofApp::keyPressed(int key) +{ +} + +void ofApp::keyReleased(int key) { // TODO: Time since phase change should be calculate for boath ON and OFF - // Modifier keys (Shift, Ctrl, etc.) produce large keycodes outside the - // printable ASCII range — skip them to avoid spurious log entries. - if (key > 127) + // Only handle printable ASCII (32–126). Control characters (including + // modifier key releases) and values above 126 are silently ignored. + if (key < 32 || key > 126) + { + return; + } + if (show_ended_) { return; } - const char kKeyChar = static_cast(toupper(key)); - logEvent("KEY_PRESS", std::string("key=") + kKeyChar); + const char kKeyChar = static_cast(key); + logEvent("KEY_RELEASE", {std::string("key=") + kKeyChar}); if (app_settings_.keyboard_controls_.next_slide_ == kKeyChar) { changeSlide(1); @@ -614,6 +660,7 @@ void ofApp::keyPressed(int key) } if (app_settings_.keyboard_controls_.restart_slide_show_ == kKeyChar) { + beginning_pause_applied_ = false; current_state_.slide_set_index_ = -1; current_state_.init_new_set_ = true; } @@ -628,11 +675,11 @@ void ofApp::keyPressed(int key) current_state_.time_since_phase_start_on_pause_ = (float)get_time_msec_() - (float)current_state_.phase_start_msec_; - logEvent( - "PAUSE", - "set_index=" + std::to_string(current_state_.slide_set_index_) + - " slide_index=" + - std::to_string(current_state_.slide_index_)); + logEvent("PAUSE", + {"set_index=" + + std::to_string(current_state_.slide_set_index_), + "slide_index=" + + std::to_string(current_state_.slide_index_)}); } else { @@ -641,17 +688,91 @@ void ofApp::keyPressed(int key) current_state_.phase_start_msec_ = get_time_msec_() - (uint64_t)current_state_.time_since_phase_start_on_pause_; - logEvent( - "RESUME", - "set_index=" + std::to_string(current_state_.slide_set_index_) + - " slide_index=" + - std::to_string(current_state_.slide_index_)); + logEvent("RESUME", + {"set_index=" + + std::to_string(current_state_.slide_set_index_), + "slide_index=" + + std::to_string(current_state_.slide_index_)}); } } -} + if (app_settings_.keyboard_controls_.toggle_full_screen_ == kKeyChar) + { + bool going_fullscreen = (ofGetWindowMode() == OF_WINDOW); + ofToggleFullscreen(); + logEvent("FULL_SCREEN", + {std::string("state=") + (going_fullscreen ? "on" : "off")}); + } + if (app_settings_.keyboard_controls_.load_settings_file_ == kKeyChar) + { + // Freeze timing before the blocking dialog so wall-clock time + // consumed by the dialog does not advance the slide. + const auto kStateBeforeDialog = current_state_.slide_state_; + const float kElapsedBeforeDialog = + (float)get_time_msec_() - (float)current_state_.phase_start_msec_; + current_state_.slide_state_ = CurrentState::SlideState::kSlidePause; -void ofApp::keyReleased(int key) -{ + std::string chosen_path = open_file_dialog_(); + + if (chosen_path.empty()) + { + // User cancelled — restore timing as if no time passed. + current_state_.slide_state_ = kStateBeforeDialog; + current_state_.phase_start_msec_ = + get_time_msec_() - (uint64_t)kElapsedBeforeDialog; + } + else + { + settings_file_name_ = chosen_path; + if (loadAppSettings()) + { + logEvent("SETTINGS_RELOAD", + {"file=" + settings_file_name_, + "settings=" + settings_json_}); + beginning_pause_applied_ = false; + current_state_.slide_set_index_ = -1; + current_state_.init_new_set_ = true; + current_state_.slide_state_ = + CurrentState::SlideState::kSlideOn; + } + else + { + // Bad file — restore timing so show resumes unchanged. + current_state_.slide_state_ = kStateBeforeDialog; + current_state_.phase_start_msec_ = + get_time_msec_() - (uint64_t)kElapsedBeforeDialog; + } + } + } + if (app_settings_.keyboard_controls_.set_log_file_directory_ == kKeyChar) + { + const auto kStateBeforeDialog = current_state_.slide_state_; + const float kElapsedBeforeDialog = + (float)get_time_msec_() - (float)current_state_.phase_start_msec_; + current_state_.slide_state_ = CurrentState::SlideState::kSlidePause; + + std::string chosen_dir = open_directory_dialog_(); + + if (chosen_dir.empty() || !ofDirectory::doesDirectoryExist(chosen_dir)) + { + current_state_.slide_state_ = kStateBeforeDialog; + current_state_.phase_start_msec_ = + get_time_msec_() - (uint64_t)kElapsedBeforeDialog; + } + else + { + if (chosen_dir.back() != '/') + chosen_dir += '/'; + event_log_.close(); + app_settings_.log_file_directory_ = chosen_dir; + startLogToFile(); + logEvent("LOG_DIR_SET", {"dir=" + chosen_dir}); + show_ended_ = false; + beginning_pause_applied_ = false; + current_state_.slide_set_index_ = -1; + current_state_.init_new_set_ = true; + current_state_.slide_state_ = CurrentState::SlideState::kSlideOn; + } + } } void ofApp::mouseMoved(int x, int y) diff --git a/EmotiBitSlidePlayer/src/ofApp.h b/EmotiBitSlidePlayer/src/ofApp.h index 659294b..0cab2ca 100644 --- a/EmotiBitSlidePlayer/src/ofApp.h +++ b/EmotiBitSlidePlayer/src/ofApp.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -168,8 +169,12 @@ class ofApp : public ofBaseApp AppSettings app_settings_; /// @brief Live runtime state of the slide show. CurrentState current_state_; - /// @brief When true, @c update() will call @c ofExit(). - bool should_exit_ = false; + /// @brief Set to true when the last slide set has finished. Freezes + /// @c updateCurrentState() and disables key input. + bool show_ended_ = false; + /// @brief Set to true once the pauseAtBeginning pause has fired. + /// Cleared whenever the show is restarted from the beginning. + bool beginning_pause_applied_ = false; /// @brief File name of the JSON settings file. std::string settings_file_name_ = "emotibitSlidePlayerSettings.json"; /// @brief Compact single-line JSON snapshot of loaded settings, written to @@ -184,6 +189,17 @@ class ofApp : public ofBaseApp std::function get_timestamp_ = []() { return ofGetTimestampString(); }; + /// @brief Returns the current Unix epoch time in milliseconds (UTC, DST-free). + /// Injectable for testing. + std::function get_epoch_msec_ = []() -> uint64_t + { + using namespace std::chrono; + return static_cast( + duration_cast( + system_clock::now().time_since_epoch()) + .count()); + }; + /// @brief Loads and returns sorted image paths from a directory. Injectable /// for testing. std::function(const std::string&)> @@ -221,6 +237,23 @@ class ofApp : public ofBaseApp std::function load_background_image_ = [this](const std::string& path) { background_image_.load(path); }; + /// @brief Opens a file picker dialog and returns the chosen path, or empty + /// string if the user cancelled. Injectable for testing. + std::function open_file_dialog_ = []() -> std::string + { + ofFileDialogResult result = ofSystemLoadDialog("Select Settings File"); + return result.bSuccess ? result.filePath : ""; + }; + + /// @brief Opens a folder picker dialog and returns the chosen directory + /// path, or empty string if the user cancelled. Injectable for testing. + std::function open_directory_dialog_ = []() -> std::string + { + ofFileDialogResult result = + ofSystemLoadDialog("Select Log Directory", true); + return result.bSuccess ? result.filePath : ""; + }; + /// @brief Currently displayed slide image. ofImage current_slide_image_; /// @brief Background image shown during slide-off intervals. @@ -230,6 +263,8 @@ class ofApp : public ofBaseApp /// @brief Pointer to the active log output stream. Points to @c event_log_ /// after file open. Injectable for testing. std::ostream* log_stream_ = nullptr; + /// @brief Delimiter used between key=value fields in the log details column. + std::string log_details_delimiter_ = "|"; // ── Setup // ───────────────────────────────────────────────────────────────── @@ -260,7 +295,8 @@ class ofApp : public ofBaseApp /// @brief Writes a single event row to the CSV log and echoes it to stdout. /// @param event Event name (e.g. "SLIDE_ON", "KEY_PRESS"). /// @param details Space-separated key=value pairs describing the event. - void logEvent(const std::string& event, const std::string& details); + void logEvent(const std::string& event, + const std::vector& details); // ── Per-frame // ───────────────────────────────────────────────────────────── diff --git a/EmotiBitSlidePlayer/tests/test_parseSettings.cpp b/EmotiBitSlidePlayer/tests/test_parseSettings.cpp index 21c7c48..6ae67f9 100644 --- a/EmotiBitSlidePlayer/tests/test_parseSettings.cpp +++ b/EmotiBitSlidePlayer/tests/test_parseSettings.cpp @@ -59,6 +59,33 @@ TEST_CASE("per-set inherits global when not overridden", "[parseSettings]") 5); } +TEST_CASE("reloading settings replaces slide sets instead of appending", + "[parseSettings]") +{ + Json::Value root1; + root1["globalSlideSettings"]["maxSlidesPerSet"] = 5; + root1["slideSets"][0]["slideDirectory"] = "./set01/"; + root1["slideSets"][0]["maxSlidesPerSet"] = 5; + root1["slideSets"][1]["slideDirectory"] = "./set02/"; + + ofApp app; + app.parseSettings(root1); + REQUIRE(app.app_settings_.slide_sets_.size() == 2); + + Json::Value root2; + root2["globalSlideSettings"]["maxSlidesPerSet"] = 5; + root2["slideSets"][0]["slideDirectory"] = "./set01/"; + root2["slideSets"][0]["maxSlidesPerSet"] = 3; // simulates the user's edit + + app.parseSettings(root2); // simulates pressing 'S' to reload + + REQUIRE(app.app_settings_.slide_sets_.size() == 1); + REQUIRE(app.app_settings_.slide_sets_[0].slide_directory_ == + ofToDataPath("./set01/", true)); + REQUIRE(app.app_settings_.slide_sets_[0].settings_.max_slides_per_set_ == + 3); +} + TEST_CASE("slide set without slideDirectory is skipped", "[parseSettings]") { Json::Value root; diff --git a/EmotiBitSlidePlayer/tests/test_updateCurrentState.cpp b/EmotiBitSlidePlayer/tests/test_updateCurrentState.cpp index 5e20cdb..f3e735d 100644 --- a/EmotiBitSlidePlayer/tests/test_updateCurrentState.cpp +++ b/EmotiBitSlidePlayer/tests/test_updateCurrentState.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "ofApp.h" @@ -26,6 +27,11 @@ static ofApp makeApp(std::vector slide_paths, // inject controllable clock and fixed timestamp app.get_time_msec_ = [&fake_time]() { return fake_time; }; app.get_timestamp_ = []() { return std::string("2026-01-01T00:00:00"); }; + app.get_epoch_msec_ = []() -> uint64_t { return 1000000ULL; }; + app.open_directory_dialog_ = []() -> std::string { return ""; }; + + // disable start_paused so existing tests are not affected + app.app_settings_.start_paused_ = false; // one slide set with the given timing ofApp::AppSettings::SlideSet ss; @@ -43,6 +49,22 @@ static ofApp makeApp(std::vector slide_paths, return app; } +static ofApp makeAppWithIntro(std::vector slide_paths, + uint64_t& fake_time, + bool pause_on_intro, + float slide_on_time_max_msec = 1000.0f, + float slide_off_time_max_msec = 500.0f, + std::ostream* log_stream = nullptr) +{ + ofApp app = makeApp(slide_paths, fake_time, slide_on_time_max_msec, + slide_off_time_max_msec, log_stream); + app.app_settings_.slide_sets_[0].settings_.slide_set_intro_slide_ = + slide_paths[0]; + app.app_settings_.slide_sets_[0].settings_.pause_on_set_intro_slide_ = + pause_on_intro; + return app; +} + // ── Tests: set initialisation // ───────────────────────────────────────────────────── @@ -73,16 +95,37 @@ TEST_CASE("slide state is ON after set init", "[updateCurrentState]") // ── Tests: ON → OFF transition // ──────────────────────────────────────────────── -TEST_CASE("intro slide advances immediately when ON time expires", +TEST_CASE("intro slide transitions to SLIDE_OFF when ON time expires", "[updateCurrentState]") { uint64_t fake_time = 0; - ofApp app = makeApp({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); + ofApp app = makeApp({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f, + /*off=*/500.0f); - app.updateCurrentState(); // init, t=0, index=0 (intro), ON + app.updateCurrentState(); // init, t=0, index=0, ON fake_time = 1500; - app.updateCurrentState(); // intro expires → skips OFF → index=1, ON + app.updateCurrentState(); // 1500 > 1000 → kSlideOff, index still 0 + + REQUIRE(app.current_state_.slide_index_ == 0); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOff); +} + +TEST_CASE("intro slide advances to next slide after off_time expires", + "[updateCurrentState]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f, + /*off=*/500.0f); + + app.updateCurrentState(); // init, t=0, index=0, ON + + fake_time = 1500; + app.updateCurrentState(); // ON expires → kSlideOff, phase=1500 + + fake_time = 2100; + app.updateCurrentState(); // 2100-1500=600 > 500 → changeSlide(1) → index=1, ON REQUIRE(app.current_state_.slide_index_ == 1); REQUIRE(app.current_state_.slide_state_ == @@ -93,15 +136,19 @@ TEST_CASE("slide transitions to OFF after max_on_time", "[updateCurrentState]") { uint64_t fake_time = 0; ofApp app = - makeApp({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); + makeApp({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f, + /*off=*/500.0f); - app.updateCurrentState(); // init, t=0, index=0 (intro), ON + app.updateCurrentState(); // init, t=0, index=0, ON fake_time = 1500; - app.updateCurrentState(); // intro expires → index=1, ON, phase=1500 + app.updateCurrentState(); // slide 0 ON expires → kSlideOff, phase=1500 - fake_time = 3000; - app.updateCurrentState(); // index=1: 3000-1500=1500 > 1000 → OFF + fake_time = 2100; + app.updateCurrentState(); // slide 0 OFF expires → index=1, ON, phase=2100 + + fake_time = 3200; + app.updateCurrentState(); // 3200-2100=1100 > 1000 → kSlideOff REQUIRE(app.current_state_.slide_state_ == ofApp::CurrentState::SlideState::kSlideOff); @@ -111,15 +158,19 @@ TEST_CASE("slide stays ON before max_on_time elapses", "[updateCurrentState]") { uint64_t fake_time = 0; ofApp app = - makeApp({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); + makeApp({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f, + /*off=*/500.0f); - app.updateCurrentState(); // init, index=0 (intro), ON + app.updateCurrentState(); // init, index=0, ON fake_time = 1500; - app.updateCurrentState(); // intro expires → index=1, ON, phase=1500 + app.updateCurrentState(); // slide 0 ON expires → kSlideOff, phase=1500 - fake_time = 2000; - app.updateCurrentState(); // 2000-1500=500 < 1000 → stays ON + fake_time = 2100; + app.updateCurrentState(); // slide 0 OFF expires → index=1, ON, phase=2100 + + fake_time = 2500; + app.updateCurrentState(); // 2500-2100=400 < 1000 → stays ON REQUIRE(app.current_state_.slide_state_ == ofApp::CurrentState::SlideState::kSlideOn); @@ -149,16 +200,19 @@ TEST_CASE("slide advances after max_off_time", "[updateCurrentState]") ofApp app = makeApp({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f, /*off=*/500.0f); - app.updateCurrentState(); // init, t=0, index=0 (intro), ON + app.updateCurrentState(); // init, t=0, index=0, ON fake_time = 1500; - app.updateCurrentState(); // intro expires → index=1, ON, phase=1500 + app.updateCurrentState(); // slide 0 ON expires → kSlideOff, phase=1500 - fake_time = 2600; - app.updateCurrentState(); // index=1 ON: 2600-1500=1100 > 1000 → OFF, phase=2600 + fake_time = 2100; + app.updateCurrentState(); // slide 0 OFF expires → index=1, ON, phase=2100 fake_time = 3200; - app.updateCurrentState(); // OFF: 3200-2600=600 > 500 → index=2, ON + app.updateCurrentState(); // index=1 ON: 3200-2100=1100 > 1000 → OFF, phase=3200 + + fake_time = 3800; + app.updateCurrentState(); // OFF: 3800-3200=600 > 500 → index=2, ON REQUIRE(app.current_state_.slide_index_ == 2); REQUIRE(app.current_state_.slide_state_ == @@ -168,7 +222,7 @@ TEST_CASE("slide advances after max_off_time", "[updateCurrentState]") // ── Tests: pause / resume // ───────────────────────────────────────────────────── -TEST_CASE("pause stops state transitions", "[keyPressed]") +TEST_CASE("pause stops state transitions", "[keyReleased]") { uint64_t fake_time = 0; ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); @@ -177,7 +231,7 @@ TEST_CASE("pause stops state transitions", "[keyPressed]") app.updateCurrentState(); // init // pause - app.keyPressed('P'); + app.keyReleased('P'); REQUIRE(app.current_state_.slide_state_ == ofApp::CurrentState::SlideState::kSlidePause); @@ -189,15 +243,15 @@ TEST_CASE("pause stops state transitions", "[keyPressed]") ofApp::CurrentState::SlideState::kSlidePause); } -TEST_CASE("resume restores previous state", "[keyPressed]") +TEST_CASE("resume restores previous state", "[keyReleased]") { uint64_t fake_time = 0; ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); app.updateCurrentState(); // init, state = ON - app.keyPressed('P'); // pause - app.keyPressed('P'); // resume + app.keyReleased('P'); // pause + app.keyReleased('P'); // resume REQUIRE(app.current_state_.slide_state_ == ofApp::CurrentState::SlideState::kSlideOn); @@ -206,26 +260,54 @@ TEST_CASE("resume restores previous state", "[keyPressed]") // ── Tests: manual slide navigation // ─────────────────────────────────────── -TEST_CASE("next key advances slide index", "[keyPressed]") +TEST_CASE("next key advances slide index", "[keyReleased]") { uint64_t fake_time = 0; ofApp app = makeApp({"a.jpg", "b.jpg", "c.jpg"}, fake_time); app.updateCurrentState(); // init - app.keyPressed('N'); + app.keyReleased('N'); REQUIRE(app.current_state_.slide_index_ == 1); } -TEST_CASE("previous key does not go below 0", "[keyPressed]") +TEST_CASE("previous key does not go below 0", "[keyReleased]") { uint64_t fake_time = 0; ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); app.updateCurrentState(); // init, slide_index = 0 - app.keyPressed('B'); + app.keyReleased('B'); + + REQUIRE(app.current_state_.slide_index_ == 0); +} + +// ── Tests: case sensitivity +// ─────────────────────────────────────────────────── + +TEST_CASE("lowercase pause key does not pause", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); + + app.updateCurrentState(); // init, state = ON + + app.keyReleased('p'); // lowercase — should not match default 'P' + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("lowercase next key does not advance slide", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg", "c.jpg"}, fake_time); + + app.updateCurrentState(); // init, slide_index = 0 + + app.keyReleased('n'); // lowercase — should not match default 'N' REQUIRE(app.current_state_.slide_index_ == 0); } @@ -240,7 +322,7 @@ TEST_CASE("PAUSE event is written to log stream", "[logEvent]") ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, 1000.0f, 500.0f, &log); app.updateCurrentState(); - app.keyPressed('P'); + app.keyReleased('P'); REQUIRE(log.str().find("PAUSE") != std::string::npos); } @@ -252,8 +334,8 @@ TEST_CASE("RESUME event is written to log stream", "[logEvent]") ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, 1000.0f, 500.0f, &log); app.updateCurrentState(); - app.keyPressed('P'); // pause - app.keyPressed('P'); // resume + app.keyReleased('P'); // pause + app.keyReleased('P'); // resume REQUIRE(log.str().find("RESUME") != std::string::npos); } @@ -265,7 +347,327 @@ TEST_CASE("SLIDE_ON event is written to log stream on advance", "[logEvent]") ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, 1000.0f, 500.0f, &log); app.updateCurrentState(); - app.keyPressed('N'); + app.keyReleased('N'); REQUIRE(log.str().find("SLIDE_ON") != std::string::npos); } + +// ── Tests: pause_on_set_intro_slide +// ────────────────────────────────────────── + +TEST_CASE("pause_on_set_intro_slide pauses on intro slide", "[updateCurrentState]") +{ + uint64_t fake_time = 0; + ofApp app = makeAppWithIntro({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, + /*pause_on_intro=*/true); + + app.updateCurrentState(); // init → lands on intro (index 0) → auto-pause + + REQUIRE(app.current_state_.slide_index_ == 0); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlidePause); +} + +TEST_CASE("pause_on_set_intro_slide resumes to ON after key press", "[updateCurrentState]") +{ + uint64_t fake_time = 0; + ofApp app = makeAppWithIntro({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, + /*pause_on_intro=*/true); + + app.updateCurrentState(); // init → auto-pause on intro + app.keyReleased('P'); // resume + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("pause_on_set_intro_slide=false does not pause on intro slide", "[updateCurrentState]") +{ + uint64_t fake_time = 0; + ofApp app = makeAppWithIntro({"intro.jpg", "a.jpg", "b.jpg"}, fake_time, + /*pause_on_intro=*/false); + + app.updateCurrentState(); // init → intro shown, no pause + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +// ── Tests: load settings file ('S') +// ────────────────────────────────────────── + +TEST_CASE("load settings: cancel leaves slide state unchanged", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + app.open_file_dialog_ = []() { return std::string(""); }; + + app.updateCurrentState(); // init, state = ON + + app.keyReleased('S'); + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("load settings: cancel restores timing so slide does not advance early", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); + app.open_file_dialog_ = [&fake_time]() + { + fake_time += 5000; // simulate dialog open for 5 seconds + return std::string(""); + }; + + app.updateCurrentState(); // init at t=0, ON + + fake_time = 800; // 800ms into the 1000ms on-time + app.keyReleased('S'); // dialog consumes 5s but cancel → timing restored + + fake_time += 100; // only 100ms more after dialog — should not advance + app.updateCurrentState(); + + REQUIRE(app.current_state_.slide_index_ == 0); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("load settings: valid file updates settings_file_name_", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + const std::string kTmpPath = "/tmp/test_settings.json"; + std::ofstream f(kTmpPath); + f << "{}"; + f.close(); + app.open_file_dialog_ = [&kTmpPath]() { return kTmpPath; }; + + app.updateCurrentState(); + app.keyReleased('S'); + + REQUIRE(app.settings_file_name_ == kTmpPath); +} + +TEST_CASE("load settings: valid file restarts slide show", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + const std::string kTmpPath = "/tmp/test_settings.json"; + std::ofstream f(kTmpPath); + f << "{}"; + f.close(); + app.open_file_dialog_ = [&kTmpPath]() { return kTmpPath; }; + + app.updateCurrentState(); + app.keyReleased('S'); + + REQUIRE(app.current_state_.slide_set_index_ == -1); + REQUIRE(app.current_state_.init_new_set_ == true); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("load settings: bad file restores slide state and timing", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); + app.open_file_dialog_ = []() { return std::string("/nonexistent/bad.json"); }; + + app.updateCurrentState(); // init at t=0, ON + + fake_time = 500; + app.keyReleased('S'); // bad file → timing restored + + fake_time += 300; // 800ms total into 1000ms on-time — should not advance + app.updateCurrentState(); + + REQUIRE(app.current_state_.slide_index_ == 0); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +// ── Tests: end of show +// ──────────────────────────────────────────────────────── + +TEST_CASE("show ends with background visible and keys disabled", + "[updateCurrentState]") +{ + uint64_t fake_time = 0; + // Two real slides (no intro) so we can exhaust the set quickly + ofApp app = + makeApp({"a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f, /*off=*/500.0f); + + app.updateCurrentState(); // init, index=0, ON, phase=0 + + fake_time = 1500; + app.updateCurrentState(); // slide 0 ON expires → kSlideOff, phase=1500 + + fake_time = 2100; + app.updateCurrentState(); // slide 0 OFF expires → index=1, ON, phase=2100 + + fake_time = 3200; + app.updateCurrentState(); // slide 1 ON expires → kSlideOff, phase=3200 + + fake_time = 3800; + app.updateCurrentState(); // slide 1 OFF expires → changeSlide(1) out-of-bounds + // → init_new_set_=true, state still kSlideOff + app.updateCurrentState(); // init block: past last set → show_ended_=true, kSlideOff + + REQUIRE(app.show_ended_ == true); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOff); + + // Further updates must not change state + fake_time = 99999; + app.updateCurrentState(); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOff); + + // Key presses must be silently ignored + app.keyReleased('N'); + app.keyReleased('B'); + app.keyReleased('P'); + REQUIRE(app.show_ended_ == true); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOff); +} + +// ── Tests: set log directory ('L') ─────────────────────────────────────────── + +TEST_CASE("L key: cancel leaves slide state unchanged", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + app.open_directory_dialog_ = []() -> std::string { return ""; }; + + app.updateCurrentState(); // init, state = ON + + app.keyReleased('L'); + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("L key: non-directory path is ignored", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + app.open_directory_dialog_ = []() -> std::string + { return "/nonexistent/path_xyz_notadir"; }; + + app.updateCurrentState(); + + app.keyReleased('L'); + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("L key: cancel restores timing so slide does not advance early", + "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time, /*on=*/1000.0f); + app.open_directory_dialog_ = [&fake_time]() -> std::string + { + fake_time += 5000; // simulate dialog open for 5 seconds + return ""; + }; + + app.updateCurrentState(); // init at t=0, ON + + fake_time = 500; + app.keyReleased('L'); // dialog consumes 5000ms → t=5500, cancel + + fake_time += 300; // 800ms into on-time — should not advance + app.updateCurrentState(); + + REQUIRE(app.current_state_.slide_index_ == 0); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("L key: valid directory restarts slide show", "[keyReleased]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + app.open_directory_dialog_ = []() -> std::string { return "/tmp"; }; + + app.updateCurrentState(); // init + app.keyReleased('N'); // advance to slide 1 + app.updateCurrentState(); + + app.keyReleased('L'); + + REQUIRE(app.app_settings_.log_file_directory_ == "/tmp/"); + REQUIRE(app.current_state_.slide_set_index_ == -1); + REQUIRE(app.current_state_.init_new_set_ == true); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +// ── Tests: startPaused ──────────────────────────────────────────────────────── + +TEST_CASE("startPaused: pauses on first slide of first set", + "[updateCurrentState]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + app.app_settings_.start_paused_ = true; + + app.updateCurrentState(); + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlidePause); + REQUIRE(app.current_state_.slide_index_ == 0); + REQUIRE(app.beginning_pause_applied_ == true); +} + +TEST_CASE("startPaused: resumes normally after key press", "[updateCurrentState]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + app.app_settings_.start_paused_ = true; + + app.updateCurrentState(); // paused at slide 0 + app.keyReleased('P'); // resume + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("startPaused: does not pause again after advancing to next slide", + "[updateCurrentState]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + app.app_settings_.start_paused_ = true; + + app.updateCurrentState(); // paused at slide 0 + app.keyReleased('P'); // resume + app.keyReleased('N'); // advance to slide 1 + + REQUIRE(app.current_state_.slide_index_ == 1); + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlideOn); +} + +TEST_CASE("startPaused: fires again after show restart with R", + "[updateCurrentState]") +{ + uint64_t fake_time = 0; + ofApp app = makeApp({"a.jpg", "b.jpg"}, fake_time); + app.app_settings_.start_paused_ = true; + + app.updateCurrentState(); // paused at slide 0 + app.keyReleased('P'); // resume + app.keyReleased('N'); // advance + app.keyReleased('R'); // restart → clears beginning_pause_applied_ + + app.updateCurrentState(); // init set 0 again → should pause + + REQUIRE(app.current_state_.slide_state_ == + ofApp::CurrentState::SlideState::kSlidePause); + REQUIRE(app.beginning_pause_applied_ == true); +} diff --git a/src/ofxEmotiBitVersion.h b/src/ofxEmotiBitVersion.h index 95b61d5..b78761f 100644 --- a/src/ofxEmotiBitVersion.h +++ b/src/ofxEmotiBitVersion.h @@ -2,7 +2,7 @@ //#include #include "ofMain.h" -const std::string ofxEmotiBitVersion = "1.16.5"; +const std::string ofxEmotiBitVersion = "1.16.23"; static const char SOFTWARE_VERSION_PREFIX = 'v';