diff --git a/CHANGELOG.md b/CHANGELOG.md index d534da48c..6fd96758f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ in the format YYMM.X ### Changed ### Fixed +## [2504.3] - 2025-05-10 +### Added +- Ambient Mode, allowing screensaver-type imagery to be displayed on main and marquee monitors [@andre](https://github.com/andre)) + + ## [2504.2] - 2025-05-05 ### Changed - Delete the Configuration RetroFE tool, it isn't used at all, never made it past production and will only ever work on Windows. A better solution in the form of Qt in C++ is coming shortly [@aidenjbass](https://github.com/aidenjbass) diff --git a/Package/Environment/Common/ambient/default_marquee.png b/Package/Environment/Common/ambient/default_marquee.png new file mode 100644 index 000000000..c19bb660f Binary files /dev/null and b/Package/Environment/Common/ambient/default_marquee.png differ diff --git a/Package/Environment/Common/ambient/landscape_1.jpeg b/Package/Environment/Common/ambient/landscape_1.jpeg new file mode 100644 index 000000000..335d1fb40 Binary files /dev/null and b/Package/Environment/Common/ambient/landscape_1.jpeg differ diff --git a/Package/Environment/Common/ambient/landscape_2.jpg b/Package/Environment/Common/ambient/landscape_2.jpg new file mode 100644 index 000000000..d26df16d8 Binary files /dev/null and b/Package/Environment/Common/ambient/landscape_2.jpg differ diff --git a/Package/Environment/Common/ambient/landscape_3.jpg b/Package/Environment/Common/ambient/landscape_3.jpg new file mode 100644 index 000000000..10aa6ba1b Binary files /dev/null and b/Package/Environment/Common/ambient/landscape_3.jpg differ diff --git a/Package/Environment/Common/settings.conf b/Package/Environment/Common/settings.conf index 17e1b310a..850ce9b87 100644 --- a/Package/Environment/Common/settings.conf +++ b/Package/Environment/Common/settings.conf @@ -93,7 +93,7 @@ attractModeFast=false collectionInputClear=false playlistInputClear=false jumpInputClear=false -controllerComboExit=true +controllerComboExitAction=QUIT controllerComboSettings=false settingsCollectionPlaylist=Arcades:settings diff --git a/RetroFE/Source/CMakeLists.txt b/RetroFE/Source/CMakeLists.txt index 9e24b0b99..b3c0498a9 100644 --- a/RetroFE/Source/CMakeLists.txt +++ b/RetroFE/Source/CMakeLists.txt @@ -60,6 +60,11 @@ if(WIN32) find_package(SDL2_ttf REQUIRED) find_package(ZLIB REQUIRED) + + include_directories(${GSTREAMER_INCLUDE_DIRS}) + link_directories(${GSTREAMER_LIBRARY_DIRS}) + + elseif(APPLE) find_package(Glib2 REQUIRED) find_package(GStreamer REQUIRED) @@ -205,6 +210,7 @@ set(RETROFE_HEADERS "${RETROFE_DIR}/Source/Database/GlobalOpts.h" "${RETROFE_DIR}/Source/Database/HiScores.h" "${RETROFE_DIR}/Source/Execute/AttractMode.h" + "${RETROFE_DIR}/Source/Execute/AmbientMode.h" "${RETROFE_DIR}/Source/Execute/Launcher.h" "${RETROFE_DIR}/Source/Graphics/Animate/Tween.h" "${RETROFE_DIR}/Source/Graphics/Animate/TweenTypes.h" @@ -261,6 +267,7 @@ set(RETROFE_SOURCES "${RETROFE_DIR}/Source/Database/HiScores.cpp" "${RETROFE_DIR}/Source/Database/MetadataDatabase.cpp" "${RETROFE_DIR}/Source/Execute/AttractMode.cpp" + "${RETROFE_DIR}/Source/Execute/AmbientMode.cpp" "${RETROFE_DIR}/Source/Execute/Launcher.cpp" "${RETROFE_DIR}/Source/Graphics/Font.cpp" "${RETROFE_DIR}/Source/Graphics/FontCache.cpp" @@ -305,7 +312,7 @@ set(LIBRARY_OUTPUT_PATH "${RETROFE_DIR}/Build" CACHE PATH "Build directory" FORC include_directories(${RETROFE_INCLUDE_DIRS}) add_executable(retrofe ${RETROFE_SOURCES} ${RETROFE_HEADERS}) add_dependencies(retrofe GenerateVersioningHeader) - +target_link_libraries(retrofe ${GSTREAMER_LIBRARIES}) find_library(WEBPDEMUX_LIBRARY NAMES webpdemux HINTS /opt/homebrew/lib /usr/lib /usr/local/lib diff --git a/RetroFE/Source/Control/UserInput.h b/RetroFE/Source/Control/UserInput.h index ec9f469c8..8df14b927 100644 --- a/RetroFE/Source/Control/UserInput.h +++ b/RetroFE/Source/Control/UserInput.h @@ -63,6 +63,7 @@ class UserInput KeyCodeAdminMode, KeyCodeHideItem, KeyCodeQuit, + KeyCodeAmbient, KeyCodeReboot, KeyCodeSaveFirstPlaylist, KeyCodeSkipForward, diff --git a/RetroFE/Source/Database/GlobalOpts.cpp b/RetroFE/Source/Database/GlobalOpts.cpp index 23ddbfc2d..aa29a5736 100644 --- a/RetroFE/Source/Database/GlobalOpts.cpp +++ b/RetroFE/Source/Database/GlobalOpts.cpp @@ -118,11 +118,13 @@ const global_options::options_entry global_options::s_option_entries[] = { OPTION_ATTRACTMODELAUNCHRUNTIME, "30", global_options::option_type::INTEGER, "If attractModeLaunch = true, the length of time a launched item will run" }, { OPTION_ATTRACTMODELAUNCHMINMAXSCROLLS, "3,5", global_options::option_type::MSTRING, "If attractModeLaunch = true, comma separated min and max number of scrolls before launch" }, + { OPTION_AMBIENTMODEMINUTESPERIMAGE, "2", global_options::option_type::INTEGER, "Number of minutes between changing images in ambient mode" }, + { nullptr, nullptr, global_options::option_type::HEADER, "INPUT OPTIONS" }, { OPTION_COLLECTIONINPUTCLEAR, "false", global_options::option_type::BOOLEAN, "Clear input queue on collection change" }, { OPTION_PLAYLISTINPUTCLEAR, "false", global_options::option_type::BOOLEAN, "Clear input queue on playlist change" }, { OPTION_JUMPINPUTCLEAR, "false", global_options::option_type::BOOLEAN, "Clear input queue while jumping through the menu" }, - { OPTION_CONTROLLERCOMBOEXIT, "true", global_options::option_type::BOOLEAN, "Close RetroFE with the controller combo set in controls.conf" }, + { OPTION_CONTROLLERCOMBOEXITACTION, "QUIT", global_options::option_type::STRING, "Specify what happens on the controller combo set in controls.conf. Valid valies: QUIT, AMBIENT, NONE" }, { OPTION_CONTROLLERCOMBOSETTINGS, "false", global_options::option_type::BOOLEAN, "Open settings playlist with the controller combo set in controls.conf" }, { OPTION_SETTINGSCOLLECTIONPLAYLIST,"Arcades:settings", global_options::option_type::STRING, "Used by settings toggle to go to the playlist in collection:playlist format, defaults to settings.txt in the current collection" }, { OPTION_SERVOSTIKENABLED, "false", global_options::option_type::BOOLEAN, "Enable ServoStik support" }, diff --git a/RetroFE/Source/Database/GlobalOpts.h b/RetroFE/Source/Database/GlobalOpts.h index a937786a7..230780c90 100644 --- a/RetroFE/Source/Database/GlobalOpts.h +++ b/RetroFE/Source/Database/GlobalOpts.h @@ -111,11 +111,14 @@ #define OPTION_ATTRACTMODELAUNCHRUNTIME "attractModeLaunchRunTime" #define OPTION_ATTRACTMODELAUNCHMINMAXSCROLLS "attractModeLaunchMinMaxScrolls" +// AMBIENT MODE OPTIONS +#define OPTION_AMBIENTMODEMINUTESPERIMAGE "ambientModeMinutesPerImage" + // INPUT OPTIONS #define OPTION_COLLECTIONINPUTCLEAR "collectionInputClear" #define OPTION_PLAYLISTINPUTCLEAR "playlistInputClear" #define OPTION_JUMPINPUTCLEAR "jumpInputClear" -#define OPTION_CONTROLLERCOMBOEXIT "controllerComboExit" +#define OPTION_CONTROLLERCOMBOEXITACTION "controllerComboExitAction" #define OPTION_CONTROLLERCOMBOSETTINGS "controllerComboSettings" #define OPTION_SETTINGSCOLLECTIONPLAYLIST "settingsCollectionPlaylist" #define OPTION_SERVOSTIKENABLED "servoStikEnabled" @@ -239,11 +242,13 @@ class global_options bool attractmodelaunch() { return bool_value(OPTION_ATTRACTMODELAUNCH); } int attractmodelaunchruntime() { return int_value(OPTION_ATTRACTMODELAUNCHRUNTIME); } int attractmodelaunchminmaxScrolls() { return int_value(OPTION_ATTRACTMODELAUNCHMINMAXSCROLLS); } + + int ambientmodechangeminutes() { return int_value(OPTION_AMBIENTMODEMINUTESPERIMAGE); } bool collectioninputclear() { return bool_value(OPTION_COLLECTIONINPUTCLEAR); } bool playlistinputclear() { return bool_value(OPTION_PLAYLISTINPUTCLEAR); } bool jumpinputclear() { return bool_value(OPTION_JUMPINPUTCLEAR); } - bool controllercomboexit() { return bool_value(OPTION_CONTROLLERCOMBOEXIT); } + const char* controllercomboexitaction() { return value(OPTION_CONTROLLERCOMBOEXITACTION); } bool controllercombosettings() { return bool_value(OPTION_CONTROLLERCOMBOSETTINGS); } const char* quicklistcollectionplaylist() { return value(OPTION_QUICKLISTCOLLECTIONPLAYLIST); } const char *settingscollectionplaylist() { return value(OPTION_SETTINGSCOLLECTIONPLAYLIST); } diff --git a/RetroFE/Source/Execute/AmbientMode.cpp b/RetroFE/Source/Execute/AmbientMode.cpp new file mode 100644 index 000000000..ae29f259b --- /dev/null +++ b/RetroFE/Source/Execute/AmbientMode.cpp @@ -0,0 +1,269 @@ +#include "AmbientMode.h" +/* +This is the main class for "Ambient Mode". + +## Why and What + +The intent of Ambient Mode is to allow your arcade cabinet to assume a low-key presence in a room. For example, in a living room setting, you might not +want the cabinet to be a focal point all the time. Ambient Mode allows the cabinet to recede to the background without powering it all the way off. + +When enabled: +* the "exit" controller combo button from the retrofe main menu will go to ambient mode instead of exiting retrofe. +* while in ambient mode, images from the "ambient" directory will be displayed on the main screen, and rotated periodically +* to exit ambient mode, the controller combo button OR the action button will return you to the main retrofe menu. + +## Configuration + +1) create a directory called "ambient" in RetroFE's root directory, and populate it with images. +If you have a 2nd monitor, you should have at least one image name ending with "_marquee.png" (or any other common image extenion) + +2) in settings.conf: + + controllerComboExitAction = AMBIENT + ambientModeMinutesPerImage = 2 # OPTIONAL - how often to change to a new images. if left unspecified, default is 2 minutes + +## Marquee display + +If you have two monitors, the second monitor is assumed to be a marquee display. When a new image is displayed on the main screen, +the corresponding marquee image will be displayed if it exists, by looking for "*_marquee.ext". For example, if "sunset.png" is being displayed on +the main screen, the system will look for "sunset_marquee.png" to display on the marquee. +If no corresponding marquee image is found, a random marquee image is displayed instead. +*/ + +void AmbientMode::activate() { + + // set member variables + imageFiles_.clear(); + marqueeImageFiles_.clear(); + ambientPath_ = Utils::combinePath(basePath_, "ambient"); + LOG_INFO("AmbientMode", "Activating Ambient mode with "+std::to_string(SDL::getScreenCount())+" screen(s). Path for images is: " + ambientPath_); + + // Ensure the directory exists + if (!std::filesystem::is_directory(ambientPath_)) { + LOG_ERROR("AmbientMode", "Ambient directory does not exist: " + ambientPath_); + return; + } + + // Get lists of image files and marquee image files into our member variables + populateImageFiles(); + + if (imageFiles_.empty()) { + LOG_ERROR("AmbientMode", "Ambient mode will not be launched, since there are no images for the main screen in " + ambientPath_); + return; + } else { + LOG_INFO("AmbientMode", "There are " + std::to_string(imageFiles_.size()) + " images and " + std::to_string(marqueeImageFiles_.size()) + " marquee images in the ambient directory."); + } + + // Shuffle the image files to randomize the order + auto rng = std::default_random_engine(std::random_device{}()); + std::shuffle(std::begin(imageFiles_), std::end(imageFiles_), rng); + + input_.resetStates(); + SDL_Event e; + + int fadeStartTime = 0; + bool isFading = false; + int fadeDuration = 2000; // 2 second fade durationn. Could be made configurable in the future. + float firstImageOpacity = 1.0f; // 1 = fully opaque; 0 = fully transparent + auto lastChangeTime = std::chrono::steady_clock::now(); + int imageIndex = 0; + + SDL_Renderer* rendererMain = SDL::getRenderer(0); // Get the renderer for the main screen + SDL_Renderer* rendererMarquee = SDL::getRenderer(1); // Get the renderer for the Marquee screen + + currentImage_ = loadTexture(rendererMain, imageFiles_[imageIndex].c_str()); + if (SDL::getScreenCount() > 1) { + currentImageMarquee_ = loadTexture(rendererMarquee, determineMarqueePath(imageIndex).c_str()); + } + + + // Main loop for ambient mode + while (true) { + auto currentTime = std::chrono::steady_clock::now(); + auto elapsedTime = std::chrono::duration_cast(currentTime - lastChangeTime); + + if (!isFading) { + if (elapsedTime.count() >= minutesPerImage_ * 60 || + input_.keystate(UserInput::KeyCodeRight) || + input_.keystate(UserInput::KeyCodeLeft)) + { + imageIndex = (imageIndex + 1) % imageFiles_.size(); // Increment the image index, wrapping around if necessary + nextImage_ = loadTexture(rendererMain, imageFiles_[imageIndex].c_str()); + if (SDL::getScreenCount() > 1) { + nextImageMarquee_ = loadTexture(rendererMarquee, determineMarqueePath(imageIndex).c_str()); + } + isFading = true; + fadeStartTime = SDL_GetTicks(); // Record the start time of the fade + LOG_INFO("AmbientMode", "start fading to new image: " + imageFiles_[imageIndex]); + } + } + + // Handle fading + if (isFading) { + int currentFadeTime = SDL_GetTicks() - fadeStartTime; + firstImageOpacity = 1.0f - static_cast(currentFadeTime) / fadeDuration; + firstImageOpacity < 0.0f ? firstImageOpacity = 0.0f : NULL; // Clamp opacity lower value to 0.0f + + // check if we're done fading + if (firstImageOpacity == 0.0f) { + lastChangeTime = currentTime; // Reset the timer + isFading = false; // Reset the fade state + + SDL_DestroyTexture(currentImage_); // Destroy the old texture + currentImage_ = nextImage_; // Set the currentImage pointer so it will will now render + nextImage_ = nullptr; // Reset the next image texture + + SDL_DestroyTexture(currentImageMarquee_); // Destroy the old texture + currentImageMarquee_ = nextImageMarquee_; // Set the currentImage so it will renter the next iteration + nextImageMarquee_ = nullptr; // Reset the next image texture + + firstImageOpacity = 1.0f; + LOG_INFO("AmbientMode", "done fading "); + } + } + + // Display the current image (blended with the 2nd if needed) on the main screen + displayImages(currentImage_, nextImage_, firstImageOpacity, 0); + // Display the current image (blended with the 2nd if needed) on the marquee screen + if (SDL::getScreenCount() > 1) { + displayImages(currentImageMarquee_, nextImageMarquee_, firstImageOpacity, 1); + } + + // Check events to see if it's time to exit ambient mode + SDL_PollEvent(&e); + input_.update(e); + + if (input_.keystate(UserInput::KeyCodeSelect) || + (input_.keystate(UserInput::KeyCodeQuitCombo1) && input_.keystate(UserInput::KeyCodeQuitCombo2))) { + + input_.resetStates(); + break; // by breaking, we will exit the ambient mode loop, exit this function, and cease to block the main thread, thereby returning the user to retrofe. + } + + // little delay to avoid busy waiting + SDL_Delay(16); // SDL_Delay takes milliseconds, so this results in ~60 FPS. There's not enough going on in this loop to conditionally reduce the delay. + } + +} + +/* +The thing to know about this method is that nextImage_ CAN be a nullptr. That's the case when images NOT in the process of fading in/out. +If both images ARE provided, this method will render some blend between them, based on the firstImageOpacity value. + +@firstImageOpacity: 0.0f = fully transparent, 1.0f = fully opaque +*/ +void AmbientMode::displayImages(SDL_Texture* currentImage, SDL_Texture* nextImage, float firstImageOpacity, int screenNum) { + // safety -- if the screen number is out of bounds, this call is a no-op + if (screenNum + 1 > SDL::getScreenCount() ) { + return; + } + + SDL_Renderer* renderer = SDL::getRenderer(screenNum); + SDL_RenderClear(renderer); // Clear the screen + + // Set the alpha value for the first image + SDL_SetTextureAlphaMod(currentImage, static_cast(firstImageOpacity * 255)); // Set the alpha value for the first image + + // Render the first image + SDL_RenderCopy(renderer, currentImage, nullptr, nullptr); + + if (nextImage != nullptr) { + float alphaOfSecondImage = 1.0f - firstImageOpacity; // Inverse of the first image's alpha + SDL_SetTextureAlphaMod(nextImage, static_cast(alphaOfSecondImage * 255)); // Set the alpha value for the second image + SDL_RenderCopy(renderer, nextImage, nullptr, nullptr); + } + + SDL_RenderPresent(renderer); // Present the rendered frame +} +/* +The point of this mehtod is to decide which marquee image to display, given a specific image for the main screen. + + @imageIndex: refers to the index of an image in the imageFiles_ vector (the main screen). + @returns: the full path to some image, OR POSIBLY an empty string if we aren't doing marquees. +*/ +std::string AmbientMode::determineMarqueePath(int imageIndex) { + // for the main screen, just display the image by index + std::string imageName = imageFiles_[imageIndex]; + std::string marqueeImageName; + std::string marqueeImagePath = ""; + + // for the marquee screen, determine the corresponding marquee image if available (by naming convention), + // otherwise return a random marquee image. + if (!marqueeImageFiles_.empty()) { + std::filesystem::path path(imageName); + std::string baseName = path.stem().string(); // Get the filename without extension + std::string extension = path.extension().string(); // Get the file extension + marqueeImageName = baseName +"_marquee" + extension; + marqueeImagePath = Utils::combinePath(ambientPath_, marqueeImageName); + + if (!std::filesystem::exists(marqueeImagePath)) { + marqueeImageName = marqueeImageFiles_[std::rand() % marqueeImageFiles_.size()]; + marqueeImagePath = Utils::combinePath(ambientPath_, marqueeImageName); + LOG_INFO("AmbientMode", "There is no matching ambient image for "+marqueeImagePath+". Displaying random marquee image: "+ marqueeImagePath); + } + } + return marqueeImagePath; // Return the path to the marquee image +} + + +/* +This method modifies the members imageFiles_ and marqueeImageFiles_. It's intented to be called early, either in instantation or first use. +The vectors, once populated, will contain strings with full paths to image files. +*/ +void AmbientMode::populateImageFiles() +{ + namespace fs = std::filesystem; + + // Supported image extensions + const std::vector imageExtensions = { ".png", ".jpg", ".jpeg" }; + + // Iterate through the directory + for (const auto& entry : fs::directory_iterator(ambientPath_)) { + if (entry.is_regular_file()) { + std::string extension = entry.path().extension().string(); + + // Check if the file has a supported image extension + if (std::find(imageExtensions.begin(), imageExtensions.end(), extension) != imageExtensions.end()) { + std::string filenameWithoutExtension = entry.path().stem().string(); // Get the filename without extension + std::string filenameWithExtension = entry.path().filename().string(); // Get the filename with extension + + // Check if the filename (without extension) ends with "marquee" + if (filenameWithoutExtension.size() >= 8 && filenameWithoutExtension.substr(filenameWithoutExtension.size() - 8) == "_marquee") { + marqueeImageFiles_.push_back(entry.path().string()); + } else { + imageFiles_.push_back(entry.path().string()); // Store the full path of the image file + } + } + } + } +} + +/* This is needed for image formats that do not support alpha channels (like JPEG). +Convert everything we load to a format that does support alpha channels. +*/ +SDL_Texture* AmbientMode::loadTexture(SDL_Renderer* renderer, const std::string& imagePath) { + // Load the image as a surface + SDL_Surface* loadedSurface = IMG_Load(imagePath.c_str()); + if (!loadedSurface) { + LOG_ERROR("AmbientMode", "Failed to load image: " + imagePath + " - " + IMG_GetError()); + return nullptr; + } + + // Convert the surface to a format with an alpha channel (RGBA8888) + SDL_Surface* surfaceWithAlpha = SDL_ConvertSurfaceFormat(loadedSurface, SDL_PIXELFORMAT_RGBA8888, 0); + SDL_FreeSurface(loadedSurface); // Free the original surface + if (!surfaceWithAlpha) { + LOG_ERROR("AmbientMode", "Failed to convert surface to RGBA8888: " + std::string(SDL_GetError())); + return nullptr; + } + + // Create a texture from the surface + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surfaceWithAlpha); + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); + SDL_FreeSurface(surfaceWithAlpha); // Free the converted surface + if (!texture) { + LOG_ERROR("AmbientMode", "Failed to create texture: " + std::string(SDL_GetError())); + } + + return texture; +} \ No newline at end of file diff --git a/RetroFE/Source/Execute/AmbientMode.h b/RetroFE/Source/Execute/AmbientMode.h new file mode 100644 index 000000000..b4e720110 --- /dev/null +++ b/RetroFE/Source/Execute/AmbientMode.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "../SDL.h" +#if __has_include() + #include +#elif __has_include() + #include +#else + #error "Cannot find SDL_image header" +#endif +#include "../Utility/Log.h" +#include "../Utility/Utils.h" +#include "../Control/UserInput.h" + +class AmbientMode { +public: + AmbientMode(UserInput& input, const std::string& basePath, int minutesPerImage) + : input_(input), basePath_(basePath), minutesPerImage_(minutesPerImage) {} + + void activate(); + + +private: + void populateImageFiles(); + std::string determineMarqueePath(int imageIndex); + void displayImages(SDL_Texture *currentImage, SDL_Texture *nextImage, float alphaOfFirstImage, int screenNum); + SDL_Texture* loadTexture(SDL_Renderer* renderer, const std::string& imagePath); + UserInput& input_; + std::string basePath_; + std::string ambientPath_; + std::vector imageFiles_; + std::vector marqueeImageFiles_; + int minutesPerImage_; + SDL_Texture* currentImage_ = nullptr; + SDL_Texture* nextImage_ = nullptr; + SDL_Texture* currentImageMarquee_ = nullptr; + SDL_Texture* nextImageMarquee_ = nullptr; +}; diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index 2ebf66cd1..5b27777f5 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -24,6 +24,7 @@ #include "Database/GlobalOpts.h" #include "Database/HiScores.h" #include "Execute/Launcher.h" +#include "Execute/AmbientMode.h" #include "Graphics/Component/ScrollingList.h" #include "Graphics/Page.h" #include "Graphics/PageBuilder.h" @@ -2257,7 +2258,45 @@ bool RetroFE::run() running = false; } break; - } + case RETROFE_AMBIENT_REQUEST: + // First stage of entering ambient mode: stop the current page, which fades it out visually. + currentPage_->stop(); + state = RETROFE_AMBIENT; + break; + case RETROFE_AMBIENT: + // second stage of entering ambient mode: once the fade-out is complete, actually enter ambient mode. + if (currentPage_->isGraphicsIdle()) + { +#ifdef WIN32 + // stop music + Utils::postMessage("MediaplayerHiddenWindow", 0x8001, 75, 0); +#endif + currentPage_->setIsLaunched(true); + l.LEDBlinky(5); // 5= "Screensaver Start Event" per https://ledblinky.net/downloads/readme.txt + // initialize the instance of AmbientMode + int ambientModeMinutesPerImage = 2; + config_.getProperty(OPTION_AMBIENTMODEMINUTESPERIMAGE, ambientModeMinutesPerImage); + AmbientMode ambientMode(input_, Configuration::absolutePath, ambientModeMinutesPerImage); + ambientMode.activate(); // blocks until user exits + currentPage_->start(); // ... and we're back! Restart the page, and continue normally + state = RETROFE_IDLE; + // START: honestly, I'm not sure how much of this is needed, but adding it solved problems with a blank screen on exiting from ambient mode. + currentPage_->setIsLaunched(false); + currentPage_->updateReloadables(0); + currentPage_->onNewItemSelected(); + currentPage_->reallocateMenuSpritePoints(false); + currentTime_ = static_cast(SDL_GetTicks()) / 1000; + keyLastTime_ = currentTime_; + lastLaunchReturnTime_ = currentTime_; + // END +#ifdef WIN32 + // restart music + Utils::postMessage("MediaplayerHiddenWindow", 0x8001, 76, 0); +#endif + l.LEDBlinky(6); // 6= "Screensaver Stop Event" per https://ledblinky.net/downloads/readme.txt + break; + } + } // Handle screen updates and attract mode if (running) @@ -2689,15 +2728,19 @@ RetroFE::RETROFE_STATE RetroFE::processUserInput(Page* page) else if (input_.keystate(UserInput::KeyCodeQuitCombo1) && input_.keystate(UserInput::KeyCodeQuitCombo2)) { attract_.reset(); - bool controllerComboExit = false; - config_.getProperty(OPTION_CONTROLLERCOMBOEXIT, controllerComboExit); - if (controllerComboExit) + std::string controllerComboExitAction = "EXIT"; + config_.getProperty(OPTION_CONTROLLERCOMBOEXITACTION, controllerComboExitAction); + if (controllerComboExitAction == "EXIT") { #ifdef WIN32 Utils::postMessage("MediaplayerHiddenWindow", 0x8001, 51, 0); #endif return RETROFE_QUIT_REQUEST; } + else if (controllerComboExitAction == "AMBIENT") + { + return RETROFE_AMBIENT_REQUEST; + } } // KeyCodeCycleCollection shared with KeyCodeQuitCombo1 and can missfire else if (!kioskLock_ && input_.lastKeyPressed(UserInput::KeyCodeCycleCollection)) @@ -3142,7 +3185,11 @@ RetroFE::RETROFE_STATE RetroFE::processUserInput(Page* page) } } } - + else if (input_.keystate(UserInput::KeyCodeAmbient)) + { + LOG_INFO("RetroFE", "Ambient mode initated via keypress"); + state = RETROFE_AMBIENT_REQUEST; + } // !kioskLock_ && else if (input_.keystate(UserInput::KeyCodeQuit)) { diff --git a/RetroFE/Source/RetroFE.h b/RetroFE/Source/RetroFE.h index 26929eed4..5ba84215c 100644 --- a/RetroFE/Source/RetroFE.h +++ b/RetroFE/Source/RetroFE.h @@ -146,6 +146,8 @@ class RetroFE RETROFE_NEW, RETROFE_QUIT_REQUEST, RETROFE_QUIT, + RETROFE_AMBIENT_REQUEST, + RETROFE_AMBIENT, RETROFE_SCROLL_PLAYLIST_FORWARD, RETROFE_SCROLL_PLAYLIST_BACK, }; diff --git a/RetroFE/xcode/retrofe.xcodeproj/project.pbxproj b/RetroFE/xcode/retrofe.xcodeproj/project.pbxproj index 7cddfdb1b..b3647187a 100644 --- a/RetroFE/xcode/retrofe.xcodeproj/project.pbxproj +++ b/RetroFE/xcode/retrofe.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ DF07B3E52B2A14CE00B732CF /* GStreamer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF07B3E42B2A14CE00B732CF /* GStreamer.framework */; }; DF07B3E62B2A14CE00B732CF /* GStreamer.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DF07B3E42B2A14CE00B732CF /* GStreamer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DF2B282E2B683F2E00A22011 /* GlobalOpts.cpp in Sources */ = {isa = PBXBuildFile; fileRef = DF2B282C2B683F2E00A22011 /* GlobalOpts.cpp */; }; + DF2FCF8C2DC90A080077B107 /* AmbientMode.cpp in Sources */ = {isa = PBXBuildFile; fileRef = DF2FCF8B2DC90A080077B107 /* AmbientMode.cpp */; }; DF87A17A2B152AAB00548E78 /* RetroFE.png in Resources */ = {isa = PBXBuildFile; fileRef = DF87A1792B152AA400548E78 /* RetroFE.png */; }; DFC83C8A2D57973600AA7522 /* webp.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DFC83C882D57973600AA7522 /* webp.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DFC83C8B2D57973600AA7522 /* libusb-1.0.0.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DFC83C892D57973600AA7522 /* libusb-1.0.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -186,6 +187,8 @@ DF07B3E42B2A14CE00B732CF /* GStreamer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GStreamer.framework; path = ../ThirdPartyMac/GStreamer.framework; sourceTree = SOURCE_ROOT; }; DF2B282C2B683F2E00A22011 /* GlobalOpts.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = GlobalOpts.cpp; path = Database/GlobalOpts.cpp; sourceTree = SOURCE_ROOT; }; DF2B282D2B683F2E00A22011 /* GlobalOpts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = GlobalOpts.h; path = Database/GlobalOpts.h; sourceTree = SOURCE_ROOT; }; + DF2FCF8B2DC90A080077B107 /* AmbientMode.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AmbientMode.cpp; path = Execute/AmbientMode.cpp; sourceTree = ""; }; + DF2FCF8D2DC90A120077B107 /* AmbientMode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AmbientMode.h; path = Execute/AmbientMode.h; sourceTree = ""; }; DF5A44672DABDD0B005892D0 /* versioning.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = versioning.h; path = autogen/versioning.h; sourceTree = ""; }; DF87A1792B152AA400548E78 /* RetroFE.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = RetroFE.png; path = ../../Package/Environment/Common/RetroFE.png; sourceTree = ""; }; DFC83C862D5796C400AA7522 /* CollectionInfoBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CollectionInfoBuilder.cpp; path = Collection/CollectionInfoBuilder.cpp; sourceTree = SOURCE_ROOT; }; @@ -268,6 +271,7 @@ 4C418535FD0A487485FDF907 /* Header Files */ = { isa = PBXGroup; children = ( + DF2FCF8D2DC90A120077B107 /* AmbientMode.h */, 7FD09C6A782A4B69B37697F1 /* Animation.h */, 90AC9A42897248FB8AB8E13D /* AnimationEvents.h */, D32E8F4A73994068B6107132 /* AttractMode.h */, @@ -368,7 +372,7 @@ F1EBF96675C5443E881F32F0 /* Source Files */ = { isa = PBXGroup; children = ( - DFC83C9C2D579D7F00AA7522 /* VideoPool.cpp */, + DF2FCF8B2DC90A080077B107 /* AmbientMode.cpp */, 265E7B256A954D8793198331 /* Animation.cpp */, F7BFDD750A8D4FD2BD04E3E8 /* AnimationEvents.cpp */, C10786B3822040D9BB915594 /* AttractMode.cpp */, @@ -419,6 +423,7 @@ 5C2402E96D2D400793F40054 /* VideoBuilder.cpp */, F83482D74B334D73A7832224 /* VideoComponent.cpp */, A1A2A509107048D78E451632 /* VideoFactory.cpp */, + DFC83C9C2D579D7F00AA7522 /* VideoPool.cpp */, 4AE8BEDF614B479688422071 /* ViewInfo.cpp */, F916B85C7BF943D3AD1C1864 /* CollectionInfoBuilder.cpp */, ); @@ -545,6 +550,7 @@ DFC83C9D2D579D7F00AA7522 /* VideoPool.cpp in Sources */, 9E952B425AE5418FBE7740DF /* VideoComponent.cpp in Sources */, 8500F0A27BE443418BC27E80 /* ComponentItemBinding.cpp in Sources */, + DF2FCF8C2DC90A080077B107 /* AmbientMode.cpp in Sources */, 8DB049BC5D5241F48A59212D /* ComponentItemBindingBuilder.cpp in Sources */, 66790A464AE144839DE62066 /* Font.cpp in Sources */, FEC7E2567C954EC295AC1C94 /* FontCache.cpp in Sources */, diff --git a/docs/ADDITIONAL_FEATURES.md b/docs/ADDITIONAL_FEATURES.md index 133842885..03dd5a279 100644 --- a/docs/ADDITIONAL_FEATURES.md +++ b/docs/ADDITIONAL_FEATURES.md @@ -1,4 +1,54 @@ # Additional Features [Back](README.md) -Overtime RetroFE's need for extra features has grown due to community demand and due to the nature of open source, every line can be changed. \ No newline at end of file +Overtime RetroFE's need for extra features has grown due to community demand and due to the nature of open source, every line can be changed. + +## Ambient Mode +### Why and What + +Ambient Mode allows your arcade cabinet to assume a low-key presence in a room. For example, in a living room setting, you might not want the cabinet to be a focal point all the time. Ambient Mode allows the cabinet to recede to the background without powering it all the way off. + +Essentially, Ambient Mode acts as a screensaver, but instead of the typical "screensaver" functionality (which is already used elsewhere in the project and pulls its artwork from the `/collections` folder), it provides a visually engaging backdrop without disrupting the system. + +When enabled: + +The `quitCombo` controller combo button will go to ambient mode instead of exiting RetroFE. +While in ambient mode, images from the `/ambient`directory will be displayed on the main screen, and rotated periodically +To exit ambient mode, the controller combo button OR the action button will return you to the main RetroFE menu. + +### Configuration +To set up Ambient Mode + +- Create a Directory + - Create a directory named `/ambient` in the RetroFE root directory. + - Populate the `/ambient` directory with images you want to display. + + +- Configure Settings + +In the settings.conf file, add the following configuration options + +``` +controllerComboExitAction = AMBIENT +ambientModeMinutesPerImage = 2 +``` + +`controllerComboExitAction` controls what happens when you press select+start in RetroFE: + - QUIT: quits RetroFE + - AMBIENT: puts RetroFE in Ambient mode + - NONE: nothing happens. Use this if you want to prevent users from quitting RetroFE via controller (you can still quit with the keyboard) + +`ambientModeMinutesPerImage`: Optional. This option defines how often the image should change (in minutes). If left unspecified, the default is 20 minutes. + +### LEDBlinky Integration +When activated, Ambient Mode triggers LEDBLinky's "Screensaver" mode - so you want your LEDs/buttons to do something particular (including just go dark), +configure LEDBlinky's "Screensaver" mode. + +### Marquee Support +If you have a dual-monitor setup, Ambient Mode supports a marquee display on the second monitor: + +- How It Works + - When an image is displayed on the main screen, the system will check for a corresponding marquee image on the second monitor. + - It looks for a file named `imageName_marquee.ext` (e.g., `sunset_marquee.png`) where `imageName` matches the image displayed on the main screen. +- Fallback + - If no corresponding marquee image is found, a random marquee image from the `/ambient` directory will be displayed instead. \ No newline at end of file diff --git a/docs/GLOBAL_SETTINGS.md b/docs/GLOBAL_SETTINGS.md index 8af61b5b0..50e829509 100644 --- a/docs/GLOBAL_SETTINGS.md +++ b/docs/GLOBAL_SETTINGS.md @@ -122,7 +122,7 @@ For all `BOOLEAN` types, the following values are accepted: `yes`, `no`, `true`, | `collectionInputClear` | `false` | `BOOLEAN` | Clear input queue on collection change | | | `playlistInputClear` | `false` | `BOOLEAN` | Clear input queue on playlist change | | | `jumpInputClear` | `false` | `BOOLEAN` | Clear input queue while jumping through the menu | | -| `controllerComboExit` | `true` | `BOOLEAN` | Close RetroFE with the controller combo set in controls.conf | ✅ | +| `controllerComboExitAction` | `QUIT` | `STRING` | Specify what happens on the controller combo set in controls.conf. Valid valies: QUIT, AMBIENT, NONE | ✅ | | `controllerComboSettings` | `false` | `BOOLEAN` | Open settings playlist with the controller combo set in controls.conf | ✅ | | `settingsCollectionPlaylist` | `Arcades:settings` | `STRING` | Used by settings toggle to go to the playlist in collection:playlist format, defaults to settings.txt in the current collection | ✅ | | `servoStikEnabled` | `false` | `BOOLEAN` | Enable ServoStik support | |