diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4b4b057 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,193 @@ +name: Build +on: + push: + paths-ignore: + [ + "**/**.md" + ] + + pull_request: + branches: + - master + paths-ignore: + [ + "**/**.md" + ] + + workflow_dispatch: + +defaults: + run: + shell: bash +env: + VCPKG_COMMITTISH: 10b7a178346f3f0abef60cecd5130e295afd8da4 + +permissions: + actions: none + checks: none + contents: write + deployments: none + issues: none + packages: read + pull-requests: none + repository-projects: none + security-events: none + statuses: read + +jobs: + build_windows: + name: Windows + runs-on: windows-2022 + strategy: + fail-fast: false + + env: + CMAKE_BUILD_TYPE: Release + CMAKE_GENERATOR: Visual Studio 17 2022 + VCPKG_TRIPLET: x64-windows-static + + steps: + - name: Checkout Git repository + uses: actions/checkout@v3 + with: + submodules: true + + - uses: friendlyanon/setup-vcpkg@v1 + with: + committish: ${{env.VCPKG_COMMITTISH}} + + - name: Configure + run: cmake -B build -G "${{env.CMAKE_GENERATOR}}" -D "CMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=${{env.VCPKG_TRIPLET}} + + - name: Build + run: cmake --build build --target package --config ${{ env.CMAKE_BUILD_TYPE }} + + - uses: actions/upload-artifact@v3 + name: Upload package + with: + name: Windows build + path: build/*.zip + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: build/*.zip + + build_linux: + name: Linux + runs-on: ubuntu-latest + permissions: + packages: write + strategy: + fail-fast: false + matrix: + config: + - name: Debian + docker_image: debian:bullseye + package_ext: .deb + + container: + image: ${{matrix.config.docker_image}} + + env: + CMAKE_BUILD_TYPE: Release + + steps: + - name: Checkout Git repository + uses: actions/checkout@v3 + + - name: "Install dependencies" + run: | + if [[ "${{matrix.config.name}}" == "Debian" ]]; then + apt update && apt install -y build-essential git cmake pkg-config libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libinih-dev + fi + + - name: Build + run: | + if [[ "${{matrix.config.name}}" == "Debian" ]]; then + cmake -B build -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DPACKAGE=DEB -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=amd64 + cmake --build build --target package --config ${{env.CMAKE_BUILD_TYPE}} + fi + + - uses: actions/upload-artifact@v3 + name: Upload Package + with: + name: Debian build + path: build/*.deb + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: build/*${{matrix.config.package_ext}} + token: ${{secrets.ACTIONS_SECRET}} + + build_rpi: + name: Raspberry Pi + runs-on: ubuntu-latest + strategy: + fail-fast: false + + env: + CMAKE_BUILD_TYPE: Release + IMAGE_VERSION: "2022-10-30" + + steps: + - name: Setup + run: | + REPO=${{github.repository}} + echo "REPO_TITLE=${REPO#*/}" >> ${GITHUB_ENV} + sudo apt-get install -y qemu qemu-user-static binfmt-support wget tar + mkdir root + + - name: Cache Image + id: image-cache + uses: actions/cache@v3 + with: + path: image.tar.xz + key: ${{env.IMAGE_VERSION}} + + - name: Download Image + if: steps.image-cache.outputs.cache-hit != 'true' + run: wget -q https://github.com/complexlogic/rpi_image/releases/download/${{env.IMAGE_VERSION}}/image.tar.xz + + - name: Extract Image + run: tar -xf image.tar.xz + + - name: Checkout Git repository + uses: actions/checkout@v3 + with: + path: ${{env.REPO_TITLE}} + + - name: Generate Build Script + run: | + echo '#!/bin/bash' >> rpi + echo "sudo apt update &&" >> rpi + echo "sudo apt install -y libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libinih-dev &&" >> rpi + echo "cd ${{env.REPO_TITLE}} &&" >> rpi + echo "cmake -B build -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DPACKAGE=DEB -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=arm64 -DRPI=1 &&" >> rpi + echo "cmake --build build --target package --config ${{env.CMAKE_BUILD_TYPE}}" >> rpi + sudo chmod +x rpi + + - name: Build + run: | + LOOPBACK=$(sudo losetup -f -P --show rpi.img) + sudo mount ${LOOPBACK}p2 -o rw root + sudo cp /usr/bin/qemu-aarch64-static root/usr/bin + sudo cp -r ${{env.REPO_TITLE}} root + sudo cp rpi root/usr/bin + sudo chroot root rpi + + - uses: actions/upload-artifact@v3 + name: Upload package + with: + name: Raspberry Pi build + path: root/${{env.REPO_TITLE}}/build/*.deb + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: root/${{env.REPO_TITLE}}/build/*.deb + token: ${{secrets.ACTIONS_SECRET}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c94817c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +docs/Gemfile.lock +docs/_site +docs/.jekyll-cache diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..a56d599 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,84 @@ +v2.2 (2024-11-24) +- Make application timeout configurable +- Add WrapEntries feature +- Various fixes, optimizations, and code quality improvements +- Windows: Improve logic relating to handling of launched applications (focus issues, etc.) +- Linux: Don't set SDL_VIDEODRIVER environment variable + +v2.1 (2023-1-7) +- Added OnLaunch 'Quit' mode + +v2.0 (2022-12-19) +- Change application tracking to window focus-based instead of process based +- Remove OnLaunch "Hide" mode +- Reorganize configuration file into smaller sections +- Make VSync optional +- Add support for transparent backgrounds +- Allow control from multiple gamepads +- Allow unlimited number of slideshow backgrounds +- Make highlight optional +- Make application titles optional +- Specify color and key HEX codes with string beginning with # character +- Rename ButtonCenterline setting to VCenter +- Create dialog box on fatal errors +- Linux: Prefer native Wayland over XWayland + +v1.8 (2022-10-7) +- Added inhibit OS screensaver feature +- Minor bugfixes +- Windows: Added "chrome_proxy.exe" to the list of supported browser processes + +v1.7 (2022-5-30) +- Added selected icons feature +- Added StartupCmd and QuitCmd features +- New getopt-based command line parsing + +v1.6.1 (2022-4-19) +- Windows: Disable system DPI scaling + +v1.6 (2022-4-2) +- Added background overlay feature +- Added text shadows feature +- Added outline configuration option for highlight +- Added scroll indicator outline configuration option +- Fixed bug where an unmapped controller won't automatically connect upon startup + +v1.5 (2022-2-28) +- Added :fork special command +- Enabled VSync +- Added MouseSelect setting for gyroscopic mouse support +- Windows: Added exit hotkey feature +- Windows: The :sleep special command now properly sleeps the PC instead of hibernating it + +v1.4 (2022-2-5) +- Added clock widget to show the current time and date +- Windows: Full Unicode support implemented +- Linux: Changed .desktop file action delimiting character from pipe | to semicolon ; + +v1.3 (2022-1-13) +- Added hotkeys feature +- Removed EscQuit setting. Esc to quit functionality is now implemented as a configurable hotkey +- Changed slideshow time units from milliseconds to seconds +- Windows: Applications now launch via WIN32 API instead of the standard C runtime +- Windows: Implemented OnLaunch setting that was previously Linux exclusive +- Windows: Fixed web browser non-blocking issue +- Linux: Improved Wayland support + +v1.2 (2022-1-2) +- Added slideshow background mode +- Added screensaver feature +- Windows: Improved Unicode support + +v1.1 (2021-12-24) +- Added logging interface +- Linux: Added OnLaunch setting +- Changed all RGBA color settings to RGB for simplicity. Transparency is now set solely in the separate opacity setting +- Refactored automatic file searching logic + +v1.0.1 (2021-12-4) +- Added support for WebP images +- Improved handling of paths enclosed in quotes +- Windows: Added application icon to executable + +v1.0 (2021-11-28) +- Initial release diff --git a/CHANGELOG.txt b/CHANGELOG.txt deleted file mode 100644 index 6494018..0000000 --- a/CHANGELOG.txt +++ /dev/null @@ -1,32 +0,0 @@ -v1.4 (2022-2-5) -- Added clock widget to show the current time and date -- Windows: Full Unicode support implemented -- Linux: Changed .desktop file action delimiting character from pipe | to semicolon ; - -v1.3 (2022-1-13) -- Added hotkeys feature -- Removed EscQuit setting. Esc to quit functionality is now implemented as a configurable hotkey -- Changed slideshow time units from milliseconds to seconds -- Windows: Applications now launch via WIN32 API instead of the standard C runtime -- Windows: Implemented OnLaunch setting that was previously Linux exclusive -- Windows: Fixed web browser non-blocking issue -- Linux: Improved Wayland support - -v1.2 (2022-1-2) -- Added slideshow background mode -- Added screensaver feature -- Windows: Improved Unicode support - -v1.1 (2021-12-24) -- Added logging interface -- Linux: Added OnLaunch setting -- Changed all RGBA color settings to RGB for simplicity. Transparency is now set solely in the separate opacity setting -- Refactored automatic file searching logic - -v1.0.1 (2021-12-4) -- Added support for WebP images -- Improved handling of paths enclosed in quotes -- Windows: Added application icon to executable - -v1.0 (2021-11-28) -- Initial release diff --git a/CMakeLists.txt b/CMakeLists.txt index 369489b..ff6d9a8 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,210 +1,126 @@ # Project info cmake_minimum_required(VERSION 3.18) project("Flex Launcher" - LANGUAGES C - VERSION "1.4" - DESCRIPTION "Customizable HTPC Application Launcher" - HOMEPAGE_URL "https://github.com/complexlogic/flex-launcher") + LANGUAGES C + VERSION 2.2 + DESCRIPTION "Customizable HTPC Application Launcher" + HOMEPAGE_URL "https://github.com/complexlogic/flex-launcher" +) set(EXECUTABLE_TITLE "flex-launcher") +set(EXECUTABLE_OUTPUT_PATH "${PROJECT_BINARY_DIR}") +include_directories(${PROJECT_BINARY_DIR}) -#Generate PKGBUILD for Arch packages +option(EXTRA_WARNINGS "Enable extra compiler warnings" OFF) +if (EXTRA_WARNINGS) + if (MSVC) + add_compile_options(/W4 /WX) + else () + add_compile_options(-Wall -Wextra -Wpedantic -Wconversion) + endif () +endif () + +# Set Visual Studio solution startup project +if (WIN32) + set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${EXECUTABLE_TITLE}) +endif () + +# Minimum library versions for Linux +set(MIN_SDL_VERSION "2.0.14") +set(MIN_SDL_IMAGE_VERSION "2.0.5") +set(MIN_SDL_TTF_VERSION "2.0.15") +set(MIN_GLIBC_VERSION "2.31") # Enforced for .deb packages only + +# Generate PKGBUILD script for Arch packages if (PACKAGE STREQUAL "ARCH") configure_file("${PROJECT_SOURCE_DIR}/config/PKGBUILD.in" "${PROJECT_BINARY_DIR}/PKGBUILD") return() endif () -# Config setting keys -set(SETTING_DEFAULT_MENU "DefaultMenu") -set(SETTING_MAX_BUTTONS "MaxButtons") -set(SETTING_BACKGROUND_MODE "BackgroundMode") -set(SETTING_BACKGROUND_COLOR "BackgroundColor") -set(SETTING_BACKGROUND_IMAGE "BackgroundImage") -set(SETTING_SLIDESHOW_DIRECTORY "SlideshowDirectory") -set(SETTING_SLIDESHOW_IMAGE_DURATION "SlideshowImageDuration") -set(SETTING_SLIDESHOW_TRANSITION_TIME "SlideshowTransitionTime") -set(SETTING_ICON_SIZE "IconSize") -set(SETTING_ICON_SPACING "IconSpacing") -set(SETTING_TITLE_FONT "TitleFont") -set(SETTING_TITLE_FONT_SIZE "TitleFontSize") -set(SETTING_TITLE_COLOR "TitleColor") -set(SETTING_TITLE_OPACITY "TitleOpacity") -set(SETTING_TITLE_OVERSIZE_MODE "TitleOversizeMode") -set(SETTING_TITLE_PADDING "TitlePadding") -set(SETTING_HIGHLIGHT_COLOR "HighlightColor") -set(SETTING_HIGHLIGHT_OPACITY "HighlightOpacity") -set(SETTING_HIGHLIGHT_CORNER_RADIUS "HighlightCornerRadius") -set(SETTING_HIGHLIGHT_VPADDING "HighlightVPadding") -set(SETTING_HIGHLIGHT_HPADDING "HighlightHPadding") -set(SETTING_BUTTON_CENTERLINE "ButtonCenterline") -set(SETTING_SCROLL_INDICATORS "ScrollIndicators") -set(SETTING_SCROLL_INDICATOR_COLOR "ScrollIndicatorColor") -set(SETTING_SCROLL_INDICATOR_OPACITY "ScrollIndicatorOpacity") -set(SETTING_ON_LAUNCH "OnLaunch") -set(SETTING_RESET_ON_BACK "ResetOnBack") -set(SETTING_CLOCK_ENABLED "Enabled") -set(SETTING_CLOCK_SHOW_DATE "ShowDate") -set(SETTING_CLOCK_ALIGNMENT "Alignment") -set(SETTING_CLOCK_FONT "Font") -set(SETTING_CLOCK_COLOR "Color") -set(SETTING_CLOCK_OPACITY "Opacity") -set(SETTING_CLOCK_FONT_SIZE "FontSize") -set(SETTING_CLOCK_MARGIN "Margin") -set(SETTING_CLOCK_TIME_FORMAT "TimeFormat") -set(SETTING_CLOCK_DATE_FORMAT "DateFormat") -set(SETTING_CLOCK_INCLUDE_WEEKDAY "IncludeWeekday") -set(SETTING_SCREENSAVER_ENABLED "Enabled") -set(SETTING_SCREENSAVER_IDLE_TIME "IdleTime") -set(SETTING_SCREENSAVER_INTENSITY "Intensity") -set(SETTING_SCREENSAVER_PAUSE_SLIDESHOW "PauseSlideshow") -set(SETTING_GAMEPAD_ENABLED "Enabled") -set(SETTING_GAMEPAD_DEVICE "DeviceIndex") -set(SETTING_GAMEPAD_MAPPINGS_FILE "ControllerMappingsFile") -set(SETTING_GAMEPAD_LSTICK_XM "LStickX-") -set(SETTING_GAMEPAD_LSTICK_XP "LStickX+") -set(SETTING_GAMEPAD_LSTICK_YM "LStickY-") -set(SETTING_GAMEPAD_LSTICK_YP "LStickY+") -set(SETTING_GAMEPAD_RSTICK_XM "RStickX-") -set(SETTING_GAMEPAD_RSTICK_XP "RStickX+") -set(SETTING_GAMEPAD_RSTICK_YM "RStickY-") -set(SETTING_GAMEPAD_RSTICK_YP "RStickY+") -set(SETTING_GAMEPAD_LTRIGGER "LTrigger") -set(SETTING_GAMEPAD_RTRIGGER "RTrigger") -set(SETTING_GAMEPAD_BUTTON_A "ButtonA") -set(SETTING_GAMEPAD_BUTTON_B "ButtonB") -set(SETTING_GAMEPAD_BUTTON_X "ButtonX") -set(SETTING_GAMEPAD_BUTTON_Y "ButtonY") -set(SETTING_GAMEPAD_BUTTON_BACK "ButtonBack") -set(SETTING_GAMEPAD_BUTTON_GUIDE "ButtonGuide") -set(SETTING_GAMEPAD_BUTTON_START "ButtonStart") -set(SETTING_GAMEPAD_BUTTON_LEFT_STICK "ButtonLeftStick") -set(SETTING_GAMEPAD_BUTTON_RIGHT_STICK "ButtonRightStick") -set(SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER "ButtonLeftShoulder") -set(SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER "ButtonRightShoulder") -set(SETTING_GAMEPAD_BUTTON_DPAD_UP "ButtonDPadUp") -set(SETTING_GAMEPAD_BUTTON_DPAD_DOWN "ButtonDPadDown") -set(SETTING_GAMEPAD_BUTTON_DPAD_LEFT "ButtonDPadLeft") -set(SETTING_GAMEPAD_BUTTON_DPAD_RIGHT "ButtonDPadRight") +# Import settings +include(${PROJECT_SOURCE_DIR}/config/config_settings.cmake) -# Default settings -set(DEFAULT_MENU "Main") -set(DEFAULT_MAX_BUTTONS 4) -set(DEFAULT_BACKGROUND_MODE "Color") -set(DEFAULT_BACKGROUND_COLOR_R "00") -set(DEFAULT_BACKGROUND_COLOR_G "00") -set(DEFAULT_BACKGROUND_COLOR_B "00") -set(DEFAULT_SLIDESHOW_IMAGE_DURATION "30000") -set(DEFAULT_SLIDESHOW_IMAGE_DURATION_CONFIG "30") -set(DEFAULT_SLIDESHOW_TRANSITION_TIME "1500") -set(DEFAULT_SLIDESHOW_TRANSITION_TIME_CONFIG "1.5") -set(DEFAULT_ICON_SIZE 256) -set(DEFAULT_ICON_SPACING "5%") -set(DEFAULT_FONT "OpenSans-Regular.ttf") -set(DEFAULT_FONT_SIZE 36) -set(DEFAULT_TITLE_COLOR_R "FF") -set(DEFAULT_TITLE_COLOR_G "FF") -set(DEFAULT_TITLE_COLOR_B "FF") -set(DEFAULT_TITLE_COLOR_A "FF") -set(DEFAULT_TITLE_OPACITY "100%") -set(DEFAULT_TITLE_OVERSIZE_MODE "Shrink") -set(DEFAULT_TITLE_PADDING 20) -set(DEFAULT_HIGHLIGHT_COLOR_R "FF") -set(DEFAULT_HIGHLIGHT_COLOR_G "FF") -set(DEFAULT_HIGHLIGHT_COLOR_B "FF") -set(DEFAULT_HIGHLIGHT_COLOR_A "40") -set(DEFAULT_HIGHLIGHT_OPACITY "25%") -set(DEFAULT_HIGHLIGHT_CORNER_RADIUS 0) -set(DEFAULT_HIGHLIGHT_VPADDING 30) -set(DEFAULT_HIGHLIGHT_HPADDING 30) -set(DEFAULT_BUTTON_CENTERLINE "50%") -set(DEFAULT_SCROLL_INDICATORS "true") -set(DEFAULT_SCROLL_INDICATOR_COLOR_R "FF") -set(DEFAULT_SCROLL_INDICATOR_COLOR_G "FF") -set(DEFAULT_SCROLL_INDICATOR_COLOR_B "FF") -set(DEFAULT_SCROLL_INDICATOR_COLOR_A "FF") -set(DEFAULT_SCROLL_INDICATOR_OPACITY "100%") -set(DEFAULT_ON_LAUNCH "Hide") -set(DEFAULT_RESET_ON_BACK "false") -set(DEFAULT_CLOCK_ENABLED "false") -set(DEFAULT_CLOCK_SHOW_DATE "false") -set(DEFAULT_CLOCK_ALIGNMENT "Left") -set(DEFAULT_CLOCK_FONT "SourceSansPro-Regular.ttf") -set(DEFAULT_CLOCK_MARGIN "5%") -set(DEFAULT_CLOCK_COLOR_R "FF") -set(DEFAULT_CLOCK_COLOR_G "FF") -set(DEFAULT_CLOCK_COLOR_B "FF") -set(DEFAULT_CLOCK_COLOR_A "FF") -set(DEFAULT_CLOCK_OPACITY "100%") -set(DEFAULT_CLOCK_FONT_SIZE "50") -set(DEFAULT_CLOCK_TIME_FORMAT "Auto") -set(DEFAULT_CLOCK_DATE_FORMAT "Auto") -set(DEFAULT_CLOCK_INCLUDE_WEEKDAY "true") -set(DEFAULT_SCREENSAVER_ENABLED "false") -set(DEFAULT_SCREENSAVER_IDLE_TIME "300") -set(DEFAULT_SCREENSAVER_INTENSITY "70%") -set(DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW "true") -set(DEFAULT_GAMEPAD_ENABLED "false") -set(DEFAULT_GAMEPAD_DEVICE 0) +# Find dependencies - Linux +if (UNIX) + find_package(PkgConfig MODULE REQUIRED) + pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2>=${MIN_SDL_VERSION}) + pkg_check_modules(SDL2_IMAGE REQUIRED IMPORTED_TARGET SDL2_image>=${MIN_SDL_IMAGE_VERSION}) + pkg_check_modules(SDL2_TTF REQUIRED IMPORTED_TARGET SDL2_ttf>=${MIN_SDL_TTF_VERSION}) + pkg_check_modules(INIH REQUIRED IMPORTED_TARGET inih) +endif () -# Linux configuration +# Find dependencies - Windows +if (WIN32) + find_package(SDL2 CONFIG REQUIRED) + find_package(sdl2_image CONFIG REQUIRED) + find_package(SDL2_ttf CONFIG REQUIRED) + + find_path(INIH_INCLUDE_DIR "ini.h" REQUIRED) + find_library(INIH inih REQUIRED) + find_path(GETOPT_INCLUDE_DIR "getopt.h" REQUIRED) + find_library(GETOPT getopt REQUIRED) +endif () + +# Build configuration - Linux if (UNIX) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -s") set(CMAKE_SHARED_LINKER_FLAGS "--as-needed") - - #Find SDL - set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") - find_package(SDL2 MODULE REQUIRED) - find_package(SDL2_image MODULE REQUIRED) - find_package(SDL2_ttf MODULE REQUIRED) - - #Set Default application commands - set(DESKTOP_PATH "/usr/share/applications/") - set(CMD_KODI "${DESKTOP_PATH}kodi.desktop") - set(CMD_PLEX "${DESKTOP_PATH}plexmediaplayer.desktop;TVF") +endif () + +# Build configuration - Windows +if (WIN32) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE") # Workaround for Nanosvg and SDL_image header conflicts + endif () + + # Configure default applications - Linux + if (UNIX) + set(DESKTOP_PATH "/usr/share/applications") + set(CMD_KODI "${DESKTOP_PATH}/kodi.desktop") + set(CMD_PLEX "${DESKTOP_PATH}/plexmediaplayer.desktop;TVF") if (RPI) set(TITLE_GAMES "RetroArch") set(ICON_GAMES "retroarch.png") - set(CMD_GAMES "${DESKTOP_PATH}retroarch.desktop") + set(CMD_GAMES "${DESKTOP_PATH}/retroarch.desktop") else () set(TITLE_GAMES "Steam") set(ICON_GAMES "steam.png") - set(CMD_GAMES "${DESKTOP_PATH}steam.desktop;BigPicture") + set(CMD_GAMES "${DESKTOP_PATH}/steam.desktop;BigPicture") endif () endif () -# Windows configuration +# Configure default applications - Windows if (WIN32) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE") # Workaround for Nanosvg and SDL_image header conflicts - - #Force static linking for C Runtime - set(CompilerFlags - CMAKE_C_FLAGS - CMAKE_C_FLAGS_DEBUG - CMAKE_C_FLAGS_RELEASE - CMAKE_C_FLAGS_MINSIZEREL - CMAKE_C_FLAGS_RELWITHDEBINFO) - foreach(CompilerFlag ${CompilerFlags}) - string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}") - set(${CompilerFlag} "${${CompilerFlag}}" CACHE STRING "msvc compiler flags" FORCE) - endforeach() - - #Find SDL - find_package(SDL2 CONFIG REQUIRED) - find_package(sdl2-image CONFIG REQUIRED) - find_package(sdl2-ttf CONFIG REQUIRED) - - # Application commands set(CMD_KODI "\"C:\\Program Files\\Kodi\\kodi.exe\"") set(CMD_PLEX "\"C:\\Program Files\\Plex\\Plex Media Player\\PlexMediaPlayer.exe\"") set(TITLE_GAMES "Steam") set(ICON_GAMES "steam.png") set(CMD_GAMES "\"C:\\Program Files (x86)\\Steam\\steam.exe\" steam://open/bigpicture") - - if (NOT PACKAGE) - set(PACKAGE "ZIP") - endif () -endif () +endif () -# Time formats +# Generate Windows application manifest +if (WIN32) + set(VERSION_M ${PROJECT_VERSION_MAJOR}) + if (PROJECT_VERSION_MINOR) + set(VERSION_N ${PROJECT_VERSION_MINOR}) + else () + set(VERSION_N 0) + endif() + if (PROJECT_VERSION_PATCH) + set(VERSION_O ${PROJECT_VERSION_PATCH}) + else () + set(VERSION_O 0) + endif() + if (PROJECT_VERSION_TWEAK) + set(VERSION_P ${PROJECT_VERSION_TWEAK}) + else () + set(VERSION_P 0) + endif() + configure_file( + ${PROJECT_SOURCE_DIR}/config/${EXECUTABLE_TITLE}.manifest.in + ${PROJECT_BINARY_DIR}/${EXECUTABLE_TITLE}.manifest + ) +endif() + +# Set time format strings if (WIN32) set(REMOVE_ZERO "#") else() @@ -215,74 +131,84 @@ set(TIME_STRING_24HR "%H:%M") set(DATE_STRING_LITTLE "%d %b") set(DATE_STRING_BIG "%b %${REMOVE_ZERO}d") -#Build src +# Configure main header file +configure_file("${PROJECT_SOURCE_DIR}/config/launcher_config.h.in" "${PROJECT_BINARY_DIR}/launcher_config.h") + +#Build source files add_subdirectory("src") -# Install +# Installation - Linux if (UNIX) set(INSTALL_DIR_BIN "${CMAKE_INSTALL_PREFIX}/bin") set(INSTALL_DIR_SHARE "${CMAKE_INSTALL_PREFIX}/share/${EXECUTABLE_TITLE}") set(INSTALL_DIR_DESKTOP "${CMAKE_INSTALL_PREFIX}/share/applications") set(INSTALL_DIR_CONFIGFILE "${INSTALL_DIR_SHARE}") set(INSTALL_DIR_ASSETS "${INSTALL_DIR_SHARE}") -else () - set(INSTALL_DIR_BIN "./") - set(INSTALL_DIR_CONFIGFILE "./") - set(INSTALL_DIR_ASSETS "./") -endif () -install(TARGETS ${EXECUTABLE_TITLE} DESTINATION "${INSTALL_DIR_BIN}") -install(FILES "${PROJECT_BINARY_DIR}/install/config.ini" DESTINATION "${INSTALL_DIR_CONFIGFILE}") -install(DIRECTORY "${PROJECT_SOURCE_DIR}/assets" DESTINATION "${INSTALL_DIR_ASSETS}") - -#Configure icons and desktop file -if (UNIX) - configure_file("${PROJECT_SOURCE_DIR}/config/launcher.desktop.in" "${PROJECT_BINARY_DIR}/install/${EXECUTABLE_TITLE}.desktop") - install(FILES "${PROJECT_BINARY_DIR}/install/${EXECUTABLE_TITLE}.desktop" DESTINATION "${INSTALL_DIR_DESKTOP}") - set(INSTALL_DIR_ICONS "${CMAKE_INSTALL_PREFIX}/share/icons") - install(FILES "${PROJECT_SOURCE_DIR}/extra/flex-launcher.png" DESTINATION "${INSTALL_DIR_ICONS}/hicolor/48x48/apps") - install(FILES "${PROJECT_SOURCE_DIR}/extra/flex-launcher.svg" DESTINATION "${INSTALL_DIR_ICONS}/hicolor/scalable/apps") + + # Configure Debian packages + if (PACKAGE STREQUAL "DEB") + set(CPACK_DEBIAN_PACKAGE_NAME ${EXECUTABLE_TITLE}) + set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT") + set(CPACK_DEBIAN_PACKAGE_VERSION ${CMAKE_PROJECT_VERSION}) + if (NOT CPACK_DEBIAN_PACKAGE_ARCHITECTURE) + set (CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") + endif () + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libsdl2-2.0-0 (>= ${MIN_SDL_VERSION}), libsdl2-image-2.0-0 (>= ${MIN_SDL_IMAGE_VERSION}), libsdl2-ttf-2.0-0 (>= ${MIN_SDL_TTF_VERSION}), libc6 (>= ${MIN_GLIBC_VERSION}), libinih1") + set(CPACK_DEBIAN_PACKAGE_MAINTAINER "complexlogic") + set(CPACK_DEBIAN_PACKAGE_SECTION "video") + set(CPACK_DEBIAN_ARCHIVE_TYPE "gnutar") + set(CPACK_DEBIAN_COMPRESSION_TYPE "gzip") + set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") + endif () endif () +# Installation - Windows if (WIN32) - install(FILES "${PROJECT_SOURCE_DIR}/CHANGELOG.txt" DESTINATION "${INSTALL_DIR_BIN}") -endif () + set(INSTALL_DIR_BIN "./") + set(INSTALL_DIR_CONFIGFILE "./") + set(INSTALL_DIR_ASSETS "./") -# Configure CPack for packages -if (PACKAGE STREQUAL "DEB") - set(CPACK_DEBIAN_PACKAGE_NAME ${EXECUTABLE_TITLE}) - set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT") - set(CPACK_DEBIAN_PACKAGE_VERSION ${CMAKE_PROJECT_VERSION}) - if (NOT DEB_ARCH) - if (RPI) - set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "armhf") - else () - set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") + configure_file(${PROJECT_SOURCE_DIR}/CHANGELOG ${PROJECT_BINARY_DIR}/CHANGELOG.txt) + install(FILES ${PROJECT_BINARY_DIR}/CHANGELOG.txt DESTINATION "${INSTALL_DIR_BIN}") + + # Copy the VC Runtime DLL in case user doesn't have it installed + set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) + include(InstallRequiredSystemLibraries) + foreach(lib ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}) + string(FIND ${lib} "vcruntime140.dll" vcruntime) + if (NOT vcruntime EQUAL -1) + install(FILES ${lib} DESTINATION "./") endif () - else () - set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "${DEB_ARCH}") + endforeach() + + # Set package + if (NOT PACKAGE) + set(PACKAGE "ZIP") endif () - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libsdl2-2.0-0 (>= 2.0.14), libsdl2-image-2.0-0 (>= 2.0.5), libsdl2-ttf-2.0-0 (>= 2.0.15)") - set(CPACK_DEBIAN_PACKAGE_MAINTAINER "complexlogic") - set(CPACK_DEBIAN_PACKAGE_SECTION "video") - set(CPACK_DEBIAN_ARCHIVE_TYPE "gnutar") - set(CPACK_DEBIAN_COMPRESSION_TYPE "gzip") - set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") endif () +# Installation - Common +install(TARGETS ${EXECUTABLE_TITLE} DESTINATION "${INSTALL_DIR_BIN}") +install(FILES "${PROJECT_BINARY_DIR}/install/config.ini" DESTINATION "${INSTALL_DIR_CONFIGFILE}") +install(DIRECTORY "${PROJECT_SOURCE_DIR}/assets" DESTINATION "${INSTALL_DIR_ASSETS}") if (PACKAGE) set(CPACK_PACKAGE_NAME ${EXECUTABLE_TITLE}) set(CPACK_GENERATOR ${PACKAGE}) include(CPack) endif () + +#Configure Linux application icons and .desktop file +if (UNIX) + configure_file("${PROJECT_SOURCE_DIR}/config/launcher.desktop.in" "${PROJECT_BINARY_DIR}/install/${EXECUTABLE_TITLE}.desktop") + install(FILES "${PROJECT_BINARY_DIR}/install/${EXECUTABLE_TITLE}.desktop" DESTINATION "${INSTALL_DIR_DESKTOP}") + set(INSTALL_DIR_ICONS "${CMAKE_INSTALL_PREFIX}/share/icons") + install(FILES "${PROJECT_SOURCE_DIR}/docs/flex-launcher.png" DESTINATION "${INSTALL_DIR_ICONS}/hicolor/48x48/apps") + install(FILES "${PROJECT_SOURCE_DIR}/docs/flex-launcher.svg" DESTINATION "${INSTALL_DIR_ICONS}/hicolor/scalable/apps") +endif () # Set paths for config files set(FONT_PREFIX "${PROJECT_BINARY_DIR}/assets/fonts/") set(ICONS_PREFIX "${PROJECT_BINARY_DIR}/assets/icons/") -if("${PROJECT_SOURCE_DIR}/build" STREQUAL "${PROJECT_BINARY_DIR}") - set(SVG_RELATIVE "../assets/") -else() - set(SVG_RELATIVE "../${EXECUTABLE_TITLE}/assets/") -endif() file(COPY "${PROJECT_SOURCE_DIR}/assets" DESTINATION ${PROJECT_BINARY_DIR}) # Generate build config file @@ -294,9 +220,9 @@ configure_file("${PROJECT_SOURCE_DIR}/config/config.ini.in" "${PROJECT_BINARY_DI # Generate install config file if (WIN32) - set(PATH_ASSETS_RELATIVE "./assets/") - set(FONT_PREFIX "${PATH_ASSETS_RELATIVE}fonts/") - set(ICONS_PREFIX "${PATH_ASSETS_RELATIVE}icons/") + set(PATH_ASSETS_RELATIVE "./assets") + set(FONT_PREFIX "${PATH_ASSETS_RELATIVE}/fonts/") + set(ICONS_PREFIX "${PATH_ASSETS_RELATIVE}/icons/") else () set(FONT_PREFIX "${INSTALL_DIR_ASSETS}/assets/fonts/") set(ICONS_PREFIX "${INSTALL_DIR_ASSETS}/assets/icons/") diff --git a/README.md b/README.md index d170790..12e188a 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- Logo + Logo @@ -27,91 +27,94 @@
  • Controls
  • Debugging
  • -
  • Contributing
  • +
  • Development Status
  • Documentation
  • Credits
  • ## About -Flex Launcher is a customizable application launcher designed with a [10 foot user interface](https://en.wikipedia.org/wiki/10-foot_user_interface). Its intended purpose is to simplify the control of a home theater or couch gaming PC by providing an interface that is similar to a TV set-top box or game console. Flex Launcher allows you to launch applications on your living room PC entirely by use of a TV remote or a gamepad. No keyboard or mouse required! +Flex Launcher is a customizable application launcher and front end designed with a TV-friendly [10 foot user interface](https://en.wikipedia.org/wiki/10-foot_user_interface), intending to mimic the look and feel of a streaming box or game console. Flex Launcher allows you to launch applications on your HTPC or couch gaming PC entirely by use of a TV remote or a gamepad. No keyboard or mouse required! Flex Launcher is compatible with both Windows and Linux (including Raspberry Pi devices). ## Screenshots -![Screenshot 1](extra/screenshots/screenshot1.png "Screenshot 1") -![Screenshot 1](extra/screenshots/screenshot2.png "Screenshot 2") +![Screenshot 1](docs/assets/screenshots/screenshot1.png "Screenshot 1") + +https://user-images.githubusercontent.com/95071366/208355237-11f00cbb-9cc3-436b-98f7-8de350e584a7.mp4 ## Installation -Compiled binaries are available for Windows 64 bit, Linux x86-64, and Raspberry Pi. Alternatively, you may also choose to compile the program yourself using the [compilation guide](extra/docs/compilation_guide.md). +Executables are available for Windows 64 bit, Linux x86-64, and Raspberry Pi. You can also compile the program yourself using the [compilation guide](docs/compilation.md). ### Windows -A win64 zip file is provided on the [release page](https://github.com/complexlogic/flex-launcher/releases). Simply download the file and extract the contents to a directory of your choosing. +Download the win64 .zip file from the [latest release](https://github.com/complexlogic/flex-launcher/releases/latest) and extract the contents to a directory of your choice. Flex Launcher should be run on an up-to-date Windows 10 system, or Windows 11. ### Linux Binary packages are available on the [release page](https://github.com/complexlogic/flex-launcher/releases) for APT and pacman based distributions. You may use the commands below to install. -#### APT-based x86-64 Distributions (Debian, Ubuntu, Mint, etc.) -``` -VERSION=1.4 -wget https://github.com/complexlogic/flex-launcher/releases/download/v${VERSION}/flex-launcher_${VERSION}_amd64.deb -sudo apt install ./flex-launcher_${VERSION}_amd64.deb +#### APT-based x86-64 Distributions (Debian, Ubuntu, etc.) +This package is compatible with Debian Bullseye and later, Ubuntu 21.04 and later. +```bash +wget https://github.com/complexlogic/flex-launcher/releases/download/v2.2/flex-launcher_2.2_amd64.deb +sudo apt install ./flex-launcher_2.2_amd64.deb ``` #### Pacman-based x86-64 Distributions (Arch, Manjaro, etc.) -``` -VERSION=1.4 -wget https://github.com/complexlogic/flex-launcher/releases/download/v${VERSION}/flex-launcher-${VERSION}-1-x86_64.pkg.tar.zst -sudo pacman -U flex-launcher-${VERSION}-1-x86_64.pkg.tar.zst +```bash +wget https://github.com/complexlogic/flex-launcher/releases/download/v2.2/flex-launcher-2.2-1-x86_64.pkg.tar.zst +sudo pacman -U flex-launcher-2.2-1-x86_64.pkg.tar.zst ``` #### Raspberry Pi -``` -VERSION=1.4 -wget https://github.com/complexlogic/flex-launcher/releases/download/v${VERSION}/flex-launcher_${VERSION}_armhf.deb -sudo apt install ./flex-launcher_${VERSION}_armhf.deb +This package is compatible with Raspbian Bullseye and later, 64 bit only. +```bash +wget https://github.com/complexlogic/flex-launcher/releases/download/v2.2/flex-launcher_2.2_arm64.deb +sudo apt install ./flex-launcher_2.2_arm64.deb ``` #### Copying Assets to Home Directory -The Linux packages install a default config file and assets to /usr/share/flex-launcher. It is strongly recommended to NOT edit this config file directly, as it will be overwritten if you upgrade to a later version of Flex Launcher. Instead, copy these files to your home directory and edit it there. -``` +The Linux packages install a default config file and assets to `/usr/share/flex-launcher`. It is strongly recommended to *not* edit this config file directly, as it will be overwritten if you upgrade to a later version of Flex Launcher. Instead, copy these files to your home directory and edit it there. +```bash cp -r /usr/share/flex-launcher ~/.config sed -i "s|/usr/share/flex-launcher|$HOME/.config/flex-launcher|g" ~/.config/flex-launcher/config.ini ``` ## Usage -Flex Launcher uses an INI file to configure the menus and settings. Upon startup, the program will search for a file named ```config.ini``` in the following locations in order: +Flex Launcher uses an INI file to configure the menus and settings. Upon startup, the program will search for a file named `config.ini` in the following locations in order: 1. The current working directory -2. The directory containing the ```flex-launcher``` executable -3. Linux only: ~/.config/flex-launcher -4. Linux only: /usr/share/flex-launcher +2. The directory containing the `flex-launcher` executable +3. Linux only: `~/.config/flex-launcher` +4. Linux only: `/usr/share/flex-launcher` If your config file is in one of the above locations, Flex Launcher can be started simply by double clicking the executable file or adding it to autostart. If your config file is in a non-standard location, you must specify the path via command line argument: -``` +```bash flex-launcher -c /path/to/config.ini ``` -Flex Launcher ships with a default config file which is intended strictly for demonstration purposes. If you try to start one of the applications, it is possible that nothing will happen because the install path is different on your system, or you don't have the application installed at all. See the [configuration file documentation](extra/docs/configuration.md) for instuctions on how to change the menus and settings. +Flex Launcher ships with a default config file which is intended strictly for demonstration purposes. If you try to start one of the applications, it is possible that nothing will happen because the install path is different on your system, or you don't have the application installed at all. See the [configuration file documentation](docs/configuration.md#configuring-flex-launcher) for instuctions on how to change the menus and settings. ### Controls -The keyboard arrow keys move the cursor left and right. Enter selects the current entry, backspace goes back to the previous menu (if applicable), and Esc quits the program. +The keyboard arrow keys move the highlight cursor left and right. Enter selects the current entry, backspace goes back to the previous menu (if applicable), and Esc quits the program. #### TV Remotes -Flex Launcher does not feature built-in decoding of IR or CEC signals. If you plan to use a TV remote to control the device, it is assumed that these signals are decoded by the OS or another program and mapped to keyboard presses, which can then be received by Flex Launcher. You can also use a hardware-based solution, such as the FLIRC USB device +Flex Launcher does not feature built-in decoding of IR or CEC signals. If you plan to use a TV remote to control the device, it is assumed that these signals are decoded by the OS or another program and mapped to keyboard presses, which can then be received by Flex Launcher. #### Gamepads -Gamepad controls are built-in to the program, but are disabled by default. To enable them, open your configuration file, and, under the "Gamepad" section, change the "Enabled" setting from false to true. After that, the gamepad controls should "Just Work" for most users. If your gamepad is not recognized automatically, or you want to change the default controls, see the [gamepad controls documentation](extra/docs/configuration.md#gamepad-controls). +Gamepad controls are built-in to the program, but are disabled by default. To enable them, open your configuration file and, under the "Gamepad" section, change the "Enabled" setting from false to true. After that, the gamepad controls should "Just Work" for most users. If your gamepad is not recognized automatically, or you want to change the default controls, see the [gamepad controls documentation](docs/configuration.md#gamepad-controls). ### Debugging Flex Launcher has a debug mode which may be enabled as follows: -``` +```bash flex-launcher -d ``` -This will output a logfile named ```flex-launcher.log``` in the same directory as ```flex-launcher.exe``` on Windows, and in ~/.local/share/flex-launcher on Linux. +This will output a logfile named `flex-launcher.log` in the same directory as `flex-launcher.exe` on Windows, and in `~/.local/share/flex-launcher` on Linux. -## Contributing -Contributions are welcome for bugfixes and new features. Please keep code formatted to [2 space K&R style](https://gist.github.com/jesseschalken/0f47a2b5a738ced9c845). +## Development Status +Flex Launcher has reached a mature state, and there are currently no feature releases planned for the future. I've started a [new HTPC launcher project](https://github.com/complexlogic/big-launcher) which is similar in nature to Flex Launcher, but aims to provide a more advanced, Smart TV-like user interface. My future development effort will be focused on that new project, but I will still maintain Flex Launcher for bugfixes and dependency updates. ## Documentation Here is a list of available documentation: -- [Configuration File](extra/docs/configuration.md) -- [Compilation Guide](extra/docs/compilation_guide.md) +- [Configuration](docs/configuration.md#configuring-flex-launcher) +- [General Setup Guide](docs/setup.md#setup-guide) + - [Windows-specific Setup Guide](docs/setup_windows.md#windows-setup-guide) + - [Linux-specific Setup Guide](docs/setup_linux.md#linux-setup-guide) +- [Compilation Guide](docs/compilation.md#compilation-guide) ## Credits Flex Launcher is made possible by the following projects: @@ -122,4 +125,4 @@ Flex Launcher is made possible by the following projects: - [inih](https://github.com/benhoyt/inih) - [Numix icons](https://github.com/numixproject) -Flex Launcher was inspired and strongly influenced by the excellent desktop application launcher [xlunch](https://github.com/Tomas-M/xlunch). +The design of Flex Launcher was strongly influenced by the excellent desktop application launcher [xlunch](https://github.com/Tomas-M/xlunch). diff --git a/UNLICENSE.txt b/UNLICENSE similarity index 100% rename from UNLICENSE.txt rename to UNLICENSE diff --git a/assets/fonts/Inter-Regular.ttf b/assets/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..cc73944 Binary files /dev/null and b/assets/fonts/Inter-Regular.ttf differ diff --git a/assets/fonts/Roboto-Regular.ttf b/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..3d6861b Binary files /dev/null and b/assets/fonts/Roboto-Regular.ttf differ diff --git a/assets/fonts/SourceSansPro-Regular.ttf b/assets/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000..5623846 Binary files /dev/null and b/assets/fonts/SourceSansPro-Regular.ttf differ diff --git a/assets/scroll_indicator.svg b/assets/scroll_indicator.svg deleted file mode 100755 index 8a39e3a..0000000 --- a/assets/scroll_indicator.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - diff --git a/cmake/FindSDL2.cmake b/cmake/FindSDL2.cmake deleted file mode 100755 index 2445d36..0000000 --- a/cmake/FindSDL2.cmake +++ /dev/null @@ -1,388 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -# Copyright 2019 Amine Ben Hassouna -# Copyright 2000-2019 Kitware, Inc. and Contributors -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: - -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. - -# * Neither the name of Kitware, Inc. nor the names of Contributors -# may be used to endorse or promote products derived from this -# software without specific prior written permission. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#[=======================================================================[.rst: -FindSDL2 --------- - -Locate SDL2 library - -This module defines the following 'IMPORTED' targets: - -:: - - SDL2::Core - The SDL2 library, if found. - Libraries should link to SDL2::Core - - SDL2::Main - The SDL2main library, if found. - Applications should link to SDL2::Main instead of SDL2::Core - - - -This module will set the following variables in your project: - -:: - - SDL2_LIBRARIES, the name of the library to link against - SDL2_INCLUDE_DIRS, where to find SDL.h - SDL2_FOUND, if false, do not try to link to SDL2 - SDL2MAIN_FOUND, if false, do not try to link to SDL2main - SDL2_VERSION_STRING, human-readable string containing the version of SDL2 - - - -This module responds to the following cache variables: - -:: - - SDL2_PATH - Set a custom SDL2 Library path (default: empty) - - SDL2_NO_DEFAULT_PATH - Disable search SDL2 Library in default path. - If SDL2_PATH (default: ON) - Else (default: OFF) - - SDL2_INCLUDE_DIR - SDL2 headers path. - - SDL2_LIBRARY - SDL2 Library (.dll, .so, .a, etc) path. - - SDL2MAIN_LIBRAY - SDL2main Library (.a) path. - - SDL2_BUILDING_LIBRARY - This flag is useful only when linking to SDL2_LIBRARIES insead of - SDL2::Main. It is required only when building a library that links to - SDL2_LIBRARIES, because only applications need main() (No need to also - link to SDL2main). - If this flag is defined, then no SDL2main will be added to SDL2_LIBRARIES - and no SDL2::Main target will be created. - - -Don't forget to include SDLmain.h and SDLmain.m in your project for the -OS X framework based version. (Other versions link to -lSDL2main which -this module will try to find on your behalf.) Also for OS X, this -module will automatically add the -framework Cocoa on your behalf. - - -Additional Note: If you see an empty SDL2_LIBRARY in your project -configuration, it means CMake did not find your SDL2 library -(SDL2.dll, libsdl2.so, SDL2.framework, etc). Set SDL2_LIBRARY to point -to your SDL2 library, and configure again. Similarly, if you see an -empty SDL2MAIN_LIBRARY, you should set this value as appropriate. These -values are used to generate the final SDL2_LIBRARIES variable and the -SDL2::Core and SDL2::Main targets, but when these values are unset, -SDL2_LIBRARIES, SDL2::Core and SDL2::Main does not get created. - - -$SDL2DIR is an environment variable that would correspond to the -./configure --prefix=$SDL2DIR used in building SDL2. l.e.galup 9-20-02 - - - -Created by Amine Ben Hassouna: - Adapt FindSDL.cmake to SDL2 (FindSDL2.cmake). - Add cache variables for more flexibility: - SDL2_PATH, SDL2_NO_DEFAULT_PATH (for details, see doc above). - Mark 'Threads' as a required dependency for non-OSX systems. - Modernize the FindSDL2.cmake module by creating specific targets: - SDL2::Core and SDL2::Main (for details, see doc above). - - -Original FindSDL.cmake module: - Modified by Eric Wing. Added code to assist with automated building - by using environmental variables and providing a more - controlled/consistent search behavior. Added new modifications to - recognize OS X frameworks and additional Unix paths (FreeBSD, etc). - Also corrected the header search path to follow "proper" SDL - guidelines. Added a search for SDLmain which is needed by some - platforms. Added a search for threads which is needed by some - platforms. Added needed compile switches for MinGW. - -On OSX, this will prefer the Framework version (if found) over others. -People will have to manually change the cache value of SDL2_LIBRARY to -override this selection or set the SDL2_PATH variable or the CMake -environment CMAKE_INCLUDE_PATH to modify the search paths. - -Note that the header path has changed from SDL/SDL.h to just SDL.h -This needed to change because "proper" SDL convention is #include -"SDL.h", not . This is done for portability reasons -because not all systems place things in SDL/ (see FreeBSD). -#]=======================================================================] - -# Define options for searching SDL2 Library in a custom path - -set(SDL2_PATH "" CACHE STRING "Custom SDL2 Library path") - -set(_SDL2_NO_DEFAULT_PATH OFF) -if(SDL2_PATH) - set(_SDL2_NO_DEFAULT_PATH ON) -endif() - -set(SDL2_NO_DEFAULT_PATH ${_SDL2_NO_DEFAULT_PATH} - CACHE BOOL "Disable search SDL2 Library in default path") -unset(_SDL2_NO_DEFAULT_PATH) - -set(SDL2_NO_DEFAULT_PATH_CMD) -if(SDL2_NO_DEFAULT_PATH) - set(SDL2_NO_DEFAULT_PATH_CMD NO_DEFAULT_PATH) -endif() - -# Search for the SDL2 include directory -find_path(SDL2_INCLUDE_DIR SDL.h - HINTS - ENV SDL2DIR - ${SDL2_NO_DEFAULT_PATH_CMD} - PATH_SUFFIXES SDL2 - # path suffixes to search inside ENV{SDL2DIR} - include/SDL2 include - PATHS ${SDL2_PATH} - DOC "Where the SDL2 headers can be found" -) - -set(SDL2_INCLUDE_DIRS "${SDL2_INCLUDE_DIR}") - -if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(VC_LIB_PATH_SUFFIX lib/x64) -else() - set(VC_LIB_PATH_SUFFIX lib/x86) -endif() - -# SDL-2.0 is the name used by FreeBSD ports... -# don't confuse it for the version number. -find_library(SDL2_LIBRARY - NAMES SDL2 SDL-2.0 - HINTS - ENV SDL2DIR - ${SDL2_NO_DEFAULT_PATH_CMD} - PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX} - PATHS ${SDL2_PATH} - DOC "Where the SDL2 Library can be found" -) - -set(SDL2_LIBRARIES "${SDL2_LIBRARY}") - -if(NOT SDL2_BUILDING_LIBRARY) - if(NOT SDL2_INCLUDE_DIR MATCHES ".framework") - # Non-OS X framework versions expect you to also dynamically link to - # SDL2main. This is mainly for Windows and OS X. Other (Unix) platforms - # seem to provide SDL2main for compatibility even though they don't - # necessarily need it. - - if(SDL2_PATH) - set(SDL2MAIN_LIBRARY_PATHS "${SDL2_PATH}") - endif() - - if(NOT SDL2_NO_DEFAULT_PATH) - set(SDL2MAIN_LIBRARY_PATHS - /sw - /opt/local - /opt/csw - /opt - "${SDL2MAIN_LIBRARY_PATHS}" - ) - endif() - - find_library(SDL2MAIN_LIBRARY - NAMES SDL2main - HINTS - ENV SDL2DIR - ${SDL2_NO_DEFAULT_PATH_CMD} - PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX} - PATHS ${SDL2MAIN_LIBRARY_PATHS} - DOC "Where the SDL2main library can be found" - ) - unset(SDL2MAIN_LIBRARY_PATHS) - endif() -endif() - -# SDL2 may require threads on your system. -# The Apple build may not need an explicit flag because one of the -# frameworks may already provide it. -# But for non-OSX systems, I will use the CMake Threads package. -if(NOT APPLE) - find_package(Threads QUIET) - if(NOT Threads_FOUND) - set(SDL2_THREADS_NOT_FOUND "Could NOT find Threads (Threads is required by SDL2).") - if(SDL2_FIND_REQUIRED) - message(FATAL_ERROR ${SDL2_THREADS_NOT_FOUND}) - else() - if(NOT SDL2_FIND_QUIETLY) - message(STATUS ${SDL2_THREADS_NOT_FOUND}) - endif() - return() - endif() - unset(SDL2_THREADS_NOT_FOUND) - endif() -endif() - -# MinGW needs an additional link flag, -mwindows -# It's total link flags should look like -lmingw32 -lSDL2main -lSDL2 -mwindows -if(MINGW) - set(MINGW32_LIBRARY mingw32 "-mwindows" CACHE STRING "link flags for MinGW") -endif() - -if(SDL2_LIBRARY) - # For SDL2main - if(SDL2MAIN_LIBRARY AND NOT SDL2_BUILDING_LIBRARY) - list(FIND SDL2_LIBRARIES "${SDL2MAIN_LIBRARY}" _SDL2_MAIN_INDEX) - if(_SDL2_MAIN_INDEX EQUAL -1) - set(SDL2_LIBRARIES "${SDL2MAIN_LIBRARY}" ${SDL2_LIBRARIES}) - endif() - unset(_SDL2_MAIN_INDEX) - endif() - - # For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa. - # CMake doesn't display the -framework Cocoa string in the UI even - # though it actually is there if I modify a pre-used variable. - # I think it has something to do with the CACHE STRING. - # So I use a temporary variable until the end so I can set the - # "real" variable in one-shot. - if(APPLE) - set(SDL2_LIBRARIES ${SDL2_LIBRARIES} -framework Cocoa) - endif() - - # For threads, as mentioned Apple doesn't need this. - # In fact, there seems to be a problem if I used the Threads package - # and try using this line, so I'm just skipping it entirely for OS X. - if(NOT APPLE) - set(SDL2_LIBRARIES ${SDL2_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) - endif() - - # For MinGW library - if(MINGW) - set(SDL2_LIBRARIES ${MINGW32_LIBRARY} ${SDL2_LIBRARIES}) - endif() - -endif() - -# Read SDL2 version -if(SDL2_INCLUDE_DIR AND EXISTS "${SDL2_INCLUDE_DIR}/SDL_version.h") - file(STRINGS "${SDL2_INCLUDE_DIR}/SDL_version.h" SDL2_VERSION_MAJOR_LINE REGEX "^#define[ \t]+SDL_MAJOR_VERSION[ \t]+[0-9]+$") - file(STRINGS "${SDL2_INCLUDE_DIR}/SDL_version.h" SDL2_VERSION_MINOR_LINE REGEX "^#define[ \t]+SDL_MINOR_VERSION[ \t]+[0-9]+$") - file(STRINGS "${SDL2_INCLUDE_DIR}/SDL_version.h" SDL2_VERSION_PATCH_LINE REGEX "^#define[ \t]+SDL_PATCHLEVEL[ \t]+[0-9]+$") - string(REGEX REPLACE "^#define[ \t]+SDL_MAJOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_VERSION_MAJOR "${SDL2_VERSION_MAJOR_LINE}") - string(REGEX REPLACE "^#define[ \t]+SDL_MINOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_VERSION_MINOR "${SDL2_VERSION_MINOR_LINE}") - string(REGEX REPLACE "^#define[ \t]+SDL_PATCHLEVEL[ \t]+([0-9]+)$" "\\1" SDL2_VERSION_PATCH "${SDL2_VERSION_PATCH_LINE}") - set(SDL2_VERSION_STRING ${SDL2_VERSION_MAJOR}.${SDL2_VERSION_MINOR}.${SDL2_VERSION_PATCH}) - unset(SDL2_VERSION_MAJOR_LINE) - unset(SDL2_VERSION_MINOR_LINE) - unset(SDL2_VERSION_PATCH_LINE) - unset(SDL2_VERSION_MAJOR) - unset(SDL2_VERSION_MINOR) - unset(SDL2_VERSION_PATCH) -endif() - -include(FindPackageHandleStandardArgs) - -FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2 - REQUIRED_VARS SDL2_LIBRARY SDL2_INCLUDE_DIR - VERSION_VAR SDL2_VERSION_STRING) - -if(SDL2MAIN_LIBRARY) - FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2main - REQUIRED_VARS SDL2MAIN_LIBRARY SDL2_INCLUDE_DIR - VERSION_VAR SDL2_VERSION_STRING) -endif() - - -mark_as_advanced(SDL2_PATH - SDL2_NO_DEFAULT_PATH - SDL2_LIBRARY - SDL2MAIN_LIBRARY - SDL2_INCLUDE_DIR - SDL2_BUILDING_LIBRARY) - - -# SDL2:: targets (SDL2::Core and SDL2::Main) -if(SDL2_FOUND) - - # SDL2::Core target - if(SDL2_LIBRARY AND NOT TARGET SDL2::Core) - add_library(SDL2::Core UNKNOWN IMPORTED) - set_target_properties(SDL2::Core PROPERTIES - IMPORTED_LOCATION "${SDL2_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INCLUDE_DIR}") - - if(APPLE) - # For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa. - # For more details, please see above. - set_property(TARGET SDL2::Core APPEND PROPERTY - INTERFACE_LINK_OPTIONS -framework Cocoa) - else() - # For threads, as mentioned Apple doesn't need this. - # For more details, please see above. - set_property(TARGET SDL2::Core APPEND PROPERTY - INTERFACE_LINK_LIBRARIES Threads::Threads) - endif() - endif() - - # SDL2::Main target - # Applications should link to SDL2::Main instead of SDL2::Core - # For more details, please see above. - if(NOT SDL2_BUILDING_LIBRARY AND NOT TARGET SDL2::Main) - - if(SDL2_INCLUDE_DIR MATCHES ".framework" OR NOT SDL2MAIN_LIBRARY) - add_library(SDL2::Main INTERFACE IMPORTED) - set_property(TARGET SDL2::Main PROPERTY - INTERFACE_LINK_LIBRARIES SDL2::Core) - elseif(SDL2MAIN_LIBRARY) - # MinGW requires that the mingw32 library is specified before the - # libSDL2main.a static library when linking. - # The SDL2::MainInternal target is used internally to make sure that - # CMake respects this condition. - add_library(SDL2::MainInternal UNKNOWN IMPORTED) - set_property(TARGET SDL2::MainInternal PROPERTY - IMPORTED_LOCATION "${SDL2MAIN_LIBRARY}") - set_property(TARGET SDL2::MainInternal PROPERTY - INTERFACE_LINK_LIBRARIES SDL2::Core) - - add_library(SDL2::Main INTERFACE IMPORTED) - - if(MINGW) - # MinGW needs an additional link flag '-mwindows' and link to mingw32 - set_property(TARGET SDL2::Main PROPERTY - INTERFACE_LINK_LIBRARIES "mingw32" "-mwindows") - endif() - - set_property(TARGET SDL2::Main APPEND PROPERTY - INTERFACE_LINK_LIBRARIES SDL2::MainInternal) - endif() - - endif() -endif() diff --git a/cmake/FindSDL2_image.cmake b/cmake/FindSDL2_image.cmake deleted file mode 100755 index 624e915..0000000 --- a/cmake/FindSDL2_image.cmake +++ /dev/null @@ -1,222 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -# Copyright 2019 Amine Ben Hassouna -# Copyright 2000-2019 Kitware, Inc. and Contributors -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: - -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. - -# * Neither the name of Kitware, Inc. nor the names of Contributors -# may be used to endorse or promote products derived from this -# software without specific prior written permission. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#[=======================================================================[.rst: -FindSDL2_image --------------- - -Locate SDL2_image library - -This module defines the following 'IMPORTED' target: - -:: - - SDL2::Image - The SDL2_image library, if found. - Have SDL2::Core as a link dependency. - - - -This module will set the following variables in your project: - -:: - - SDL2_IMAGE_LIBRARIES, the name of the library to link against - SDL2_IMAGE_INCLUDE_DIRS, where to find the headers - SDL2_IMAGE_FOUND, if false, do not try to link against - SDL2_IMAGE_VERSION_STRING - human-readable string containing the - version of SDL2_image - - - -This module responds to the following cache variables: - -:: - - SDL2_IMAGE_PATH - Set a custom SDL2_image Library path (default: empty) - - SDL2_IMAGE_NO_DEFAULT_PATH - Disable search SDL2_image Library in default path. - If SDL2_IMAGE_PATH (default: ON) - Else (default: OFF) - - SDL2_IMAGE_INCLUDE_DIR - SDL2_image headers path. - - SDL2_IMAGE_LIBRARY - SDL2_image Library (.dll, .so, .a, etc) path. - - -Additional Note: If you see an empty SDL2_IMAGE_LIBRARY in your project -configuration, it means CMake did not find your SDL2_image library -(SDL2_image.dll, libsdl2_image.so, etc). Set SDL2_IMAGE_LIBRARY to point -to your SDL2_image library, and configure again. This value is used to -generate the final SDL2_IMAGE_LIBRARIES variable and the SDL2::Image target, -but when this value is unset, SDL2_IMAGE_LIBRARIES and SDL2::Image does not -get created. - - -$SDL2IMAGEDIR is an environment variable that would correspond to the -./configure --prefix=$SDL2IMAGEDIR used in building SDL2_image. - -$SDL2DIR is an environment variable that would correspond to the -./configure --prefix=$SDL2DIR used in building SDL2. - - - -Created by Amine Ben Hassouna: - Adapt FindSDL_image.cmake to SDL2_image (FindSDL2_image.cmake). - Add cache variables for more flexibility: - SDL2_IMAGE_PATH, SDL2_IMAGE_NO_DEFAULT_PATH (for details, see doc above). - Add SDL2 as a required dependency. - Modernize the FindSDL2_image.cmake module by creating a specific target: - SDL2::Image (for details, see doc above). - -Original FindSDL_image.cmake module: - Created by Eric Wing. This was influenced by the FindSDL.cmake - module, but with modifications to recognize OS X frameworks and - additional Unix paths (FreeBSD, etc). -#]=======================================================================] - -# SDL2 Library required -find_package(SDL2 QUIET) -if(NOT SDL2_FOUND) - set(SDL2_IMAGE_SDL2_NOT_FOUND "Could NOT find SDL2 (SDL2 is required by SDL2_image).") - if(SDL2_image_FIND_REQUIRED) - message(FATAL_ERROR ${SDL2_IMAGE_SDL2_NOT_FOUND}) - else() - if(NOT SDL2_image_FIND_QUIETLY) - message(STATUS ${SDL2_IMAGE_SDL2_NOT_FOUND}) - endif() - return() - endif() - unset(SDL2_IMAGE_SDL2_NOT_FOUND) -endif() - - -# Define options for searching SDL2_image Library in a custom path - -set(SDL2_IMAGE_PATH "" CACHE STRING "Custom SDL2_image Library path") - -set(_SDL2_IMAGE_NO_DEFAULT_PATH OFF) -if(SDL2_IMAGE_PATH) - set(_SDL2_IMAGE_NO_DEFAULT_PATH ON) -endif() - -set(SDL2_IMAGE_NO_DEFAULT_PATH ${_SDL2_IMAGE_NO_DEFAULT_PATH} - CACHE BOOL "Disable search SDL2_image Library in default path") -unset(_SDL2_IMAGE_NO_DEFAULT_PATH) - -set(SDL2_IMAGE_NO_DEFAULT_PATH_CMD) -if(SDL2_IMAGE_NO_DEFAULT_PATH) - set(SDL2_IMAGE_NO_DEFAULT_PATH_CMD NO_DEFAULT_PATH) -endif() - -# Search for the SDL2_image include directory -find_path(SDL2_IMAGE_INCLUDE_DIR SDL_image.h - HINTS - ENV SDL2IMAGEDIR - ENV SDL2DIR - ${SDL2_IMAGE_NO_DEFAULT_PATH_CMD} - PATH_SUFFIXES SDL2 - # path suffixes to search inside ENV{SDL2DIR} - # and ENV{SDL2IMAGEDIR} - include/SDL2 include - PATHS ${SDL2_IMAGE_PATH} - DOC "Where the SDL2_image headers can be found" -) - -if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(VC_LIB_PATH_SUFFIX lib/x64) -else() - set(VC_LIB_PATH_SUFFIX lib/x86) -endif() - -# Search for the SDL2_image library -find_library(SDL2_IMAGE_LIBRARY - NAMES SDL2_image - HINTS - ENV SDL2IMAGEDIR - ENV SDL2DIR - ${SDL2_IMAGE_NO_DEFAULT_PATH_CMD} - PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX} - PATHS ${SDL2_IMAGE_PATH} - DOC "Where the SDL2_image Library can be found" -) - -# Read SDL2_image version -if(SDL2_IMAGE_INCLUDE_DIR AND EXISTS "${SDL2_IMAGE_INCLUDE_DIR}/SDL_image.h") - file(STRINGS "${SDL2_IMAGE_INCLUDE_DIR}/SDL_image.h" SDL2_IMAGE_VERSION_MAJOR_LINE REGEX "^#define[ \t]+SDL_IMAGE_MAJOR_VERSION[ \t]+[0-9]+$") - file(STRINGS "${SDL2_IMAGE_INCLUDE_DIR}/SDL_image.h" SDL2_IMAGE_VERSION_MINOR_LINE REGEX "^#define[ \t]+SDL_IMAGE_MINOR_VERSION[ \t]+[0-9]+$") - file(STRINGS "${SDL2_IMAGE_INCLUDE_DIR}/SDL_image.h" SDL2_IMAGE_VERSION_PATCH_LINE REGEX "^#define[ \t]+SDL_IMAGE_PATCHLEVEL[ \t]+[0-9]+$") - string(REGEX REPLACE "^#define[ \t]+SDL_IMAGE_MAJOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_IMAGE_VERSION_MAJOR "${SDL2_IMAGE_VERSION_MAJOR_LINE}") - string(REGEX REPLACE "^#define[ \t]+SDL_IMAGE_MINOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_IMAGE_VERSION_MINOR "${SDL2_IMAGE_VERSION_MINOR_LINE}") - string(REGEX REPLACE "^#define[ \t]+SDL_IMAGE_PATCHLEVEL[ \t]+([0-9]+)$" "\\1" SDL2_IMAGE_VERSION_PATCH "${SDL2_IMAGE_VERSION_PATCH_LINE}") - set(SDL2_IMAGE_VERSION_STRING ${SDL2_IMAGE_VERSION_MAJOR}.${SDL2_IMAGE_VERSION_MINOR}.${SDL2_IMAGE_VERSION_PATCH}) - unset(SDL2_IMAGE_VERSION_MAJOR_LINE) - unset(SDL2_IMAGE_VERSION_MINOR_LINE) - unset(SDL2_IMAGE_VERSION_PATCH_LINE) - unset(SDL2_IMAGE_VERSION_MAJOR) - unset(SDL2_IMAGE_VERSION_MINOR) - unset(SDL2_IMAGE_VERSION_PATCH) -endif() - -set(SDL2_IMAGE_LIBRARIES ${SDL2_IMAGE_LIBRARY}) -set(SDL2_IMAGE_INCLUDE_DIRS ${SDL2_IMAGE_INCLUDE_DIR}) - -include(FindPackageHandleStandardArgs) - -FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2_image - REQUIRED_VARS SDL2_IMAGE_LIBRARIES SDL2_IMAGE_INCLUDE_DIRS - VERSION_VAR SDL2_IMAGE_VERSION_STRING) - - -mark_as_advanced(SDL2_IMAGE_PATH - SDL2_IMAGE_NO_DEFAULT_PATH - SDL2_IMAGE_LIBRARY - SDL2_IMAGE_INCLUDE_DIR) - - -if(SDL2_IMAGE_FOUND) - - # SDL2::Image target - if(SDL2_IMAGE_LIBRARY AND NOT TARGET SDL2::Image) - add_library(SDL2::Image UNKNOWN IMPORTED) - set_target_properties(SDL2::Image PROPERTIES - IMPORTED_LOCATION "${SDL2_IMAGE_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${SDL2_IMAGE_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES SDL2::Core) - endif() -endif() diff --git a/cmake/FindSDL2_ttf.cmake b/cmake/FindSDL2_ttf.cmake deleted file mode 100755 index 2ab9a76..0000000 --- a/cmake/FindSDL2_ttf.cmake +++ /dev/null @@ -1,222 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -# Copyright 2019 Amine Ben Hassouna -# Copyright 2000-2019 Kitware, Inc. and Contributors -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: - -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. - -# * Neither the name of Kitware, Inc. nor the names of Contributors -# may be used to endorse or promote products derived from this -# software without specific prior written permission. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#[=======================================================================[.rst: -FindSDL2_ttf ------------- - -Locate SDL2_ttf library - -This module defines the following 'IMPORTED' target: - -:: - - SDL2::TTF - The SDL2_ttf library, if found. - Have SDL2::Core as a link dependency. - - - -This module will set the following variables in your project: - -:: - - SDL2_TTF_LIBRARIES, the name of the library to link against - SDL2_TTF_INCLUDE_DIRS, where to find the headers - SDL2_TTF_FOUND, if false, do not try to link against - SDL2_TTF_VERSION_STRING - human-readable string containing the - version of SDL2_ttf - - - -This module responds to the following cache variables: - -:: - - SDL2_TTF_PATH - Set a custom SDL2_ttf Library path (default: empty) - - SDL2_TTF_NO_DEFAULT_PATH - Disable search SDL2_ttf Library in default path. - If SDL2_TTF_PATH (default: ON) - Else (default: OFF) - - SDL2_TTF_INCLUDE_DIR - SDL2_ttf headers path. - - SDL2_TTF_LIBRARY - SDL2_ttf Library (.dll, .so, .a, etc) path. - - -Additional Note: If you see an empty SDL2_TTF_LIBRARY in your project -configuration, it means CMake did not find your SDL2_ttf library -(SDL2_ttf.dll, libsdl2_ttf.so, etc). Set SDL2_TTF_LIBRARY to point -to your SDL2_ttf library, and configure again. This value is used to -generate the final SDL2_TTF_LIBRARIES variable and the SDL2::TTF target, -but when this value is unset, SDL2_TTF_LIBRARIES and SDL2::TTF does not -get created. - - -$SDL2TTFDIR is an environment variable that would correspond to the -./configure --prefix=$SDL2TTFDIR used in building SDL2_ttf. - -$SDL2DIR is an environment variable that would correspond to the -./configure --prefix=$SDL2DIR used in building SDL2. - - - -Created by Amine Ben Hassouna: - Adapt FindSDL_ttf.cmake to SDL2_ttf (FindSDL2_ttf.cmake). - Add cache variables for more flexibility: - SDL2_TTF_PATH, SDL2_TTF_NO_DEFAULT_PATH (for details, see doc above). - Add SDL2 as a required dependency. - Modernize the FindSDL2_ttf.cmake module by creating a specific target: - SDL2::TTF (for details, see doc above). - -Original FindSDL_ttf.cmake module: - Created by Eric Wing. This was influenced by the FindSDL.cmake - module, but with modifications to recognize OS X frameworks and - additional Unix paths (FreeBSD, etc). -#]=======================================================================] - -# SDL2 Library required -find_package(SDL2 QUIET) -if(NOT SDL2_FOUND) - set(SDL2_TTF_SDL2_NOT_FOUND "Could NOT find SDL2 (SDL2 is required by SDL2_ttf).") - if(SDL2_ttf_FIND_REQUIRED) - message(FATAL_ERROR ${SDL2_TTF_SDL2_NOT_FOUND}) - else() - if(NOT SDL2_ttf_FIND_QUIETLY) - message(STATUS ${SDL2_TTF_SDL2_NOT_FOUND}) - endif() - return() - endif() - unset(SDL2_TTF_SDL2_NOT_FOUND) -endif() - - -# Define options for searching SDL2_ttf Library in a custom path - -set(SDL2_TTF_PATH "" CACHE STRING "Custom SDL2_ttf Library path") - -set(_SDL2_TTF_NO_DEFAULT_PATH OFF) -if(SDL2_TTF_PATH) - set(_SDL2_TTF_NO_DEFAULT_PATH ON) -endif() - -set(SDL2_TTF_NO_DEFAULT_PATH ${_SDL2_TTF_NO_DEFAULT_PATH} - CACHE BOOL "Disable search SDL2_ttf Library in default path") -unset(_SDL2_TTF_NO_DEFAULT_PATH) - -set(SDL2_TTF_NO_DEFAULT_PATH_CMD) -if(SDL2_TTF_NO_DEFAULT_PATH) - set(SDL2_TTF_NO_DEFAULT_PATH_CMD NO_DEFAULT_PATH) -endif() - -# Search for the SDL2_ttf include directory -find_path(SDL2_TTF_INCLUDE_DIR SDL_ttf.h - HINTS - ENV SDL2TTFDIR - ENV SDL2DIR - ${SDL2_TTF_NO_DEFAULT_PATH_CMD} - PATH_SUFFIXES SDL2 - # path suffixes to search inside ENV{SDL2DIR} - # and ENV{SDL2TTFDIR} - include/SDL2 include - PATHS ${SDL2_TTF_PATH} - DOC "Where the SDL2_ttf headers can be found" -) - -if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(VC_LIB_PATH_SUFFIX lib/x64) -else() - set(VC_LIB_PATH_SUFFIX lib/x86) -endif() - -# Search for the SDL2_ttf library -find_library(SDL2_TTF_LIBRARY - NAMES SDL2_ttf - HINTS - ENV SDL2TTFDIR - ENV SDL2DIR - ${SDL2_TTF_NO_DEFAULT_PATH_CMD} - PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX} - PATHS ${SDL2_TTF_PATH} - DOC "Where the SDL2_ttf Library can be found" -) - -# Read SDL2_ttf version -if(SDL2_TTF_INCLUDE_DIR AND EXISTS "${SDL2_TTF_INCLUDE_DIR}/SDL_ttf.h") - file(STRINGS "${SDL2_TTF_INCLUDE_DIR}/SDL_ttf.h" SDL2_TTF_VERSION_MAJOR_LINE REGEX "^#define[ \t]+SDL_TTF_MAJOR_VERSION[ \t]+[0-9]+$") - file(STRINGS "${SDL2_TTF_INCLUDE_DIR}/SDL_ttf.h" SDL2_TTF_VERSION_MINOR_LINE REGEX "^#define[ \t]+SDL_TTF_MINOR_VERSION[ \t]+[0-9]+$") - file(STRINGS "${SDL2_TTF_INCLUDE_DIR}/SDL_ttf.h" SDL2_TTF_VERSION_PATCH_LINE REGEX "^#define[ \t]+SDL_TTF_PATCHLEVEL[ \t]+[0-9]+$") - string(REGEX REPLACE "^#define[ \t]+SDL_TTF_MAJOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_TTF_VERSION_MAJOR "${SDL2_TTF_VERSION_MAJOR_LINE}") - string(REGEX REPLACE "^#define[ \t]+SDL_TTF_MINOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_TTF_VERSION_MINOR "${SDL2_TTF_VERSION_MINOR_LINE}") - string(REGEX REPLACE "^#define[ \t]+SDL_TTF_PATCHLEVEL[ \t]+([0-9]+)$" "\\1" SDL2_TTF_VERSION_PATCH "${SDL2_TTF_VERSION_PATCH_LINE}") - set(SDL2_TTF_VERSION_STRING ${SDL2_TTF_VERSION_MAJOR}.${SDL2_TTF_VERSION_MINOR}.${SDL2_TTF_VERSION_PATCH}) - unset(SDL2_TTF_VERSION_MAJOR_LINE) - unset(SDL2_TTF_VERSION_MINOR_LINE) - unset(SDL2_TTF_VERSION_PATCH_LINE) - unset(SDL2_TTF_VERSION_MAJOR) - unset(SDL2_TTF_VERSION_MINOR) - unset(SDL2_TTF_VERSION_PATCH) -endif() - -set(SDL2_TTF_LIBRARIES ${SDL2_TTF_LIBRARY}) -set(SDL2_TTF_INCLUDE_DIRS ${SDL2_TTF_INCLUDE_DIR}) - -include(FindPackageHandleStandardArgs) - -FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2_ttf - REQUIRED_VARS SDL2_TTF_LIBRARIES SDL2_TTF_INCLUDE_DIRS - VERSION_VAR SDL2_TTF_VERSION_STRING) - - -mark_as_advanced(SDL2_TTF_PATH - SDL2_TTF_NO_DEFAULT_PATH - SDL2_TTF_LIBRARY - SDL2_TTF_INCLUDE_DIR) - - -if(SDL2_TTF_FOUND) - - # SDL2::TTF target - if(SDL2_TTF_LIBRARY AND NOT TARGET SDL2::TTF) - add_library(SDL2::TTF UNKNOWN IMPORTED) - set_target_properties(SDL2::TTF PROPERTIES - IMPORTED_LOCATION "${SDL2_TTF_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${SDL2_TTF_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES SDL2::Core) - endif() -endif() diff --git a/config/PKGBUILD.in b/config/PKGBUILD.in index 2f47b12..1c7c5fe 100644 --- a/config/PKGBUILD.in +++ b/config/PKGBUILD.in @@ -7,7 +7,7 @@ arch=('x86_64') url="@CMAKE_PROJECT_HOMEPAGE_URL@" license=('Unlicense') #groups=() -depends=('sdl2' 'sdl2_image' 'sdl2_ttf') +depends=('sdl2' 'sdl2_image' 'sdl2_ttf' 'libinih') makedepends=('cmake') #checkdepends=() #optdepends=() diff --git a/config/config.ini.in b/config/config.ini.in index 6b7afcb..3cc8b55 100644 --- a/config/config.ini.in +++ b/config/config.ini.in @@ -1,33 +1,64 @@ # @PROJECT_NAME@ v@PROJECT_VERSION@ sample configuration file -# For documentation of these settings, visit: https://github.com/complexlogic/flex-launcher/blob/master/extra/docs/configuration.md -[Settings] +# For documentation of these settings, visit: https://complexlogic.github.io/flex-launcher/configuration +[General] @SETTING_DEFAULT_MENU@=@DEFAULT_MENU@ -@SETTING_MAX_BUTTONS@=@DEFAULT_MAX_BUTTONS@ +@SETTING_VSYNC@=@DEFAULT_VSYNC@ +#@SETTING_FPS_LIMIT@= +#@SETTING_APPLICATION_TIMEOUT@=@DEFAULT_APPLICATION_TIMEOUT@ +@SETTING_ON_LAUNCH@=@DEFAULT_ON_LAUNCH@ +@SETTING_WRAP_ENTRIES@=@DEFAULT_WRAP_ENTRIES@ +@SETTING_RESET_ON_BACK@=@DEFAULT_RESET_ON_BACK@ +@SETTING_MOUSE_SELECT@=@DEFAULT_MOUSE_SELECT@ +@SETTING_INHIBIT_OS_SCREENSAVER@=@DEFAULT_INHIBIT_OS_SCREENSAVER@ +#@SETTING_STARTUP_CMD@= +#@SETTING_QUIT_CMD@= + +[Background] @SETTING_BACKGROUND_MODE@=@DEFAULT_BACKGROUND_MODE@ -@SETTING_BACKGROUND_COLOR@=@DEFAULT_BACKGROUND_COLOR_R@@DEFAULT_BACKGROUND_COLOR_G@@DEFAULT_BACKGROUND_COLOR_B@ +@SETTING_BACKGROUND_COLOR@=#@DEFAULT_BACKGROUND_COLOR_R@@DEFAULT_BACKGROUND_COLOR_G@@DEFAULT_BACKGROUND_COLOR_B@ #@SETTING_BACKGROUND_IMAGE@= #@SETTING_SLIDESHOW_DIRECTORY@= #@SETTING_SLIDESHOW_IMAGE_DURATION@=@DEFAULT_SLIDESHOW_IMAGE_DURATION_CONFIG@ #@SETTING_SLIDESHOW_TRANSITION_TIME@=@DEFAULT_SLIDESHOW_TRANSITION_TIME_CONFIG@ +#@SETTING_CHROMA_KEY_COLOR@=#@DEFAULT_CHROMA_KEY_COLOR_R@@DEFAULT_CHROMA_KEY_COLOR_G@@DEFAULT_CHROMA_KEY_COLOR_B@ +@SETTING_BACKGROUND_OVERLAY@=@DEFAULT_BACKGROUND_OVERLAY@ +@SETTING_BACKGROUND_OVERLAY_COLOR@=#@DEFAULT_BACKGROUND_OVERLAY_COLOR_R@@DEFAULT_BACKGROUND_OVERLAY_COLOR_G@@DEFAULT_BACKGROUND_OVERLAY_COLOR_B@ +@SETTING_BACKGROUND_OVERLAY_OPACITY@=@DEFAULT_BACKGROUND_OVERLAY_OPACITY@ + +[Layout] +@SETTING_MAX_BUTTONS@=@DEFAULT_MAX_BUTTONS@ @SETTING_ICON_SIZE@=@DEFAULT_ICON_SIZE@ @SETTING_ICON_SPACING@=@DEFAULT_ICON_SPACING@ +@SETTING_VCENTER@=@DEFAULT_VCENTER@ + +[Titles] +@SETTING_TITLES_ENABLED@=@DEFAULT_TITLES_ENABLED@ @SETTING_TITLE_FONT@=@FONT_PREFIX@@DEFAULT_FONT@ @SETTING_TITLE_FONT_SIZE@=@DEFAULT_FONT_SIZE@ -@SETTING_TITLE_COLOR@=@DEFAULT_TITLE_COLOR_R@@DEFAULT_TITLE_COLOR_G@@DEFAULT_TITLE_COLOR_B@ +@SETTING_TITLE_FONT_COLOR@=#@DEFAULT_TITLE_FONT_COLOR_R@@DEFAULT_TITLE_FONT_COLOR_G@@DEFAULT_TITLE_FONT_COLOR_B@ @SETTING_TITLE_OPACITY@=@DEFAULT_TITLE_OPACITY@ +@SETTING_TITLE_SHADOWS@=@DEFAULT_TITLE_SHADOWS@ +@SETTING_TITLE_SHADOW_COLOR@=#@DEFAULT_TITLE_SHADOW_COLOR_R@@DEFAULT_TITLE_SHADOW_COLOR_G@@DEFAULT_TITLE_SHADOW_COLOR_B@ @SETTING_TITLE_OVERSIZE_MODE@=@DEFAULT_TITLE_OVERSIZE_MODE@ @SETTING_TITLE_PADDING@=@DEFAULT_TITLE_PADDING@ -@SETTING_HIGHLIGHT_COLOR@=@DEFAULT_HIGHLIGHT_COLOR_R@@DEFAULT_HIGHLIGHT_COLOR_G@@DEFAULT_HIGHLIGHT_COLOR_B@ -@SETTING_HIGHLIGHT_OPACITY@=@DEFAULT_HIGHLIGHT_OPACITY@ + +[Highlight] +@SETTING_HIGHLIGHT_ENABLED@=@DEFAULT_HIGHLIGHT_ENABLED@ +@SETTING_HIGHLIGHT_FILL_COLOR@=#@DEFAULT_HIGHLIGHT_FILL_COLOR_R@@DEFAULT_HIGHLIGHT_FILL_COLOR_G@@DEFAULT_HIGHLIGHT_FILL_COLOR_B@ +@SETTING_HIGHLIGHT_FILL_OPACITY@=@DEFAULT_HIGHLIGHT_FILL_OPACITY@ +@SETTING_HIGHLIGHT_OUTLINE_SIZE@=@DEFAULT_HIGHLIGHT_OUTLINE_SIZE@ +@SETTING_HIGHLIGHT_OUTLINE_COLOR@=#@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R@@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G@@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B@ +@SETTING_HIGHLIGHT_OUTLINE_OPACITY@=@DEFAULT_HIGHLIGHT_OUTLINE_OPACITY@ @SETTING_HIGHLIGHT_CORNER_RADIUS@=@DEFAULT_HIGHLIGHT_CORNER_RADIUS@ @SETTING_HIGHLIGHT_VPADDING@=@DEFAULT_HIGHLIGHT_VPADDING@ @SETTING_HIGHLIGHT_HPADDING@=@DEFAULT_HIGHLIGHT_HPADDING@ -@SETTING_BUTTON_CENTERLINE@=@DEFAULT_BUTTON_CENTERLINE@ + +[Scroll Indicators] @SETTING_SCROLL_INDICATORS@=@DEFAULT_SCROLL_INDICATORS@ -@SETTING_SCROLL_INDICATOR_COLOR@=@DEFAULT_SCROLL_INDICATOR_COLOR_R@@DEFAULT_SCROLL_INDICATOR_COLOR_G@@DEFAULT_SCROLL_INDICATOR_COLOR_B@ +@SETTING_SCROLL_INDICATOR_FILL_COLOR@=#@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R@@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G@@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B@ +@SETTING_SCROLL_INDICATOR_OUTLINE_SIZE@=@DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE@ +@SETTING_SCROLL_INDICATOR_OUTLINE_COLOR@=#@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R@@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G@@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B@ @SETTING_SCROLL_INDICATOR_OPACITY@=@DEFAULT_SCROLL_INDICATOR_OPACITY@ -@SETTING_ON_LAUNCH@=@DEFAULT_ON_LAUNCH@ -@SETTING_RESET_ON_BACK@=@DEFAULT_RESET_ON_BACK@ [Clock] @SETTING_CLOCK_ENABLED@=@DEFAULT_CLOCK_ENABLED@ @@ -35,8 +66,10 @@ @SETTING_CLOCK_ALIGNMENT@=@DEFAULT_CLOCK_ALIGNMENT@ @SETTING_CLOCK_FONT@=@FONT_PREFIX@@DEFAULT_CLOCK_FONT@ @SETTING_CLOCK_FONT_SIZE@=@DEFAULT_CLOCK_FONT_SIZE@ +@SETTING_CLOCK_FONT_COLOR@=#@DEFAULT_CLOCK_FONT_COLOR_R@@DEFAULT_CLOCK_FONT_COLOR_G@@DEFAULT_CLOCK_FONT_COLOR_B@ +@SETTING_CLOCK_SHADOWS@=@DEFAULT_CLOCK_SHADOWS@ +@SETTING_CLOCK_SHADOW_COLOR@=#@DEFAULT_CLOCK_SHADOW_COLOR_R@@DEFAULT_CLOCK_SHADOW_COLOR_G@@DEFAULT_CLOCK_SHADOW_COLOR_B@ @SETTING_CLOCK_MARGIN@=@DEFAULT_CLOCK_MARGIN@ -@SETTING_CLOCK_COLOR@=@DEFAULT_CLOCK_COLOR_R@@DEFAULT_CLOCK_COLOR_G@@DEFAULT_CLOCK_COLOR_B@ @SETTING_CLOCK_OPACITY@=@DEFAULT_CLOCK_OPACITY@ @SETTING_CLOCK_TIME_FORMAT@=@DEFAULT_CLOCK_TIME_FORMAT@ @SETTING_CLOCK_DATE_FORMAT@=@DEFAULT_CLOCK_DATE_FORMAT@ @@ -50,11 +83,11 @@ [Hotkeys] # Esc to quit -Hotkey1=1B;:quit +Hotkey1=#1B;:quit [Gamepad] @SETTING_GAMEPAD_ENABLED@=@DEFAULT_GAMEPAD_ENABLED@ -#@SETTING_GAMEPAD_DEVICE@=@DEFAULT_GAMEPAD_DEVICE@ +@SETTING_GAMEPAD_DEVICE@=@DEFAULT_GAMEPAD_DEVICE@ #@SETTING_GAMEPAD_MAPPINGS_FILE@= @SETTING_GAMEPAD_LSTICK_XM@=:left @SETTING_GAMEPAD_LSTICK_XP@=:right diff --git a/config/config_settings.cmake b/config/config_settings.cmake new file mode 100644 index 0000000..13f22df --- /dev/null +++ b/config/config_settings.cmake @@ -0,0 +1,192 @@ +# Config setting keys +set(SETTING_DEFAULT_MENU "DefaultMenu") +set(SETTING_MAX_BUTTONS "MaxButtons") +set(SETTING_VSYNC "VSync") +set(SETTING_FPS_LIMIT "FPSLimit") +set(SETTING_APPLICATION_TIMEOUT "ApplicationTimeout") +set(SETTING_WRAP_ENTRIES "WrapEntries") +set(SETTING_BACKGROUND_MODE "Mode") +set(SETTING_BACKGROUND_COLOR "Color") +set(SETTING_BACKGROUND_IMAGE "Image") +set(SETTING_SLIDESHOW_DIRECTORY "SlideshowDirectory") +set(SETTING_SLIDESHOW_IMAGE_DURATION "SlideshowImageDuration") +set(SETTING_SLIDESHOW_TRANSITION_TIME "SlideshowTransitionTime") +set(SETTING_CHROMA_KEY_COLOR "ChromaKeyColor") +set(SETTING_BACKGROUND_OVERLAY "Overlay") +set(SETTING_BACKGROUND_OVERLAY_COLOR "OverlayColor") +set(SETTING_BACKGROUND_OVERLAY_OPACITY "OverlayOpacity") +set(SETTING_ICON_SIZE "IconSize") +set(SETTING_ICON_SPACING "IconSpacing") +set(SETTING_TITLES_ENABLED "Enabled") +set(SETTING_TITLE_FONT "Font") +set(SETTING_TITLE_FONT_SIZE "FontSize") +set(SETTING_TITLE_FONT_COLOR "Color") +set(SETTING_TITLE_SHADOWS "Shadows") +set(SETTING_TITLE_SHADOW_COLOR "ShadowColor") +set(SETTING_TITLE_OPACITY "Opacity") +set(SETTING_TITLE_OVERSIZE_MODE "OversizeMode") +set(SETTING_TITLE_PADDING "Padding") +set(SETTING_HIGHLIGHT_ENABLED "Enabled") +set(SETTING_HIGHLIGHT_FILL_COLOR "FillColor") +set(SETTING_HIGHLIGHT_FILL_OPACITY "FillOpacity") +set(SETTING_HIGHLIGHT_OUTLINE_SIZE "OutlineSize") +set(SETTING_HIGHLIGHT_OUTLINE_COLOR "OutlineColor") +set(SETTING_HIGHLIGHT_OUTLINE_OPACITY "OutlineOpacity") +set(SETTING_HIGHLIGHT_CORNER_RADIUS "CornerRadius") +set(SETTING_HIGHLIGHT_VPADDING "VPadding") +set(SETTING_HIGHLIGHT_HPADDING "HPadding") +set(SETTING_VCENTER "VCenter") +set(SETTING_SCROLL_INDICATORS "Enabled") +set(SETTING_SCROLL_INDICATOR_FILL_COLOR "FillColor") +set(SETTING_SCROLL_INDICATOR_OUTLINE_SIZE "OutlineSize") +set(SETTING_SCROLL_INDICATOR_OUTLINE_COLOR "OutlineColor") +set(SETTING_SCROLL_INDICATOR_OPACITY "Opacity") +set(SETTING_ON_LAUNCH "OnLaunch") +set(SETTING_RESET_ON_BACK "ResetOnBack") +set(SETTING_MOUSE_SELECT "MouseSelect") +set(SETTING_INHIBIT_OS_SCREENSAVER "InhibitOSScreensaver") +set(SETTING_STARTUP_CMD "StartupCmd") +set(SETTING_QUIT_CMD "QuitCmd") +set(SETTING_CLOCK_ENABLED "Enabled") +set(SETTING_CLOCK_SHOW_DATE "ShowDate") +set(SETTING_CLOCK_ALIGNMENT "Alignment") +set(SETTING_CLOCK_FONT "Font") +set(SETTING_CLOCK_FONT_COLOR "FontColor") +set(SETTING_CLOCK_SHADOWS "Shadows") +set(SETTING_CLOCK_SHADOW_COLOR "ShadowColor") +set(SETTING_CLOCK_OPACITY "Opacity") +set(SETTING_CLOCK_FONT_SIZE "FontSize") +set(SETTING_CLOCK_MARGIN "Margin") +set(SETTING_CLOCK_TIME_FORMAT "TimeFormat") +set(SETTING_CLOCK_DATE_FORMAT "DateFormat") +set(SETTING_CLOCK_INCLUDE_WEEKDAY "IncludeWeekday") +set(SETTING_SCREENSAVER_ENABLED "Enabled") +set(SETTING_SCREENSAVER_IDLE_TIME "IdleTime") +set(SETTING_SCREENSAVER_INTENSITY "Intensity") +set(SETTING_SCREENSAVER_PAUSE_SLIDESHOW "PauseSlideshow") +set(SETTING_GAMEPAD_ENABLED "Enabled") +set(SETTING_GAMEPAD_DEVICE "DeviceIndex") +set(SETTING_GAMEPAD_MAPPINGS_FILE "ControllerMappingsFile") +set(SETTING_GAMEPAD_LSTICK_XM "LStickX-") +set(SETTING_GAMEPAD_LSTICK_XP "LStickX+") +set(SETTING_GAMEPAD_LSTICK_YM "LStickY-") +set(SETTING_GAMEPAD_LSTICK_YP "LStickY+") +set(SETTING_GAMEPAD_RSTICK_XM "RStickX-") +set(SETTING_GAMEPAD_RSTICK_XP "RStickX+") +set(SETTING_GAMEPAD_RSTICK_YM "RStickY-") +set(SETTING_GAMEPAD_RSTICK_YP "RStickY+") +set(SETTING_GAMEPAD_LTRIGGER "LTrigger") +set(SETTING_GAMEPAD_RTRIGGER "RTrigger") +set(SETTING_GAMEPAD_BUTTON_A "ButtonA") +set(SETTING_GAMEPAD_BUTTON_B "ButtonB") +set(SETTING_GAMEPAD_BUTTON_X "ButtonX") +set(SETTING_GAMEPAD_BUTTON_Y "ButtonY") +set(SETTING_GAMEPAD_BUTTON_BACK "ButtonBack") +set(SETTING_GAMEPAD_BUTTON_GUIDE "ButtonGuide") +set(SETTING_GAMEPAD_BUTTON_START "ButtonStart") +set(SETTING_GAMEPAD_BUTTON_LEFT_STICK "ButtonLeftStick") +set(SETTING_GAMEPAD_BUTTON_RIGHT_STICK "ButtonRightStick") +set(SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER "ButtonLeftShoulder") +set(SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER "ButtonRightShoulder") +set(SETTING_GAMEPAD_BUTTON_DPAD_UP "ButtonDPadUp") +set(SETTING_GAMEPAD_BUTTON_DPAD_DOWN "ButtonDPadDown") +set(SETTING_GAMEPAD_BUTTON_DPAD_LEFT "ButtonDPadLeft") +set(SETTING_GAMEPAD_BUTTON_DPAD_RIGHT "ButtonDPadRight") + +# Default settings +set(DEFAULT_MENU "Main") +set(DEFAULT_MAX_BUTTONS 4) +set(DEFAULT_VSYNC "true") +set(DEFAULT_APPLICATION_TIMEOUT "15") +set(DEFAULT_WRAP_ENTRIES "false") +set(DEFAULT_BACKGROUND_MODE "Color") +set(DEFAULT_BACKGROUND_COLOR_R "00") +set(DEFAULT_BACKGROUND_COLOR_G "00") +set(DEFAULT_BACKGROUND_COLOR_B "00") +set(DEFAULT_SLIDESHOW_IMAGE_DURATION "30000") +set(DEFAULT_SLIDESHOW_IMAGE_DURATION_CONFIG "30") +set(DEFAULT_SLIDESHOW_TRANSITION_TIME "1500") +set(DEFAULT_SLIDESHOW_TRANSITION_TIME_CONFIG "3") +set(DEFAULT_CHROMA_KEY_COLOR_R "01") +set(DEFAULT_CHROMA_KEY_COLOR_G "01") +set(DEFAULT_CHROMA_KEY_COLOR_B "01") +set(DEFAULT_CHROMA_KEY_COLOR_A "FF") +set(DEFAULT_BACKGROUND_OVERLAY "false") +set(DEFAULT_BACKGROUND_OVERLAY_COLOR_R "00") +set(DEFAULT_BACKGROUND_OVERLAY_COLOR_G "00") +set(DEFAULT_BACKGROUND_OVERLAY_COLOR_B "00") +set(DEFAULT_BACKGROUND_OVERLAY_COLOR_A "7F") +set(DEFAULT_BACKGROUND_OVERLAY_OPACITY "50%") +set(DEFAULT_ICON_SIZE 256) +set(DEFAULT_ICON_SPACING "5%") +set(DEFAULT_FONT "OpenSans-Regular.ttf") +set(DEFAULT_FONT_SIZE 36) +set(DEFAULT_TITLES_ENABLED "true") +set(DEFAULT_TITLE_FONT_COLOR_R "FF") +set(DEFAULT_TITLE_FONT_COLOR_G "FF") +set(DEFAULT_TITLE_FONT_COLOR_B "FF") +set(DEFAULT_TITLE_FONT_COLOR_A "FF") +set(DEFAULT_TITLE_SHADOWS "false") +set(DEFAULT_TITLE_SHADOW_COLOR_R "00") +set(DEFAULT_TITLE_SHADOW_COLOR_G "00") +set(DEFAULT_TITLE_SHADOW_COLOR_B "00") +set(DEFAULT_TITLE_SHADOW_COLOR_A "FF") +set(DEFAULT_TITLE_OPACITY "100%") +set(DEFAULT_TITLE_OVERSIZE_MODE "Shrink") +set(DEFAULT_TITLE_PADDING 20) +set(DEFAULT_HIGHLIGHT_ENABLED "true") +set(DEFAULT_HIGHLIGHT_FILL_COLOR_R "FF") +set(DEFAULT_HIGHLIGHT_FILL_COLOR_G "FF") +set(DEFAULT_HIGHLIGHT_FILL_COLOR_B "FF") +set(DEFAULT_HIGHLIGHT_FILL_COLOR_A "40") +set(DEFAULT_HIGHLIGHT_FILL_OPACITY "25%") +set(DEFAULT_HIGHLIGHT_OUTLINE_SIZE 0) +set(DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R "00") +set(DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G "00") +set(DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B "FF") +set(DEFAULT_HIGHLIGHT_OUTLINE_COLOR_A "FF") +set(DEFAULT_HIGHLIGHT_OUTLINE_OPACITY "100%") +set(DEFAULT_HIGHLIGHT_CORNER_RADIUS 0) +set(DEFAULT_HIGHLIGHT_VPADDING 30) +set(DEFAULT_HIGHLIGHT_HPADDING 30) +set(DEFAULT_VCENTER "50%") +set(DEFAULT_SCROLL_INDICATORS "true") +set(DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R "FF") +set(DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G "FF") +set(DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B "FF") +set(DEFAULT_SCROLL_INDICATOR_FILL_COLOR_A "FF") +set(DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE 0) +set(DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R "00") +set(DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G "00") +set(DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B "00") +set(DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_A "FF") +set(DEFAULT_SCROLL_INDICATOR_OPACITY "100%") +set(DEFAULT_ON_LAUNCH "Blank") +set(DEFAULT_RESET_ON_BACK "false") +set(DEFAULT_INHIBIT_OS_SCREENSAVER "true") +set(DEFAULT_MOUSE_SELECT "false") +set(DEFAULT_CLOCK_ENABLED "false") +set(DEFAULT_CLOCK_SHOW_DATE "false") +set(DEFAULT_CLOCK_ALIGNMENT "Left") +set(DEFAULT_CLOCK_FONT "SourceSansPro-Regular.ttf") +set(DEFAULT_CLOCK_MARGIN "5%") +set(DEFAULT_CLOCK_FONT_COLOR_R "FF") +set(DEFAULT_CLOCK_FONT_COLOR_G "FF") +set(DEFAULT_CLOCK_FONT_COLOR_B "FF") +set(DEFAULT_CLOCK_FONT_COLOR_A "FF") +set(DEFAULT_CLOCK_SHADOWS "false") +set(DEFAULT_CLOCK_SHADOW_COLOR_R "00") +set(DEFAULT_CLOCK_SHADOW_COLOR_G "00") +set(DEFAULT_CLOCK_SHADOW_COLOR_B "00") +set(DEFAULT_CLOCK_SHADOW_COLOR_A "FF") +set(DEFAULT_CLOCK_OPACITY "100%") +set(DEFAULT_CLOCK_FONT_SIZE "50") +set(DEFAULT_CLOCK_TIME_FORMAT "Auto") +set(DEFAULT_CLOCK_DATE_FORMAT "Auto") +set(DEFAULT_CLOCK_INCLUDE_WEEKDAY "true") +set(DEFAULT_SCREENSAVER_ENABLED "false") +set(DEFAULT_SCREENSAVER_IDLE_TIME "300") +set(DEFAULT_SCREENSAVER_INTENSITY "70%") +set(DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW "true") +set(DEFAULT_GAMEPAD_ENABLED "false") +set(DEFAULT_GAMEPAD_DEVICE -1) diff --git a/config/flex-launcher.manifest.in b/config/flex-launcher.manifest.in new file mode 100644 index 0000000..88a49f8 --- /dev/null +++ b/config/flex-launcher.manifest.in @@ -0,0 +1,10 @@ + + + + + + UTF-8 + permonitor + + + diff --git a/src/launcher_config.h.in b/config/launcher_config.h.in similarity index 55% rename from src/launcher_config.h.in rename to config/launcher_config.h.in index 1083f33..5ee6bb2 100644 --- a/src/launcher_config.h.in +++ b/config/launcher_config.h.in @@ -1,12 +1,12 @@ // Project information +#define PROJECT_NAME "@CMAKE_PROJECT_NAME@" +#define EXECUTABLE_TITLE "@EXECUTABLE_TITLE@" +#define PROJECT_VERSION "@PROJECT_VERSION@" #define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ #define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@ #define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@ -#define PROJECT_NAME "@CMAKE_PROJECT_NAME@" -#define EXECUTABLE_TITLE "@EXECUTABLE_TITLE@" // Default filenames and paths -#define FILENAME_SCROLL_INDICATOR "scroll_indicator.svg" #define FILENAME_DEFAULT_CONFIG "config.ini" #define FILENAME_DEFAULT_FONT "OpenSans-Regular.ttf" #define FILENAME_DEFAULT_CLOCK_FONT "@DEFAULT_CLOCK_FONT@" @@ -33,6 +33,10 @@ // Config file setting names #define SETTING_DEFAULT_MENU "@SETTING_DEFAULT_MENU@" #define SETTING_MAX_BUTTONS "@SETTING_MAX_BUTTONS@" +#define SETTING_VSYNC "@SETTING_VSYNC@" +#define SETTING_FPS_LIMIT "@SETTING_FPS_LIMIT@" +#define SETTING_APPLICATION_TIMEOUT "@SETTING_APPLICATION_TIMEOUT@" +#define SETTING_WRAP_ENTRIES "@SETTING_WRAP_ENTRIES@" #define SETTING_BACKGROUND_MODE "@SETTING_BACKGROUND_MODE@" #define SETTING_BACKGROUND_IMAGE "@SETTING_BACKGROUND_IMAGE@" #define SETTING_BACKGROUND_COLOR "@SETTING_BACKGROUND_COLOR@" @@ -40,30 +44,49 @@ #define SETTING_SLIDESHOW_IMAGE_DURATION "@SETTING_SLIDESHOW_IMAGE_DURATION@" #define SETTING_SLIDESHOW_TRANSITION_TIME "@SETTING_SLIDESHOW_TRANSITION_TIME@" #define SETTING_SCREENSAVER_PAUSE_SLIDESHOW "@SETTING_SCREENSAVER_PAUSE_SLIDESHOW@" +#define SETTING_CHROMA_KEY_COLOR "@SETTING_CHROMA_KEY_COLOR@" +#define SETTING_BACKGROUND_OVERLAY "@SETTING_BACKGROUND_OVERLAY@" +#define SETTING_BACKGROUND_OVERLAY_COLOR "@SETTING_BACKGROUND_OVERLAY_COLOR@" +#define SETTING_BACKGROUND_OVERLAY_OPACITY "@SETTING_BACKGROUND_OVERLAY_OPACITY@" #define SETTING_ICON_SIZE "@SETTING_ICON_SIZE@" #define SETTING_ICON_SPACING "@SETTING_ICON_SPACING@" +#define SETTING_TITLES_ENABLED "@SETTING_TITLES_ENABLED@" #define SETTING_TITLE_FONT "@SETTING_TITLE_FONT@" #define SETTING_TITLE_FONT_SIZE "@SETTING_TITLE_FONT_SIZE@" -#define SETTING_TITLE_COLOR "@SETTING_TITLE_COLOR@" +#define SETTING_TITLE_FONT_COLOR "@SETTING_TITLE_FONT_COLOR@" +#define SETTING_TITLE_SHADOWS "@SETTING_TITLE_SHADOWS@" +#define SETTING_TITLE_SHADOW_COLOR "@SETTING_TITLE_SHADOW_COLOR@" #define SETTING_TITLE_OVERSIZE_MODE "@SETTING_TITLE_OVERSIZE_MODE@" #define SETTING_TITLE_OPACITY "@SETTING_TITLE_OPACITY@" #define SETTING_TITLE_PADDING "@SETTING_TITLE_PADDING@" -#define SETTING_HIGHLIGHT_COLOR "@SETTING_HIGHLIGHT_COLOR@" -#define SETTING_HIGHLIGHT_OPACITY "@SETTING_HIGHLIGHT_OPACITY@" +#define SETTING_HIGHLIGHT_ENABLED "@SETTING_HIGHLIGHT_ENABLED@" +#define SETTING_HIGHLIGHT_FILL_COLOR "@SETTING_HIGHLIGHT_FILL_COLOR@" +#define SETTING_HIGHLIGHT_FILL_OPACITY "@SETTING_HIGHLIGHT_FILL_OPACITY@" +#define SETTING_HIGHLIGHT_OUTLINE_COLOR "@SETTING_HIGHLIGHT_OUTLINE_COLOR@" +#define SETTING_HIGHLIGHT_OUTLINE_OPACITY "@SETTING_HIGHLIGHT_OUTLINE_OPACITY@" +#define SETTING_HIGHLIGHT_OUTLINE_SIZE "@SETTING_HIGHLIGHT_OUTLINE_SIZE@" #define SETTING_HIGHLIGHT_VPADDING "@SETTING_HIGHLIGHT_VPADDING@" #define SETTING_HIGHLIGHT_HPADDING "@SETTING_HIGHLIGHT_HPADDING@" #define SETTING_HIGHLIGHT_CORNER_RADIUS "@SETTING_HIGHLIGHT_CORNER_RADIUS@" -#define SETTING_BUTTON_CENTERLINE "@SETTING_BUTTON_CENTERLINE@" +#define SETTING_VCENTER "@SETTING_VCENTER@" #define SETTING_SCROLL_INDICATORS "@SETTING_SCROLL_INDICATORS@" -#define SETTING_SCROLL_INDICATOR_COLOR "@SETTING_SCROLL_INDICATOR_COLOR@" +#define SETTING_SCROLL_INDICATOR_FILL_COLOR "@SETTING_SCROLL_INDICATOR_FILL_COLOR@" +#define SETTING_SCROLL_INDICATOR_OUTLINE_SIZE "@SETTING_SCROLL_INDICATOR_OUTLINE_SIZE@" +#define SETTING_SCROLL_INDICATOR_OUTLINE_COLOR "@SETTING_SCROLL_INDICATOR_OUTLINE_COLOR@" #define SETTING_SCROLL_INDICATOR_OPACITY "@SETTING_SCROLL_INDICATOR_OPACITY@" #define SETTING_ON_LAUNCH "@SETTING_ON_LAUNCH@" #define SETTING_RESET_ON_BACK "@SETTING_RESET_ON_BACK@" +#define SETTING_MOUSE_SELECT "@SETTING_MOUSE_SELECT@" +#define SETTING_INHIBIT_OS_SCREENSAVER "@SETTING_INHIBIT_OS_SCREENSAVER@" +#define SETTING_STARTUP_CMD "@SETTING_STARTUP_CMD@" +#define SETTING_QUIT_CMD "@SETTING_QUIT_CMD@" #define SETTING_CLOCK_ENABLED "@SETTING_CLOCK_ENABLED@" #define SETTING_CLOCK_SHOW_DATE "@SETTING_CLOCK_SHOW_DATE@" #define SETTING_CLOCK_ALIGNMENT "@SETTING_CLOCK_ALIGNMENT@" #define SETTING_CLOCK_FONT "@SETTING_CLOCK_FONT@" -#define SETTING_CLOCK_COLOR "@SETTING_CLOCK_COLOR@" +#define SETTING_CLOCK_FONT_COLOR "@SETTING_CLOCK_FONT_COLOR@" +#define SETTING_CLOCK_SHADOWS "@SETTING_CLOCK_SHADOWS@" +#define SETTING_CLOCK_SHADOW_COLOR "@SETTING_CLOCK_SHADOW_COLOR@" #define SETTING_CLOCK_OPACITY "@SETTING_CLOCK_OPACITY@" #define SETTING_CLOCK_FONT_SIZE "@SETTING_CLOCK_FONT_SIZE@" #define SETTING_CLOCK_MARGIN "@SETTING_CLOCK_MARGIN@" @@ -104,42 +127,79 @@ // Config file default settings #define DEFAULT_MAX_BUTTONS @DEFAULT_MAX_BUTTONS@ +#define DEFAULT_VSYNC @DEFAULT_VSYNC@ +#define DEFAULT_APPLICATION_TIMEOUT @DEFAULT_APPLICATION_TIMEOUT@ +#define DEFAULT_WRAP_ENTRIES @DEFAULT_WRAP_ENTRIES@ #define DEFAULT_BACKGROUND_COLOR_R 0x@DEFAULT_BACKGROUND_COLOR_R@ #define DEFAULT_BACKGROUND_COLOR_G 0x@DEFAULT_BACKGROUND_COLOR_G@ #define DEFAULT_BACKGROUND_COLOR_B 0x@DEFAULT_BACKGROUND_COLOR_B@ #define DEFAULT_SLIDESHOW_IMAGE_DURATION @DEFAULT_SLIDESHOW_IMAGE_DURATION@ #define DEFAULT_SLIDESHOW_TRANSITION_TIME @DEFAULT_SLIDESHOW_TRANSITION_TIME@ +#define DEFAULT_CHROMA_KEY_COLOR_R 0x@DEFAULT_CHROMA_KEY_COLOR_R@ +#define DEFAULT_CHROMA_KEY_COLOR_G 0x@DEFAULT_CHROMA_KEY_COLOR_G@ +#define DEFAULT_CHROMA_KEY_COLOR_B 0x@DEFAULT_CHROMA_KEY_COLOR_B@ +#define DEFAULT_CHROMA_KEY_COLOR_A 0x@DEFAULT_CHROMA_KEY_COLOR_A@ +#define DEFAULT_BACKGROUND_OVERLAY @DEFAULT_BACKGROUND_OVERLAY@ +#define DEFAULT_BACKGROUND_OVERLAY_COLOR_R 0x@DEFAULT_BACKGROUND_OVERLAY_COLOR_R@ +#define DEFAULT_BACKGROUND_OVERLAY_COLOR_G 0x@DEFAULT_BACKGROUND_OVERLAY_COLOR_G@ +#define DEFAULT_BACKGROUND_OVERLAY_COLOR_B 0x@DEFAULT_BACKGROUND_OVERLAY_COLOR_B@ +#define DEFAULT_BACKGROUND_OVERLAY_COLOR_A 0x@DEFAULT_BACKGROUND_OVERLAY_COLOR_A@ +#define DEFAULT_BACKGROUND_OVERLAY_OPACITY "@DEFAULT_BACKGROUND_OVERLAY_OPACITY@" #define DEFAULT_ICON_SIZE @DEFAULT_ICON_SIZE@ #define DEFAULT_ICON_SPACING "@DEFAULT_ICON_SPACING@" #define DEFAULT_FONT_SIZE @DEFAULT_FONT_SIZE@ -#define DEFAULT_TITLE_COLOR_R 0x@DEFAULT_TITLE_COLOR_R@ -#define DEFAULT_TITLE_COLOR_G 0x@DEFAULT_TITLE_COLOR_G@ -#define DEFAULT_TITLE_COLOR_B 0x@DEFAULT_TITLE_COLOR_B@ -#define DEFAULT_TITLE_COLOR_A 0x@DEFAULT_TITLE_COLOR_A@ +#define DEFAULT_TITLES_ENABLED @DEFAULT_TITLES_ENABLED@ +#define DEFAULT_TITLE_FONT_COLOR_R 0x@DEFAULT_TITLE_FONT_COLOR_R@ +#define DEFAULT_TITLE_FONT_COLOR_G 0x@DEFAULT_TITLE_FONT_COLOR_G@ +#define DEFAULT_TITLE_FONT_COLOR_B 0x@DEFAULT_TITLE_FONT_COLOR_B@ +#define DEFAULT_TITLE_FONT_COLOR_A 0x@DEFAULT_TITLE_FONT_COLOR_A@ +#define DEFAULT_TITLE_SHADOWS @DEFAULT_TITLE_SHADOWS@ +#define DEFAULT_TITLE_SHADOW_COLOR_R 0x@DEFAULT_TITLE_SHADOW_COLOR_R@ +#define DEFAULT_TITLE_SHADOW_COLOR_G 0x@DEFAULT_TITLE_SHADOW_COLOR_G@ +#define DEFAULT_TITLE_SHADOW_COLOR_B 0x@DEFAULT_TITLE_SHADOW_COLOR_B@ +#define DEFAULT_TITLE_SHADOW_COLOR_A 0x@DEFAULT_TITLE_SHADOW_COLOR_A@ #define DEFAULT_TITLE_PADDING @DEFAULT_TITLE_PADDING@ -#define DEFAULT_HIGHLIGHT_COLOR_R 0x@DEFAULT_HIGHLIGHT_COLOR_R@ -#define DEFAULT_HIGHLIGHT_COLOR_G 0x@DEFAULT_HIGHLIGHT_COLOR_G@ -#define DEFAULT_HIGHLIGHT_COLOR_B 0x@DEFAULT_HIGHLIGHT_COLOR_B@ -#define DEFAULT_HIGHLIGHT_COLOR_A 0x@DEFAULT_HIGHLIGHT_COLOR_A@ +#define DEFAULT_HIGHLIGHT_ENABLED @DEFAULT_HIGHLIGHT_ENABLED@ +#define DEFAULT_HIGHLIGHT_FILL_COLOR_R 0x@DEFAULT_HIGHLIGHT_FILL_COLOR_R@ +#define DEFAULT_HIGHLIGHT_FILL_COLOR_G 0x@DEFAULT_HIGHLIGHT_FILL_COLOR_G@ +#define DEFAULT_HIGHLIGHT_FILL_COLOR_B 0x@DEFAULT_HIGHLIGHT_FILL_COLOR_B@ +#define DEFAULT_HIGHLIGHT_FILL_COLOR_A 0x@DEFAULT_HIGHLIGHT_FILL_COLOR_A@ +#define DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R 0x@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R@ +#define DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G 0x@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G@ +#define DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B 0x@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B@ +#define DEFAULT_HIGHLIGHT_OUTLINE_COLOR_A 0x@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_A@ +#define DEFAULT_HIGHLIGHT_OUTLINE_SIZE @DEFAULT_HIGHLIGHT_OUTLINE_SIZE@ #define DEFAULT_HIGHLIGHT_CORNER_RADIUS @DEFAULT_HIGHLIGHT_CORNER_RADIUS@ #define DEFAULT_HIGHLIGHT_VPADDING @DEFAULT_HIGHLIGHT_VPADDING@ #define DEFAULT_HIGHLIGHT_HPADDING @DEFAULT_HIGHLIGHT_HPADDING@ #define DEFAULT_SCROLL_INDICATORS @DEFAULT_SCROLL_INDICATORS@ -#define DEFAULT_SCROLL_INDICATOR_COLOR_R 0x@DEFAULT_SCROLL_INDICATOR_COLOR_R@ -#define DEFAULT_SCROLL_INDICATOR_COLOR_G 0x@DEFAULT_SCROLL_INDICATOR_COLOR_G@ -#define DEFAULT_SCROLL_INDICATOR_COLOR_B 0x@DEFAULT_SCROLL_INDICATOR_COLOR_B@ -#define DEFAULT_SCROLL_INDICATOR_COLOR_A 0x@DEFAULT_SCROLL_INDICATOR_COLOR_A@ -#define DEFAULT_BUTTON_CENTERLINE "@DEFAULT_BUTTON_CENTERLINE@" +#define DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R 0x@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R@ +#define DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G 0x@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G@ +#define DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B 0x@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B@ +#define DEFAULT_SCROLL_INDICATOR_FILL_COLOR_A 0x@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_A@ +#define DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE @DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE@ +#define DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R 0x@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R@ +#define DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G 0x@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G@ +#define DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B 0x@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B@ +#define DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_A 0x@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_A@ +#define DEFAULT_VCENTER "@DEFAULT_VCENTER@" #define DEFAULT_RESET_ON_BACK @DEFAULT_RESET_ON_BACK@ +#define DEFAULT_MOUSE_SELECT @DEFAULT_MOUSE_SELECT@ +#define DEFAULT_INHIBIT_OS_SCREENSAVER @DEFAULT_INHIBIT_OS_SCREENSAVER@ #define DEFAULT_CLOCK_ENABLED @DEFAULT_CLOCK_ENABLED@ #define DEFAULT_CLOCK_SHOW_DATE @DEFAULT_CLOCK_SHOW_DATE@ #define DEFAULT_CLOCK_FONT @DEFAULT_CLOCK_FONT@ #define DEFAULT_CLOCK_MARGIN "@DEFAULT_CLOCK_MARGIN@" -#define DEFAULT_CLOCK_COLOR_R 0x@DEFAULT_CLOCK_COLOR_R@ -#define DEFAULT_CLOCK_COLOR_G 0x@DEFAULT_CLOCK_COLOR_G@ -#define DEFAULT_CLOCK_COLOR_B 0x@DEFAULT_CLOCK_COLOR_B@ -#define DEFAULT_CLOCK_COLOR_A 0x@DEFAULT_CLOCK_COLOR_A@ +#define DEFAULT_CLOCK_FONT_COLOR_R 0x@DEFAULT_CLOCK_FONT_COLOR_R@ +#define DEFAULT_CLOCK_FONT_COLOR_G 0x@DEFAULT_CLOCK_FONT_COLOR_G@ +#define DEFAULT_CLOCK_FONT_COLOR_B 0x@DEFAULT_CLOCK_FONT_COLOR_B@ +#define DEFAULT_CLOCK_FONT_COLOR_A 0x@DEFAULT_CLOCK_FONT_COLOR_A@ #define DEFAULT_CLOCK_FONT_SIZE @DEFAULT_CLOCK_FONT_SIZE@ +#define DEFAULT_CLOCK_SHADOWS @DEFAULT_CLOCK_SHADOWS@ +#define DEFAULT_CLOCK_SHADOW_COLOR_R 0x@DEFAULT_CLOCK_SHADOW_COLOR_R@ +#define DEFAULT_CLOCK_SHADOW_COLOR_G 0x@DEFAULT_CLOCK_SHADOW_COLOR_G@ +#define DEFAULT_CLOCK_SHADOW_COLOR_B 0x@DEFAULT_CLOCK_SHADOW_COLOR_B@ +#define DEFAULT_CLOCK_SHADOW_COLOR_A 0x@DEFAULT_CLOCK_SHADOW_COLOR_A@ #define DEFAULT_CLOCK_INCLUDE_WEEKDAY @DEFAULT_CLOCK_INCLUDE_WEEKDAY@ #define DEFAULT_CLOCK_ALIGNMENT ALIGNMENT_LEFT #define DEFAULT_CLOCK_TIME_FORMAT FORMAT_TIME_AUTO diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..68d388c --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,27 @@ +source "https://rubygems.org" + +#gem "jekyll", "~> 4.2.2" +gem 'jekyll-seo-tag' +gem "jekyll-remote-theme" +gem "github-pages", group: :jekyll_plugins + +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.12" +end + +# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem +# and associated library. +platforms :mingw, :x64_mingw, :mswin, :jruby do + gem "tzinfo", "~> 1.2" + gem "tzinfo-data" +end + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] + +# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem +# do not have a Java counterpart. +gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] + +gem "webrick", "~> 1.7" diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..6013668 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,15 @@ +title: Flex Launcher +description: A customizable HTPC application launcher and front-end interface +url: "" # the base hostname & protocol for your site, e.g. http://example.com +github_username: complexlogic +repository: complexlogic/flex-launcher +launcher_version: 2.2 + +# Build settings +remote_theme: pages-themes/slate@v0.2.0 +plugins: + - jekyll-feed + - jekyll-remote-theme + - jekyll-seo-tag + + diff --git a/docs/_data/menu.yml b/docs/_data/menu.yml new file mode 100644 index 0000000..21dc110 --- /dev/null +++ b/docs/_data/menu.yml @@ -0,0 +1,8 @@ +- title: Home + link: /flex-launcher +- title: Download + link: /flex-launcher/download +- title: Configuration + link: /flex-launcher/configuration +- title: Setup Guide + link: /flex-launcher/setup diff --git a/docs/_includes/head-custom-google-analytics.html b/docs/_includes/head-custom-google-analytics.html new file mode 100644 index 0000000..fde10e6 --- /dev/null +++ b/docs/_includes/head-custom-google-analytics.html @@ -0,0 +1,9 @@ + + + diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 0000000..1ed7ec5 --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,67 @@ + + + + + + + + + + + {% seo %} + {% include head-custom.html %} + + + + + +
    +
    + {% if site.github.is_project_page %} + View on GitHub + {% endif %} + +

    {{ site.title | default: site.github.repository_name }}

    +

    {{ site.description | default: site.github.project_tagline }}

    + + {% if site.show_downloads %} +
    + Download this project as a .zip file + Download this project as a tar.gz file +
    + {% endif %} +
    +
    + + +
    +
    + {{ content }} +
    +
    + + + + + + diff --git a/docs/_sass/jekyll-theme-slate.scss b/docs/_sass/jekyll-theme-slate.scss new file mode 100644 index 0000000..3096280 --- /dev/null +++ b/docs/_sass/jekyll-theme-slate.scss @@ -0,0 +1,528 @@ +@import "rouge-github"; + +/******************************************************************************* +MeyerWeb Reset +*******************************************************************************/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font: inherit; + vertical-align: baseline; +} + +sup { + font-size: 65%; + vertical-align: super; + font-weight: bold; +} + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +ol, ul { + list-style: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +/******************************************************************************* +Theme Styles +*******************************************************************************/ + +body { + box-sizing: border-box; + color:#373737; + background: #212121; + font-size: 16px; + font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +h1, h2, h3, h4, h5, h6 { + margin: 10px 0; + font-weight: 700; + color:#222222; + font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; + letter-spacing: -1px; +} + +h1 { + font-size: 36px; + font-weight: 700; +} + +h2 { + padding-bottom: 10px; + font-size: 32px; + background: url('../images/bg_hr.png') repeat-x bottom; +} + +h3 { + font-size: 24px; +} + +h4 { + font-size: 21px; +} + +h5 { + font-size: 18px; +} + +h6 { + font-size: 16px; +} + +nav ul { + display: flex; + list-style: none; + padding-left: 0px; + margin-top: 5px; + margin-bottom: 0px; +} + +nav ul li { + margin-right: 5px; + font-size: 20px; +} + +p { + margin: 10px 0 15px 0; +} + +footer p { + color: #f2f2f2; +} + +a { + text-decoration: none; + color: #0F79D0; + text-shadow: none; + + transition: color 0.5s ease; + transition: text-shadow 0.5s ease; + -webkit-transition: color 0.5s ease; + -webkit-transition: text-shadow 0.5s ease; + -moz-transition: color 0.5s ease; + -moz-transition: text-shadow 0.5s ease; + -o-transition: color 0.5s ease; + -o-transition: text-shadow 0.5s ease; + -ms-transition: color 0.5s ease; + -ms-transition: text-shadow 0.5s ease; +} + +a:hover, a:focus { + text-decoration: underline; +} + +footer a { + color: #F2F2F2; + text-decoration: underline; +} + +em, cite { + font-style: italic; +} + +strong { + font-weight: bold; +} + +img { + position: relative; + margin: 0 auto; + max-width: 800px; + padding: 5px; + margin: 10px 0 10px 0; + border: 1px solid #ebebeb; + + box-shadow: 0 0 5px #ebebeb; + -webkit-box-shadow: 0 0 5px #ebebeb; + -moz-box-shadow: 0 0 5px #ebebeb; + -o-box-shadow: 0 0 5px #ebebeb; + -ms-box-shadow: 0 0 5px #ebebeb; +} + +p img { + display: inline; + margin: 0; + padding: 0; + vertical-align: middle; + text-align: center; + border: none; +} + +pre, code { + color: #222; + background-color: #fff; + + font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; + font-size: 0.875em; + + border-radius: 2px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; +} + +pre { + padding: 10px; + box-shadow: 0 0 10px rgba(0,0,0,.1); + overflow: auto; +} + +code { + padding: 3px; + margin: 0 3px; + box-shadow: 0 0 10px rgba(0,0,0,.1); +} + +pre code { + display: block; + box-shadow: none; +} + +blockquote { + color: #666; + margin-bottom: 20px; + padding: 0 0 0 20px; + border-left: 3px solid #bbb; +} + + +ul, ol, dl { + margin-bottom: 15px +} + +ul { + list-style-position: inside; + list-style: disc; + padding-left: 20px; +} + +ol { + list-style-position: inside; + list-style: decimal; + padding-left: 20px; +} + +dl dt { + font-weight: bold; +} + +dl dd { + padding-left: 20px; + font-style: italic; +} + +dl p { + padding-left: 20px; + font-style: italic; +} + +hr { + height: 1px; + margin-bottom: 5px; + border: none; + background: url('../images/bg_hr.png') repeat-x center; +} + +table { + border: 1px solid #373737; + margin-bottom: 20px; + text-align: left; + } + +th { + font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif; + padding: 10px; + background: #373737; + color: #fff; + } + +td { + padding: 10px; + border: 1px solid #373737; + } + +form { + background: #f2f2f2; + padding: 20px; +} + +kbd { + background-color: #fafbfc; + border: 1px solid #c6cbd1; + border-bottom-color: #959da5; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #959da5; + color: #444d56; + display: inline-block; + font-size: 11px; + line-height: 11px; + padding: 3px 5px; + vertical-align: middle; +} + +/******************************************************************************* +Full-Width Styles +*******************************************************************************/ + +.outer { + width: 100%; +} + +.inner { + position: relative; + max-width: 800px; + padding: 20px 10px; + margin: 0 auto; +} + +.download { + border: none; + vertical-align: top; + box-shadow: none; + padding-bottom: 20px; +} + +.menu_link { + font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; +} + +.menu_no_link { + color: #ffffff; +} + +.menu_link_active { + font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; + color: #ffffff; +} + +#forkme_banner { + display: block; + position: absolute; + top:0; + right: 10px; + z-index: 10; + padding: 10px 50px 10px 10px; + color: #fff; + background: url('../images/blacktocat.png') #0090ff no-repeat 95% 50%; + font-weight: 700; + box-shadow: 0 0 10px rgba(0,0,0,.5); + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +#header_wrap { + background: #212121; + background: -moz-linear-gradient(top, #373737, #212121); + background: -webkit-linear-gradient(top, #373737, #212121); + background: -ms-linear-gradient(top, #373737, #212121); + background: -o-linear-gradient(top, #373737, #212121); + background: linear-gradient(to top, #373737, #212121); +} + +#header_wrap .inner { + padding: 40px 10px 5px 10px; +} + +#project_title { + margin: 0; + color: #fff; + font-size: 42px; + font-weight: 700; + text-shadow: #111 0px 0px 10px; +} + +#project_tagline { + color: #fff; + font-size: 24px; + font-weight: 300; + background: none; + text-shadow: #111 0px 0px 10px; +} + +#downloads { + position: absolute; + width: 210px; + z-index: 10; + bottom: -40px; + right: 0; + height: 70px; + background: url('../images/icon_download.png') no-repeat 0% 90%; +} + +.zip_download_link { + display: block; + float: right; + width: 90px; + height:70px; + text-indent: -5000px; + overflow: hidden; + background: url(../images/sprite_download.png) no-repeat bottom left; +} + +.tar_download_link { + display: block; + float: right; + width: 90px; + height:70px; + text-indent: -5000px; + overflow: hidden; + background: url(../images/sprite_download.png) no-repeat bottom right; + margin-left: 10px; +} + +.zip_download_link:hover { + background: url(../images/sprite_download.png) no-repeat top left; +} + +.tar_download_link:hover { + background: url(../images/sprite_download.png) no-repeat top right; +} + +#main_content_wrap { + background: #f2f2f2; + border-top: 1px solid #111; + border-bottom: 1px solid #111; +} + +#main_content { + padding-top: 40px; +} + +#footer_wrap { + background: #212121; +} + + + +/******************************************************************************* +Small Device Styles +*******************************************************************************/ + +@media screen and (max-width: 992px) { + img { + max-width: 100%; + } +} + +@media screen and (max-width: 480px) { + body { + font-size:14px; + } + + #downloads { + display: none; + } + + .inner { + min-width: 320px; + max-width: 480px; + } + + #project_title { + font-size: 32px; + } + + h1 { + font-size: 28px; + } + + h2 { + font-size: 24px; + } + + h3 { + font-size: 21px; + } + + h4 { + font-size: 18px; + } + + h5 { + font-size: 14px; + } + + h6 { + font-size: 12px; + } + + code, pre { + font-size: 11px; + } + +} + +@media screen and (max-width: 320px) { + body { + font-size:14px; + } + + #downloads { + display: none; + } + + .inner { + min-width: 240px; + max-width: 320px; + } + + #project_title { + font-size: 28px; + } + + h1 { + font-size: 24px; + } + + h2 { + font-size: 21px; + } + + h3 { + font-size: 18px; + } + + h4 { + font-size: 16px; + } + + h5 { + font-size: 14px; + } + + h6 { + font-size: 12px; + } + + code, pre { + min-width: 240px; + max-width: 320px; + font-size: 11px; + } + +} + diff --git a/docs/assets/icons/favicon.png b/docs/assets/icons/favicon.png new file mode 100644 index 0000000..526daf5 Binary files /dev/null and b/docs/assets/icons/favicon.png differ diff --git a/docs/assets/icons/linux.svg b/docs/assets/icons/linux.svg new file mode 100644 index 0000000..dce080a --- /dev/null +++ b/docs/assets/icons/linux.svg @@ -0,0 +1,368 @@ + + + + + Tux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/icons/raspberry_pi.svg b/docs/assets/icons/raspberry_pi.svg new file mode 100644 index 0000000..27192be --- /dev/null +++ b/docs/assets/icons/raspberry_pi.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/assets/icons/source.svg b/docs/assets/icons/source.svg new file mode 100644 index 0000000..8cdf492 --- /dev/null +++ b/docs/assets/icons/source.svg @@ -0,0 +1,7 @@ + + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/docs/assets/icons/windows.svg b/docs/assets/icons/windows.svg new file mode 100644 index 0000000..6fac0df --- /dev/null +++ b/docs/assets/icons/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extra/screenshots/screenshot1.png b/docs/assets/screenshots/screenshot1.png old mode 100755 new mode 100644 similarity index 100% rename from extra/screenshots/screenshot1.png rename to docs/assets/screenshots/screenshot1.png diff --git a/extra/screenshots/screenshot2.png b/docs/assets/screenshots/screenshot2.png old mode 100755 new mode 100644 similarity index 100% rename from extra/screenshots/screenshot2.png rename to docs/assets/screenshots/screenshot2.png diff --git a/docs/compilation.md b/docs/compilation.md new file mode 100644 index 0000000..38d5fb0 --- /dev/null +++ b/docs/compilation.md @@ -0,0 +1,92 @@ +--- +layout: default +title: Compilation Guide +--- +# Compilation Guide +## Table of Contents +1. [Overview](#overview) +2. [Linux](#linux) +3. [Windows](#windows) + +## Overview + Flex Launcher builds natively on Linux and Windows, and features a cross-platform CMake build system. The following external dependencies are required: + - SDL ≥ 2.0.14 + - SDL_image ≥ 2.0.5 + - SDL_ttf ≥ 2.0.15 + +## Linux +Flex Launcher on Linux builds with GCC. This guide assumes you already have the development tools Git, CMake, pkg-config, and GCC installed on your system. If not, consult your distro's documentation. + +First, install the dependencies. The steps to do so are dependent on your distro: + +#### APT-based Distributions (Debian, Ubuntu, Mint, Raspberry Pi OS etc.) +```bash +sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libinih-dev +``` + +#### Pacman-based Distributions (Arch, Manjaro, etc.) +```bash +sudo pacman -S sdl2 sdl2_image sdl2_ttf libinih +``` + +#### DNF-based Distributions (Fedora) +```bash +sudo dnf install SDL2-devel SDL2_image-devel SDL2_ttf-devel inih-devel +``` + +### Building +Clone the master repo and create a build directory: +```bash +git clone https://github.com/complexlogic/flex-launcher.git +cd flex-launcher +mkdir build && cd build +``` +Generate the Makefile: +```bash +cmake .. -DCMAKE_BUILD_TYPE=Release +``` +If you're building on Raspberry Pi, it's recommended to pass `-DRPI=1` to cmake, which tweaks the default configuration to be more Pi-centric. + +Build and test the program: +```bash +make +./flex-launcher +``` +Optionally, install it into your system directories: +```bash +sudo make install +``` +By default, this will install the program and assets with a prefix of `/usr/local`. If you wish to use a different prefix, re-run the cmake generation step with `-DCMAKE_INSTALL_PREFIX=prefix`. + +## Windows +Flex Launcher on Windows builds with Visual Studio, and uses [vcpkg](https://vcpkg.io/en/index.html) to manage the dependencies. Before starting, make sure the following steps are completed: +- Visual Studio is installed. The free Community Edition is available for download from Microsoft's website. The following tools and features for Visual Studio are required: + - C++ core desktop features + - Latest MSVC + - Latest Windows SDK + - C++ CMake tools +- Git is installed and in your `Path` environment variable +- CMake is installed and in your `Path` environment variable + +### Building +Clone the master repo and create a build directory: +``` +git clone https://github.com/complexlogic/flex-launcher.git +cd flex-launcher +mkdir build +cd build +``` +Build the dependencies and generate the Visual Studio project files: +``` +git clone https://github.com/microsoft/vcpkg +cmake .. -DCMAKE_TOOLCHAIN_FILE=".\vcpkg\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET="x64-windows-static" +``` +Build and test the program: +``` +cmake --build . +.\Debug\flex-launcher.exe +``` +Optionally, generate a clean zipped install package which may then be extracted to a directory of your choosing: +``` +cmake --build . --config Release --target package +``` diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..1da2a67 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,644 @@ +--- +layout: default +title: Configuration +--- +# Configuring Flex Launcher +## Table of Contents + +1. [Overview](#overview) +2. [Settings](#settings) + - [General](#general) + - [Background](#background) + - [Layout](#layout) + - [Titles](#titles) + - [Highlight](#highlight) + - [Scroll Indicators](#scroll-indicators) +3. [Creating Menus](#creating-menus) + - [Special Commands](#special-commands) + - [Desktop Files (Linux Only)](#desktop-files-linux-only) +4. [Clock](#clock) +5. [Screensaver](#screensaver) +6. [Hotkeys](#hotkeys) +7. [Gamepad Controls](#gamepad-controls) +8. [Transparent Backgrounds](#transparent-backgrounds) + +## Overview +Flex Launcher uses an [INI file](https://en.wikipedia.org/wiki/INI_file) to configure settings and menus. The INI file consists of sections enclosed in square brackets, and in each section there are entries which consist of a key and a value. Example: +```ini +[Section] +Key1=value +Key2=value +... +``` +A line can be commented out by using the # character at the beginning of the line, which will cause the line to be ignored by the program. In-line comments are not allowable. Here are a few things to note about the configuration settings for Flex Launcher: +- All keys and values are case sensitive. +- Full UTF-8 character set is supported for titles. +- The following image formats are supported: JPEG, PNG, and WebP +- Relative paths are evaluated with respect to the *current working directory*, which may not be the same as the directory that the config file is located in. It is recommended to use absolute paths whenever possible to eliminate any confusion. +- Color is specified in 24 bit RGB HEX format prefixed with the # character, e.g. the color red should be `#FF0000`. The letters can be uppercase or lowercase. HEX color pickers can be easily found online to assist color choices. +- Several settings allow for values to be specified in pixels *or* as a percentage of another value. In this case, if no percent sign is detected it will be interpreted as pixels, and if the percent sign is present, than it will be interpreted as a percent value e.g. "5" means 5 pixels and "5%" means 5 percent. +- Shell variable expansion is generally not supported, e.g. you cannot use the ~ character to refer to your home directory. The exception is for commands, since those are passed through to your system shell. + +## Settings +The following sections contain settings that control the look and behavior of the launcher: +- [General](#general) +- [Background](#background) +- [Layout](#layout) +- [Titles](#titles) +- [Highlight](#highlight) +- [Scroll Indicators](#scroll-indicators) + +#### General +The settings in this section control the general behavior of the launcher. + +- [DefaultMenu](#defaultmenu) +- [VSync](#vsync) +- [FPSLimit](#fpslimit) +- [OnLaunch](#onlaunch) +- [ResetOnBack](#resetonback) +- [MouseSelect](#mouseselect) +- [InhibitOSScreensaver](#inhibitosscreensaver) +- [StartupCmd](#startupcmd) +- [QuitCmd](#quitcmd) + +##### DefaultMenu +This is the title of the main menu that shows when Flex Launcher is started. The value *must* match the name of one of your menu sections, or there will be an error and Flex Launcher will refuse to start. See the [Creating Menus](#creating-menus) section for more information. + +##### VSync +Defines whether VSync will be used to synchronize the frame rate with the refresh rate of your monitor. This setting is a boolean "true" or "false" + +Default: true + +##### FPSLimit +When `VSync` is set to false, this setting defines the maximum number of frames per second that Flex Launcher will render. The minimum is 10, and the maximum is the same as the refresh rate of your monitor. + +##### ApplicationTimeout +Defines the time in seconds that the launcher will wait for an application to launch. If the launcher does not lose the window focus before the timeout occurs, it assumes there was an error with the launched application. + +Default: 15 + +##### OnLaunch +Defines the action that Flex Launcher will take upon the launch of an application. Possible values: "None", "Blank", and "Quit" +- None: Flex Launcher will maintain its window while waiting for the launched application to initialize. +- Blank: Flex Launcher will change to a blank, black screen while waiting for the launched application to initialize. +- Quit: Flex Launcher will quit immediately after the successful launch of an application. + +Default: Blank + +##### WrapEntries +Defines whether the highlight will wrap to the other side of the screen after reaching its leftmost or rightmost position. This setting is a boolean "true" or "false". + +Default: false + +##### ResetOnBack +Defines whether Flex Launcher will remember the previous entry position when going back to a previous menu. If set to true, the highlight will be reset to the first entry in the menu when going back. This setting is a boolean "true" or "false". + +Default: false + +##### MouseSelect +Defines whether the left mouse button can be used to select the highlighted entry. This setting is intended to support gyroscopic mouse devices where the enter/ok button functions as a mouse left click instead of the keyboard enter button. This setting is a boolean "true" or "false". + +Default: false + +##### InhibitOSScreensaver +Defines whether Flex Launcher will prevent your default OS screensaver from activating while it is running. On Windows, this will also inhibit any power saving features as well (e.g. autosleep). This setting is a boolean "true" or "false". + +Default: true + +##### StartupCmd +Defines a command that Flex Launcher will execute immediately upon startup. This can be used to autostart your favorite application. + +##### QuitCmd +Defines a command that Flex Launcher will execute immediately before quitting. This can be used to do any mode switching or appplication starting to prepare your desktop, e.g. for maintenance. + +#### Background +The settings in this section control what Flex Launcher will display in the background. + +- [Mode](#mode) +- [Color](#color) +- [Image](#image) +- [SlideshowDirectory](#slideshowdirectory) +- [SlideshowImageDuration](#slideshowimageduration) +- [SlideshowTransitionTime](#slideshowtransitiontime) +- [ChromaKeyColor](#chromakeycolor) +- [Overlay](#overlay) +- [OverlayColor](#overlaycolor) +- [OverlayOpacity](#overlayopacity) + +##### Mode +Defines what mode the background will be. Possible values: "Color", "Image", and "Slideshow" +- Color: The background will be a solid color. +- Image: The background will be an image. +- Slideshow: The background will be a series of images displayed in random order, with a fading transition between each image. +- Transparent: The background will be transparent. This is an advanced feature; users should read the [Transparent Backgrounds](#transparent-backgrounds) section before proceeding. + +Default: Color + +##### Color +When `Mode` is set to "Color", this setting defines the color of the background. + +Default: #000000 (Black) + +##### Image +When `Mode` is set to "Image", this setting defines the image to be displayed in the background. The value should be a path to an image file. If the image is not the same resolution as your desktop, it will be stretched accordingly. + +##### SlideshowDirectory +When `Mode` is set to "Slideshow", this setting defines the directory (folder) which contains the images to display in the background. The value should be a path to a directory on your filesystem. The number of images that may be scanned from the directory is limited to 250. + +##### SlideshowImageDuration +When `Mode` is set to "Slideshow", this setting defines the amount of time in seconds to display each image. Must be an integer value. + +Default: 30 + +##### SlideshowTransitionTime +When `Mode` is set to "Slideshow", this setting defines the amount of time in seconds that the next background image will fade in. The fading transition may be disabled by setting this to 0, which will yield a "hard" transition between images. Decimal values are acceptable. + +Default: 3 + +##### ChromaKeyColor +When `Mode` is set to "Transparent", this setting defines the color that will be applied to the background for chroma key transparency. + +Default: #010101 + +##### Overlay +Defines whether the background overlay feature is enabled. The background overlay is a solid color, typically black, that is painted over your background to improve the contrast between the background and the text/icons. This setting is a boolean "true" or "false". + +Default: false + +##### OverlayColor +Defines the color of the background overlay. + +Default: #000000 (Black) + +##### OverlayOpacity +Defines the opacity of the background overlay. Must be a percent value. + +Default: 50% + +#### Layout +The settings in this section define the geometric layout of the launcher. + +- [MaxButtons](#maxbuttons) +- [IconSize](#iconsize) +- [IconSpacing](#iconspacing) +- [VCenter](#vcenter) + +##### MaxButtons +The maximum number of buttons that can be displayed on the screen. If a menu has more entries than this value, it will be split into multiple pages. A value of 3-5 is sensible for a typical TV size and viewing distance. + +Default: 4 + +##### IconSize +The width and height of icons on the screen in pixels. If an icon is not the same resolution, it will be stretched accordingly. + +Default: 256 + +##### IconSpacing +Distance between the menu entry icons, in pixels or percent of the screen width. + +Default: 5% + +##### VCenter +Defines the vertical centering of the menu entries in percent of the screen height. A value of 50% will cause the buttons to be centered halfway in the screen. Increasing the value will lower the buttons, and lowering it will raise them. + +Default: 50% + +#### Titles +The settings in this section affect the application titles that display below the icons. + +- [Enabled](#enabled) +- [Font](#font) +- [FontSize](#fontsize) +- [Color](#color-1) +- [Shadows](#shadows) +- [ShadowColor](#shadowcolor) +- [Opacity](#opacity) +- [OversizeMode](#oversizemode) +- [Padding](#padding) + +##### Enabled +Defines whether or not application titles are enabled. This setting is a boolean "true" or "false". + +Default: true + +##### Font +Defines the font to use for the titles of the menu entries. The value should be the path to a TrueType (TTF) font file. Non-TTF font formats are not supported. Flex Launcher ships with a handful of libre fonts. + +Default: OpenSans + +##### FontSize +Defines the font size of each menu entry title. + +Default: 36 + +##### Color +Defines the color of the menu entry titles. + +Default: #FFFFFF (White) + +##### Shadows +Defines whether shadows are enabled for the menu titles. Shadows give a 3D textured appearance to the text to improve the contrast from the background. This setting is a boolean "true" or "false". + +Default: false + +##### ShadowColor +Defines the color of the title shadows. + +Default: #000000 (Black) + +##### Opacity +Defines the opacity of the menu entry titles. Must be a percent value. + +Default: 100% + +##### OversizeMode +Defines the behavior when the width of a menu entry title exceeds the width of its icon (which is defined in `IconSize`). Possible values: "Truncate", "Shrink", and "None" +- Truncate: Truncates the title at the maximum width and adds "..." to the end. +- Shrink: Shrinks oversized titles to a smaller font size than `TitleFontSize` so that the entire title fits within the maximum width. +- None: No action is taken to limit the width of titles. Overlaps with other titles may occur, and it is the user's responsibility to manually handle any such case. + +Default: Truncate + +##### Padding +Defines the vertical spacing between an icon and its title, in pixels. + +Default: 20 + +#### Highlight +The settings in this section control the menu highlight. + +- [Enabled](#enabled-1) +- [FillColor](#fillcolor) +- [FillOpacity](#fillopacity) +- [OutlineSize](#outlinesize) +- [OutlineColor](#outlinecolor) +- [OutlineOpacity](#outlineopacity) +- [CornerRadius](#cornerradius) +- [VPadding](#vpadding) +- [HPadding](#hpadding) + +##### Enabled +Defines whether or not the highlight is enabled. If a user disables the highlight, it is assumed that they will be using the [Sected Icon Overrides](#selected-icon-overrides) feature instead. This setting is a boolean "true" or "false". + +Default: true + +##### FillColor +Defines the fill color of the highlight cursor. + +Default: #FFFFFF (White) + +##### FillOpacity +Defines the fill opacity of the highlight cursor. Must be a percent value. + +Default: 25% + +##### OutlineSize +Defines the stroke width in pixels of the outline of the highlight cursor. Setting this to 0 will disable the outline. + +Default: 0 + +##### OutlineColor +Defines the outline color of the highlight cursor. + +Default: #0000FF (Blue) + +##### OutlineOpacity +Defines the outline opacity of the highlight cursor. Must be a percent value. + +Default: 100% + +##### CornerRadius +Defines the corner radius of the highlight cursor, in pixels. A value of 0 will yield a plain rectangle. Increasing the value will yield a rounded rectangle with increasingly round corners. The value of `HighlightOutlineSize` must be 0, otherwise this setting will be ignored. + +Default: 0 + +##### VPadding +Defines the amount of vertical distance that the highlight cursor extends beyond the top and bottom of the menu entry icon, in pixels. + +Default: 30 + +##### HPadding +Defines the amount of horizontal distance that the highlight cursor extends beyond the left and right of the menu entry icon, in pixels. + +Default: 30 + +#### Scroll Indicators +The settings in this section pertain to scroll indicators. Scroll indicators are arrows which appear in the bottom left and/or bottom right of the screen to inform the user that there are additional pages of applications to scroll to. + +- [Enabled](#enabled-2) +- [FillColor](#fillcolor-1) +- [OutlineSize](#outlinesize-1) +- [OutlineColor](#outlinecolor-1) +- [Opacity](#opacity-1) + +##### Enabled +Defines whether scroll indicators will be enabled in the event that a menu has multiple pages of entries. This setting is a boolean "true" or "false". + +Default: true + +##### OutlineSize +Defines the stroke width in pixels of the scroll indicator outline. Setting this to 0 will disable the outline. + +Default: 0 + +##### FillColor +Defines the fill color of the scroll indicators. + +Default: #FFFFFF (White) + +##### OutlineColor +Defines the color of the scroll indicator outline. + +Default: #000000 (Black) + +##### Opacity +Defines the opacity of the scroll indicators. Must be a percent value. + +Default: 100% + +## Creating Menus +At least one menu must be defined in the configuration file, and the title must match the `DefaultMenu` setting value. The title of a menu is its section name. Any title may be used that is not reserved for another section, such as "Settings", "Gamepad", etc. The entries of the menu are implemented as key=value pairs. The name of the key will be ignored by the program, and is therefore arbtrary. However, it is recommended to pick something intutitive such as Entry1, Entry2, Entry3, etc. The entry information is contained in the value. + +Each entry value contains 3 parts of information in order: the title, the icon image path, and the command to run when the button is clicked. These are delimited by semicolons: +```ini +Entry=title;icon_path;command +``` +The command is typically one of the following: +1. The path to the program executable that you want to launch +2. Windows: the path to a program shortcut (.lnk file) +3. Linux: the path to a [.desktop file](#desktop-files-linux-only) +4. A [special command](#special-commands) +5. The path to an executable script, in the case that you want to perform multiple actions upon program launch. + + A simple example menu titled `Media` is shown below: +```ini +[Media] +Entry1=Kodi;C:\Pictures\Icons\kodi.png;"C:\Program Shortcuts\kodi.lnk" +Entry2=Netflix;C:\Pictures\Icons\netflix.png;"C:\Program Shortcuts\netflix.lnk" +Entry3=Plex;C:\Pictures\Icons\plex.png;"C:\Program Shortcuts\plex.lnk" +Entry4=Back;C:\Pictures\Icons\back.png;:back +``` + +### Selected Icon Overrides +The Selected Icon Override feature allows the user to define a different icon for the launcher to display when an entry is highlighted. To use this feature, name the path of the selected icon the same as the default entry icon path, but with a suffix of `_selected` (not including the file extension). + +For example, if the icon path for an entry is defined as `C:\icons\kodi.png`, then the program will check for the existence of `C:\icons\kodi_selected.png` and, if it exists, this icon will be shown when the entry is selected instead of the default. This feature allows the user to implement custom highlight effects such as glowing, color changes, etc. + +### Special Commands +Special commands are commands that are internal to Flex Launcher and begin with a colon. The following is a list of special commands: + +#### :submenu +Change to a different menu. Requires a menu title as an argument. For example, the command `:submenu Games` will change to the menu `Games`. The argument must be a valid menu title that is defined elsewhere in the config file. + +#### :fork +Forks a new process and executes a command in it without exiting the launcher. This is typically used in combination with a [hotkey](#hotkeys). Use this special command when you want to execute a command on your system for some reason other than launching a graphical application. Example use cases: +- Change a Wi-Fi connection +- Pair or connect a Bluetooth device +- Start or stop some system service/daemon + +The :fork special command requires a command as an argument. For example `:fork command arguments` will execute `command arguments` without leaving the launcher. + +Windows users should invoke a command line interpreter such as Command Prompt and pass the command to run as an argument, e.g. `:fork cmd.exe /c "command arguments"` + +#### :exit +Windows only. Quits the currently running application. This special command is only available as a hotkey command. See the [Exit Hotkey](#exit-hotkey-windows-only) section for more information. + +#### :back +Go back to the previous menu. + +#### :home +Change to the menu defined in the `DefaultMenu` setting. + +#### :quit +Quit Flex Launcher. + +#### :left +Move the highlight cursor left. + +#### :right +Move the highlight cursor right. + +#### :select +Press enter on the current selection. This special command is only available as a gamepad or hotkey command, it is forbidden for menu entries. + +#### :shutdown +Shut down the computer.1 + +#### :restart +Restart the computer.1 + +#### :sleep +Put the computer to sleep.1 + +1 *Linux: Works in systemd-based distros only. Non-systemd distro users need to implement the command manually for their init system.* + +### Desktop Files (Linux Only) +If the application you want to launch was installed via your distro's package manager, a .desktop file was most likely provided. The command to launch a Linux application can simply be the path to its .desktop file, and Flex Launcher will run the Exec command that the developers have specified in the file. Desktop files are located in /usr/share/applications. + +#### Desktop Actions +Some .desktop files contain "Actions", which affect how the program is launched. An action may be specified by delimiting it from the path to the .desktop file with a semicolon. For example, Steam has a mode called "Big Picture Mode", which provides an interface similar to a game console and is ideal for a living room PC. The action in the .desktop file is called "BigPicture". A sample menu entry to launch Steam in Big Picture mode is shown below: +``` +Entry1=Steam;/path/to/steamicon.png;/usr/share/applications/steam.desktop;BigPicture +``` + +## Clock +Flex Launcher contains a clock widget, which displays the current time, and, optionally, the current date. The following settings may be used to control the behavior of the clock. + +#### Enabled +Defines whether or not the clock is enabled. This setting is a boolean "true" or "false". + +Default: false + +#### ShowDate +Defines whether or not the current date should be shown in addition to the current time. This setting is a boolean "true" or "false". + +Default: false + +#### Alignment +Defines which side of the screen the clock text should align to. Possible values: "Left" and "Right" + +Default: Left + +#### Font +Defines the font to use for the clock text. The value should be the path to a TrueType (TTF) font file. + +Default: SourceSansPro + +#### FontSize +Defines the font size of the clock text + +Default: 50 + +#### Margin +Defines the distance of the clock text from the top and side of the screen, in pixels or percent of the screen height. + +Default: 5% + +#### FontColor +Defines the color of the clock text. + +Default: #FFFFFF (White) + +#### Shadows +Defines whether shadows are enabled for the clock text. Shadows give a 3D textured appearance to the text to improve the contrast from the background. This setting is a boolean "true" or "false". + +Default: false + +#### ShadowColor +Defines the color of the clock text shadows. + +Default: #000000 (Black) + +#### Opacity +Defines the opacity of the clock text. Must be a percent value. + +Default: 100% + +#### TimeFormat +Defines the format of the current time. Possible values: "24hr", "12hr", and "Auto" +- 24hr: The clock will be a 24 hour format. +- 12hr: The clock will be a 12 hour format, including AM/PM designation in your locale. +- Auto: Automatically determine the time format based on your system locale. + +Default: Auto + +#### DateFormat +Defines the order of the month and day in the date. Possible values: "Little", "Big", "Auto" +- Little: The day will come before the month. +- Big: The month will come before the day. +- Auto: Automatically determine the date format based on your system locale. + +#### IncludeWeekday +Defines whether the date format should include the abbreviated weekday in your system locale. This setting is a boolean "true" or "false" + +Default: true + +## Screensaver +Flex Launcher contains a screensaver feature, which will dim the screen after the input has been idle for the specified amount of time. Here are the settings that control the behavior of the screensaver + +#### Enabled +Defines whether or not the screensaver is enabled. This setting is a boolean "true" or "false". + +Default: false + +#### IdleTime +Defines the amount of time in seconds that the input should be idle before activating the screensaver + +Default: 300 (5 minutes) + +#### Intensity +Defines the amount to dim the screen. Must be a percent value. + +Default: 70% + +#### PauseSlideshow +When `BackgroundMode` is set to "Slideshow", this setting defines whether or not the slideshow should be paused while the screensaver is active. This setting is a boolean "true" or "false". + +Default: true + +## Hotkeys +Flex Launcher supports configurable hotkeys, which executes a command when a specified key is pressed. Each hotkey consists of a key=value pair, where the key is an arbitrary name, and the value contains the SDL keycode of the hotkey and the command to run when it is pressed, delimited by a semicolon: +```ini +Hotkey=keycode;command +``` +The keycode is a HEX prefixed with the # character. There are two ways to find a keycode for a given key. The first is to use the [lookup table provided by SDL](https://wiki.libsdl.org/SDLKeycodeLookup). The name of each key is in the right column of the table, and the corresponding HEX keycode is in the center column. The second is to run Flex Launcher in debug mode, press the key, then check the log. For each keystroke, the name of the key will be printed and the HEX value will be in parenthesis next to it. + +Any key can be set as a hotkey, except keys that are reserved for the default controls: the left and right arrow keys, enter/return, and backspace. Hotkeys may be used to "speed dial" your favorite applications, or to add controls via [special commands](#special-commands). As an example configuration below, the first hotkey is mapped to F1 and will launch Kodi when it is pressed, and the second hotkey is mapped to F12 and will cause Flex Launcher to quit when it is pressed: +```ini +[Hotkeys] +Hotkey1=#4000003A;"C:\Program Shortcuts\kodi.lnk" +Hotkey2=#40000045;:quit +``` + +### Exit Hotkey (Windows only) +The exit hotkey feature allows a user to quit the running application using a button on their remote. This is especially useful for applications that don't have a quit button, such as a web browser operating in fullscreen mode. + +Only the function keys F1-F24 may be used as an exit hotkey, with the exception of F12 which is forbidden by Windows. Pressing an exit hotkey is functionally equivalent to using the Alt+F4 keyboard shortcut on the active window; it is not a forceful method, so the application is able to close cleanly. However, the application could also choose to ignore it, display a confirmation dialog, or not respond if it's hung. + +The following example maps F10 as an exit hotkey: +```ini +Hotkey=#40000043;:exit +``` +Linux users that desire similar functionality should check the documentation of their desktop environment and/or window manager. Most support global hotkeys that can be configured to close the active window. + +## Gamepad Controls +Flex Launcher has built-in support for gamepad controls through SDL. All settings for gamepads will be in a section titled `Gamepad`. Within the section, there are key=value pairs which define the gamepad settings and the commands to be run when a button or axis is pressed. + +### Settings +The following settings are available in the `Gamepad` section to define the behavior of gamepads + +#### Enabled +Defines whether or not gamepad controls are enabled. This setting is a boolean "true" or "false". + +Default: false + +#### DeviceIndex +Defines the device index of the gamepad in SDL. If this value is negative, any gamepad may be used to control the launcher. + +Default: -1 + +#### ControllerMappingsFile +A path to a text file that contains 1 or more controller mappings to override the default. This is usually not necessary, but if you want to change the mapping for your controller, or there is no default mapping for your controller in SDL, it can be specified via this interface. A community database of mappings for many common controllers can be found [here](https://github.com/gabomdq/SDL_GameControllerDB). Alternatively, you may create a custom mapping using a GUI tool such as the [SDL2 Gamepad Tool](https://generalarcade.com/gamepadtool/). + +### Controls +The controls are defined in key=value pairs, where the key is the name of the axis or button that is pressed, and the value is the command that is to be run, which is typically a [special command](#special-commands). An axis is an analog stick or a trigger. For analog sticks, negative (-) represents left for the x axis and up for the y axis, and postive (+) represents right for the x axis and down for the y axis. + +The [SDL GameController](https://wiki.libsdl.org/CategoryGameController) interface is an abstraction which conceptualizes a controller as having an Xbox-style layout. The mapping names in SDL are based on the *location* of the buttons on an Xbox controller, and may not correspond to the actual labelling of the buttons on your controller. For example, `ButtonA` is for the "bottom" button, `ButtonB` is for the "right" button of the 4 main control buttons. If you have a Playstation-style controller, those mapping names will correspond to the X button and the Circle button, respectively. + +The default controls in Flex Launcher allow the user to move the highlight cursor left and right by using the left stick or the DPad, select an entry by pressing A, and go back to the previous menu by pressing B. These controls are simple and will suffice for the vast majority of use cases. + +The following axis and buttons are available for control in Flex Launcher: +- LStickX- +- LStickX+ +- LStickY- +- LStickY+ +- RStickX- +- RStickX+ +- RStickY- +- RStickY+ +- LTrigger +- RTrigger +- ButtonA +- ButtonB +- ButtonX +- ButtonY +- ButtonBack +- ButtonGuide +- ButtonStart +- ButtonLeftStick +- ButtonRightStick +- ButtonLeftShoulder +- ButtonRightShoulder +- ButtonDPadUp +- ButtonDPadDown +- ButtonDPadLeft +- ButtonDPadRight + +## Transparent Backgrounds +*Note for Linux users only: this feature requires compositor implementation. See the [Linux Setup Guide](https://complexlogic.github.io/flex-launcher/setup_linux#transparent-backgrounds) for details.* + +Flex Launcher supports transparent backgrounds using the chroma key technique. This method works by setting a strategically chosen color to the background, which is removed later. In film production, this technique is often refered to as "blue screening" or "green screening". + +The chosen chroma key color should be one that is not found in your icons/text, as this would cause them to become transparent. The color is set with the `ChromaKeyColor` setting in the Background section. The default is `#010101`, a slight off-shade of black. + +A limitation of this method is that your icons and text must be fully opqaue or fully transparent. Any semi-transparent pixels will blend with the chroma key background so that it does not produce a color match, and consequently will not be removed from the background. + +Flex Launcher's text rendering is anti-aliased, which gives the text a "feathered" look with semi-transparent pixels on the edges. Normally, this is desirable, but in the case of chroma keying, semi-transparent pixels will cause your chroma key background color to "bleed through" on the edges of the text. If bright blue or bright green is chosen as the chroma key, this will result in a blue or green glowing effect around the text, which is usually undesirable. This is the reason why a dark color is chosen as the default chroma key. The bleed though appears as a dark outline rather than as a bright glowing. + +Some icons have shadows which are intended to provide a textured look. The shadows are usually semi-transparent, which will cause them to not render properly with the chroma key technique. You should choose icons without shadows, or manually erase the shadows from an icon if there are no other icons available for the given application. Some icons are also heavily anti-aliased, which can give a glowing or outline effect similar to the text rendering described above. + +Another common issue is the highlight. The default highlight is semi-transparent, which will not render properly when blended with the chroma key background. There are a few ways to address with this: +- Set the `FillOpacity` setting to 0%, and use an outline-only highlight instead +- Use custom [Selected Icons](#selected-icon-overrides) in place of Flex Launcher's highlight +- Linux only: use a shader to recover the highlight's transparency. See the [Linux Setup Guide](https://complexlogic.github.io/flex-launcher/setup_linux#transparent-backgrounds) for details. + +Transparent backgrounds will require some effort to obtain a setup that looks good and works well. Be prepared to do a significant amount of tinkering if you wish to use this feature. + +### Windows Implenetation +The Windows implementation of transparency is not hardware accelerated. If your refresh rate is very high, this can result in a signficant load on the CPU. If you find that the trasparent background is causing a high load on your system, consider changing the `VSync` setting to false and setting `FPSLimit` to 30 or lower, which will reduce the amount of computation required. + +### Animated Backgrounds +Transparent backgrounds can be used to implement animated backgrounds in combination with another program. On Windows, [Wallpaper Engine](https://www.wallpaperengine.io) and [Lively](https://github.com/rocksdanister/lively) are popular choices. For Linux, I recommend [anipaper](https://github.com/Theldus/anipaper); see the [Linux Setup Guide](https://complexlogic.github.io/flex-launcher/setup_linux#animated-backgrounds) for details. + +### Custom Widgets +Flex Launcher offers a simple clock widget which can show the current time and date. For more advanced functionality, you can combine a transparent background with a third party widget program. For example, you can have a widget that displays weather, news, etc. in addition to the time. [Rainmeter](https://www.rainmeter.net/) is a popular option on Windows, and [Conky](https://github.com/brndnmtthws/conky) for Linux. diff --git a/docs/download.md b/docs/download.md new file mode 100644 index 0000000..f0d4fc8 --- /dev/null +++ b/docs/download.md @@ -0,0 +1,64 @@ +--- +layout: default +title: Download +--- + +## License +Flex Launcher is released under the [Unlicense](https://unlicense.org/), i.e. public domain. + +## Download +Binary packages are available for Windows 64 bit, Linux x86-64, and Raspberry Pi. The latest stable release is version {{site.launcher_version}}. + + + + + + + + + + + + + + + + + +
    +

    Windows 10/11:

    + +
    +

    APT-based (Debian, Ubuntu):

    + +

    Pacman-based (Arch, Manjaro):

    + +
    +

    Raspberry Pi (64 bit only)

    + +
    + + +## Source Code +If your platform is not listed above, you're interested in development, or you simply prefer compiling your own software, you can download source packages below. See the [compilation guide](compilation) for build instructions. + + + + + + + + +
    +

    Source Packages:

    + +
    + + diff --git a/extra/flex-launcher.png b/docs/flex-launcher.png old mode 100755 new mode 100644 similarity index 100% rename from extra/flex-launcher.png rename to docs/flex-launcher.png diff --git a/extra/flex-launcher.svg b/docs/flex-launcher.svg old mode 100755 new mode 100644 similarity index 100% rename from extra/flex-launcher.svg rename to docs/flex-launcher.svg diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..aa17d82 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,58 @@ +--- +layout: default +--- + +Flex Launcher is a customizable application launcher and front end designed with a TV-friendly [10 foot user interface](https://en.wikipedia.org/wiki/10-foot_user_interface), intending to mimic the look and feel of a streaming box or game console. Flex Launcher allows you to launch applications on your HTPC or couch gaming PC entirely by use of a TV remote or a gamepad. No keyboard or mouse required! + +Flex Launcher is compatible with both Windows and Linux (including Raspberry Pi devices). + +## Screenshots +

    +

    + +## About +Flex Launcher was initially released in late 2021. Its creation was motivated by the puzzling lack of good HTPC control options at the time. Flex Launcher is designed with the following principles in mind: +- **Flexibility:** Indicated in the name *Flex* Launcher, the configuration should be highly flexible, with user having a great deal of control over the appearance and behavior of the program. +- **Simplicity:** The interface should be simple, yet beautiful. +- **Portability:** Many of the other existing solutions only support a single platform. Flex Launcher aims to be portable and cross-platform, running on ARM-based SBCs such as Raspberry Pi, to high-end Windows PCs, and everything in between. +- **Ease of Navigation:** Flex Launcher should be completely navigable using only directional keys, enter, and back. The launcher should include built-in support for gamepad devices. + +Flex Launcher is completely free and open source. Accordingly, you are entitled to modify or redsitribute it as you wish. The source code is available on [GitHub](https://github.com/complexlogic/flex-launcher). + +## Features +The following is a list of features supported by Flex Launcher: + +#### Custom Backgrounds +The background of the Flex Launcher is customizable. There are three supported modes for the background: a solid color, an image, or a slideshow of images. In the case of the slideshow, the user is able to specify how long to show the images, and how quickly to fade between them. + +#### Application Icons +The user is able to pick the application icons, specify how large they should be, and how far apart they should be. The user is also able to control the vertical centering of the application icons. + +#### Submenus +Flex Launcher supports submenus, which allows you to group your various applications in an organized fashion. For example, you can have separate menus for media and gaming applications. + +#### Selection Highlighting +The user is able to change the size, color, opacity, and corner radius of the default highlight rectangle, as well as the border. + +Additionally, Flex Launcher also supports an icon-based selection mode where the user can specify a different icon that will display when a menu entry is selected. This allows the user to implement custom highlighting effects. + +#### Fonts +The font, size, color, and opacity of all texts are customizable. Flex Launcher ships with a few royalty free fonts, but the user is able to substitute their own font instead. + +#### Hotkeys +With the hotkeys feature, you can map custom commands to a button on your remote. This can be used to “speed dial” your favorite applications, add new controls, or exit a currently running application. + +#### System Controls +There are built-in menu options to shut down, restart, or put your PC to sleep. + +#### Gamepad Controls +Flex Launcher has full support for gamepad controls, allowing easy menu navigation. Enable the controls via the configuration file and connect your gamepad. The user can also add a custom mapping for a given gamepad. + +#### Screensaver +Flex Launcher includes a screensaver mode that will dim the screen after it has been idle. The user is able to adjust how long the idle time should be, and how much to dim the screen. + +#### Clock +There is an available clock widget that displays the current time and, optionally, the current date. The user is able to adjust the size, font, placement, and time/date format. + +## Support +If you believe you have found a bug in Flex Launcher, open an issue on the [GitHub issue tracker](https://github.com/complexlogic/flex-launcher/issues). For general technical support or feature requests, please use the [Discussions page](https://github.com/complexlogic/flex-launcher/discussions) instead. diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..6172668 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,87 @@ +--- +layout: default +title: Setup Guide +--- +# Setup Guide +## Table of Contents +1. [Overview](#overview) +2. [Selecting Menu Icons](#selecting-menu-icons) +3. [Maintaining Controls](#maintaining-contrast) +4. [Launching a Web Browser](#launching-a-web-browser) +5. [Watching YouTube](#watching-youtube) +6. [Directly Launching Steam Games](#directly-launching-steam-games) + +## Overview +This page contains tips for setting up Flex Launcher, and HTPCs in general. The recommendations herein are broadly applicable to all platforms supported by Flex Launcher. Additionally, see the platform setup guides for platform-specfic advice: +- [Windows Setup Guide](https://complexlogic.github.io/flex-launcher/setup_windows) +- [Linux Setup Guide](https://complexlogic.github.io/flex-launcher/setup_linux) + +Make that you are generally familiar with the [configuration options](https://complexlogic.github.io/flex-launcher/configuration) as well. + +## Selecting Menu Icons +Transparency is essential for menu icons. Therefore, you should not use JPEG images for icons, since the JPEG format does not support transparency. Use PNG or WebP instead. PNG icons for most popular applications are easily found online in common sizes up to 256x256. + +Any icon that is not the same resolution as the `IconSize` setting in your config file will be stretched. If your `IconSize` setting is not a common icon resolution (e.g. 256), then it is advisable to find SVG icons instead. However, Flex Launcher does not currently support SVGs for menu icons, so you will need to rasterize them into PNG or WebP using a tool such as [Inkscape](https://inkscape.org/). An example command can quickly rasterize an SVG into your desired resolution: +```bash +inkscape --export-width= --export-type=png /path/to/file.svg +``` +You can easily write a script to rasterize all SVGs in a directory to PNG at a given resolution. Here is an example in Python: +```python +import glob +import subprocess +WIDTH=300 # Width of the PNG in pixels + +svg_files = glob.glob("*.svg") +for file in svg_files: + subprocess.run(['inkscape', f'--export-width={WIDTH}', '--export-type=png', file]) +``` + +## Maintaining Contrast +When using an image as the background, it is often difficult to read the text that is displayed on top. This is particularly true if the image is a photograph and the text is white. Flex Launcher has several features that will improve the contrast between the background and the objects on top. + +The background overlay feature draws a solid color, typically black, over the background. This will darken the background to improve the contrast ratio. The user can adjust how much to darken the background with the `OverlayOpacity` setting. + +Text shadows will give displayed text a textured, 3 dimensional appearance, which helps it stand out from the background. + +The highlight and scroll indicators each have an outline setting. The user can choose how thick and which color the outline should be, to improve the contrast with the background. + +## Launching a Web Browser +My recommended web browser for HTPC use is Chrome/Chromium. This browser has many command line launch options which make it more flexible to configure than Firefox and its derivatives. Some launch options that have particular relevance to HTPC use: +- `--start-fullscreen`: This starts the browser in a fullscreen mode. However, do note that the address bar will be hidden, so make sure to include the URL of the website you want to launch as an argument. +- `--force-device-scale-factor=n`: This can be used to make web pages rendered larger for viewing from a distance. For example, try, 1.1 or 1.2 as `n`. +- `--user-agent`: Sets a custom HTML user-agent string. This is necessary for [watching YouTube](#watching-youtube). + +## Watching YouTube +There is currently no desktop application for YouTube. However, there is a TV-friendly web interface located at [youtube.com/tv](https://www.youtube.com/tv) that is intended for use by Smart TVs . Google recently blocked access to this interface for desktop web browsers, but the block can be easily circumvented by spoofing the user-agent string of a Smart TV. A list of valid Smart TV user-agent strings is easily found online by search engine. The following example menu entries will launch an app-like YouTube experience in a browser: + +**Windows:** +```ini +Entry=YouTube;C:\icons\youtube.png;"C:\Program Files\Google\Chrome\Application\chrome.exe" --start-fullscreen --user-agent="Mozilla/5.0 (Linux; Tizen 2.3; SmartHub; SMART-TV; SmartTV; U; Maple2012) AppleWebKit/538.1+ (KHTML, like Gecko) TV Safari/538.1+" youtube.com/tv +``` + +**Linux:** +```ini +Entry=YouTube;/path/to/icons/youtube.png;chromium --start-fullscreen --user-agent="Mozilla/5.0 (Linux; Tizen 2.3; SmartHub; SMART-TV; SmartTV; U; Maple2012) AppleWebKit/538.1+ (KHTML, like Gecko) TV Safari/538.1+" youtube.com/tv +``` + +This method is far superior to other HTPC YouTube options, such as Kodi's YouTube add-on. You can install a browser extension such as [uBlock Origin](https://ublockorigin.com/) to prevent ads from being shown before videos. + +The web interface also supports casting videos from the YouTube app on your smartphone to your TV. You can pair your phone in the settings. You can also sign into your YouTube account in the settings if you wish. + +### Exiting +The one caveat to this method is that the exit button in the menu doesn't work. As such, you will need to provide an alternative method to close the web browser after you've finished watching so you can return back to the launcher. For Windows users, the most straightforward solution is to configure an [exit hotkey](https://complexlogic.github.io/flex-launcher/configuration#exit-hotkey-windows-only) on your remote. Linux users should set up a hotkey with their DE/WM to close the active window. + +## Directly Launching Steam Games +Steam users may desire to launch their most frequently played games directly from Flex Launcher to avoid having to navigate through the Steam client UI first. Valve provides a [protocol](https://developer.valvesoftware.com/wiki/Steam_browser_protocol) to directly launch games, among other actions. To do so, pass `steam://run/` as an argument to Steam, where `` is replaced by the id of the game you want to watch. You can find the id of a game by searching [steamdb](https://steamdb.info/). For example, the id of Portal 2 is 620. You would structure your menu entry to launch Portal 2 like so: + +**Windows:** +```ini +Entry=Portal 2;C:\icons\portal_2.png;"C:\Program Files (x86)\Steam\steam.exe" steam://run/620 +``` + +**Linux:** +```ini +Entry=Portal 2;/path/to/icons/portal_2.png;steam steam://run/620 +``` + +Make sure you have autologin configured in Steam, otherwise you will be prompted for your password before the game launches. diff --git a/docs/setup_linux.md b/docs/setup_linux.md new file mode 100644 index 0000000..d348a0a --- /dev/null +++ b/docs/setup_linux.md @@ -0,0 +1,440 @@ +--- +layout: default +title: Linux Setup Guide +--- +# Linux Setup Guide +## Table of Contents +1. [Overview](#overview) +2. [Autostarting](#autostarting) +3. [Choosing a Distro](#choosing-a-distro) +4. [Desktop Enviornment](#desktop-environment) +5. [Display Protocol](#display-protocol) +6. [Transparent Backgrounds](#transparent-backgrounds) +7. [Kiosk Mode Setup](#kiosk-mode-setup) +8. [HTPC as Audio Receiver](#htpc-as-audio-receiver) +9. [Using an IR Remote](#using-an-ir-remote) + +## Overview +This page contains tips for setting up Flex Launcher on Linux-based systems, as well as general HTPC setup tips. + +## Autostarting +In a typical HTPC setup, Flex Launcher is autostarted after boot. On Linux, this can be accomplished in multiple ways. The most widely implemented is [XDG Autostart](https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html). Application .desktop files in `~/.config/autostart` will be autostarted upon user login. The .desktop file for Flex Launcher is installed to `/usr/share/applications`. Copy it to your autostart directory: +```bash +mkdir -p ~/.config/autostart +cp /usr/share/applications/flex-launcher.desktop ~/.config/autostart +``` +Additionally, some desktop enviornments have their own, separate autostart protocol. Consult your DE's documentation for more details. + +## Choosing A Distro +My preferred distribution for a Linux HTPC is [Arch](https://archlinux.org/). It has a minimal base installation and very up-to-date packages, which make it well suited for HTPC use. + +If you dislike Arch's rolling release model, another good choice is [Debian](https://www.debian.org/). When installing, make sure to deselect a desktop environment so you can have a minimal base install, similar to Arch. + +## Desktop Environment +I recommend [Openbox](http://openbox.org/wiki/Main_Page), which is not a full-fledged desktop enviornment, but rather a standalone window manager. An HTPC is a very simple device, and most of the features of desktop environments are not needed and add unnecessary bloat. Since HTPC applications are always in fullscreen, not even compositing is necessary. + +Openbox is lightweight and highly customizable. The basic install of Openbox provides only a black root window, over which Flex Launcher and your desired applications can be drawn. + +## Display Protocol +Being based on SDL, Flex Launcher has support for both X11 and Wayland display protocols. I recommend using X11. The benefits that Wayland offers aren't broadly applicable to an HTPC, and Wayland support is still lacking in many areas. + +If you choose to run Flex Launcher in Wayland, it is strongly recommended to use it with the latest stable release of SDL, as later versions have seen significantly improved support. + +## Transparent Backgrounds +Transparent backgrounds requires compositor support. I recommend [picom](https://github.com/yshui/picom), which supports transparency via GLSL shaders. The method described below requires version 10 or later which, as of this writing, is not yet packaged for most Linux distros. If this is this case for your distro, you will need to build it from source yourself. + +The picom option `--window-shader-fg` can be used to specify a custom GLSL shader to apply to the windows. The below shader program can be used as a starting point to implement transparency with Flex Launcher. The macros should be changed to match the values in your Flex Launcher config file, if necessary. + +```glsl +#version 330 + +// Set this to 1 to restore a semi-transparent highlight +#define RESTORE_HIGHLIGHT 1 + +// Set this to 1 to use a background overlay to darken the video +#define BACKGROUND_OVERLAY 0 + +#define BACKGROUND_OVERLAY_R 0x00 +#define BACKGROUND_OVERLAY_G 0x00 +#define BACKGROUND_OVERLAY_B 0x00 +#define BACKGROUND_OVERLAY_OPACITY 0.25 + +// Replace with the values from your Flex Launcher config +#define CHROMA_R 0x01 +#define CHROMA_G 0x01 +#define CHROMA_B 0x01 +#define HIGHLIGHT_R 0xFF +#define HIGHLIGHT_G 0xFF +#define HIGHLIGHT_B 0xFF +#define HIGHLIGHT_OPACITY 0.25 + +#define BLEND(src, dst) vec4( \ + src.x * src.w + (dst.x * (1.0 - src.w)), \ + src.y * src.w + (dst.y * (1.0 - src.w)), \ + src.z * src.w + (dst.z * (1.0 - src.w)), \ + src.w + (dst.w * (1 - src.w)) \ +) + +in vec2 texcoord; +uniform sampler2D tex; +uniform vec4 chroma_key = vec4(float(CHROMA_R) / 255.0, float(CHROMA_G) / 255.0, float(CHROMA_B) / 255.0, 1.0); +uniform vec4 highlight_color = vec4(float(HIGHLIGHT_R) / 255.0, float(HIGHLIGHT_G) / 255.0, float(HIGHLIGHT_B) / 255.0, float(int(HIGHLIGHT_OPACITY * 255.0)) / 255.0); +#if BACKGROUND_OVERLAY +uniform vec4 overlay_color = vec4(float(BACKGROUND_OVERLAY_R) / 255.0, float(BACKGROUND_OVERLAY_G) / 255.0, float(BACKGROUND_OVERLAY_B) / 255.0, float(int(BACKGROUND_OVERLAY_OPACITY * 255.0)) / 255.0); +#endif + +vec4 window_shader() +{ + vec4 c = texelFetch(tex, ivec2(texcoord), 0); + + // Remove background + vec4 vdiff = abs(chroma_key - c); + float diff = max(max(max(vdiff.r, vdiff.g), vdiff.b), vdiff.a); + if (diff < 0.0001) +#if BACKGROUND_OVERLAY + c = overlay_color; +#else + c.w = 0.0; +#endif + + // Restore highlight +#if RESTORE_HIGHLIGHT + else { + vec4 blend = BLEND(highlight_color, chroma_key); + vdiff = abs(blend - c); + diff = max(max(max(vdiff.r, vdiff.g), vdiff.b), vdiff.a); + if (diff < 0.01) { +#if BACKGROUND_OVERLAY + c = BLEND(highlight_color, overlay_color); +#else + c = highlight_color; + c *= c.w; +#endif + } + } +#endif + + return c; +} + +``` +Save the shader program to a .glsl file in a directory of your choice. + +Since you will typically not want to run the compositor while your launched applications are running, you will need to start and stop it frequently. This is best accomplished with a systemd service. Create a new user unit file: +```bash +nano /etc/systemd/user/picom-transparent.service +``` +Paste the following into the file: +```ini +[Unit] +Description=X11 compositor with alpha transparency + +[Service] +ExecStart=/usr/bin/picom --backend glx --force-win-blend --window-shader-fg=/path/to/shader.glsl + +[Install] +WantedBy=multi-user.target +``` +Replace `` with the appropriate path, then save the file. The compositor can be enabled/disabled with: +```bash +systemctl --user start picom-transparency.service +systemctl --user stop picom-transparency.service +``` +Use shell scripts to launch your applications and make sure to stop the compositor prior to launch, and start it again after the application has completed. + +### Animated Backgrounds +For an animated transparent background implementation, I recommend [anipaper](https://github.com/Theldus/anipaper), which will play a video of your choice in a loop. anipaper should be run with the following options enabled: +- `-b` (Borderless Fullscreen): Since this method requires a compositor, we will need to run anipaper as a borderless fullscreen window rather than as a wallpaper +- `-p` (Pause): We will need to pause the playback when the application launches, and resume it after it finishes +- `-d` (Hardware decoding): This will keep the CPU use as low as possible, and consequently fan noise and power consumption. Requires the hardware device name as an argument. This option is not absolutely required, but you should use it if your hardware is supported. + +In your autostart setup, make sure that anipaper starts *before* Flex Launcher. This ensures that Flex Launcher will have the window focus. + +#### Pausing anipaper +When the `-p` option is enabled, anipaper can be paused when applications launch, and resumed when they finish. This is necessary to prevent anipaper from unecessarily consuming resources when the video is not visible. Use the following command in your scripts to pause and resume anipaper +```bash +pkill -SIGUSR1 anipaper +``` + +#### Example Script +To handle the advanced functionality of starting and stopping various services when applications are launched, you will need to use a shell script. Here is a simple example: +```bash +# Pre-launch +systemctl --user stop picom-transparent.service +pkill -SIGUSR1 anipaper + +# Launch application +if [[ "$1" == "kodi" ]]; then + ... +elif [[ "$1" == "youtube" ]]; then + ... +fi + +# Post-launch +systemctl --user start picom-transparent.service +pkill -SIGUSR1 anipaper +``` +The above example script takes an argument which determines which application to launch. The script executes pre and post launch commands which are common to all applications, as well as commands that are specific to the particular application being launched. In your Flex Launcher config, your menu entry command should be the path to the script with the application you want to launch as the first argument. + +## Kiosk Mode Setup +"Kiosk Mode" typically refers to a user interface which resembles an embedded-style device that performs only a single function (as opposed to a multitasking PC with a desktop interface). This section contains instructions to set up my interpretation of a "Kiosk Mode" interface for a Linux HTPC. The design is based on my recommendations in the previous sections, consisting of Xorg, Openbox, and Flex Launcher. + +### Prerequisites +Before starting I assume that you have one of the following operating systems installed *without* a desktop evironnment (only a console login): +- Arch Linux +- Debian Bookworm or later +- Raspberry Pi OS Lite + +I also assume that you can use the Linux command line at an intermediate level or higher. + +### First Steps +The first thing to do is configure a network connection and SSH server. On Raspberry Pi, both of those tasks are easily accomplished with the `raspi-config` utility. Otherwise, the exact steps will vary based on which operating system you are on (Arch/Debian) and what backends you choose. Refer to the [Arch Wiki](https://wiki.archlinux.org/) and [Debian Wiki](https://wiki.debian.org/) for more detailed help. + +It is recommended to set up a static IP on your LAN. Since you will be doing most of your setup and maintenance remotely via SSH, you will want to have a consistent login. + +You will also need to install a terminal-based text editor, since we don't have any graphical interface yet. I recommend nano, and that is what will be used in subsequent sections. You can use a different text editor if you wish, just make sure to substitute commands where appropriate. + +### Set Up Autologin +Configure the TTY console to log you in automatically without user or password prompt. On Raspberry Pi, you can use `raspi-config`, or you can also follow the instructions for Arch/Debian below which will also work. + +Create a systemd drop-in file for the getty TTY1 service: +``` +sudo nano /etc/systemd/system/getty@tty1.service.d/autologin.conf +``` +Paste the following into the file: + +```ini +[Service] +ExecStart= +ExecStart=-/sbin/agetty --autologin --noclear %I $TERM +``` +Replace `` with your username, then save the file. Reboot your HTPC and verify that it logs you into the console automatically. + +### Install Packages +Install the necessary packages. + +**Arch:** +```bash +sudo pacman -S xorg xorg-xinit openbox unclutter pulseaudio wget +``` +**Debian/Raspberry Pi:** +```bash +sudo apt install xorg openbox unclutter-xfixes pulseaudio wget +``` +Then, install Flex Launcher according to the instructions on the [README](https://github.com/complexlogic/flex-launcher#linux). Also, make sure to [copy the assets to your home directory](https://github.com/complexlogic/flex-launcher#copying-assets-to-home-directory). + +### Configure Xorg +Configure X to start after user login with `.bash_profile` and `startx`: +```bash +nano ~/.bash_profile +``` +Paste the following into the file and save it: +```bash +if [ "x${SSH_TTY}" = "x" ]; then + startx +fi +``` +The `.bash_profile` script will execute every time the user logs in, *including remote logins via SSH*. The purpose of the `if` block is to ensure that `startx` will only be executed during a local login, not remote logins via SSH. + +The `startx` program will look for an `xinitrc` script to run: first in the user's home directory, then in the system directory. We will define a basic user `xinitrc` script to configure X and start Openbox: +```bash +nano ~/.xinitrc +``` +Paste the following into the file: +```bash +#!/bin/sh +userresources=$HOME/.Xresources +usermodmap=$HOME/.Xmodmap +sysresources=/etc/X11/xinit/.Xresources +sysmodmap=/etc/X11/xinit/.Xmodmap + +# merge in defaults and keymaps +if [ -f $sysresources ]; then + xrdb -merge $sysresources +fi + +if [ -f $sysmodmap ]; then + xmodmap $sysmodmap +fi + +if [ -f "$userresources" ]; then + xrdb -merge "$userresources" +fi + +if [ -f "$usermodmap" ]; then + xmodmap "$usermodmap" +fi + +# Startup scripts +if [ -d /etc/X11/xinit/xinitrc.d ] ; then + for f in /etc/X11/xinit/xinitrc.d/?*.sh ; do + [ -x "$f" ] && . "$f" + done + unset f +fi + +unclutter --start-hidden & +exec openbox-session +``` +You can make additions to this script if you wish. For example, the screen resolution can be forced using the `xrandr` utility. However, note that the `exec openbox-session` line of the script will never return. Therefore, any additions you make must come *before* this line. + +### Configure Openbox +Openbox ships with default configuration files installed to `/etc/xdg/openbox`. These files should be copied to your home directory: +```bash +mkdir -p ~/.config/openbox +cp -a /etc/xdg/openbox/ ~/.config/ +``` +Among these configuration files is `autostart`, which Openbox will execute after initialization. This is the best way to autostart Flex Launcher: +```bash +nano ~/.config/openbox/autostart +``` +Add `flex-launcher` to the file, then save it. + +#### Application Menu +You can access a basic application menu by right clicking anywhere on Openbox's root window. The menu entries are populated from `~/.config/openbox/menu.xml`. On Arch, this is a static menu that was pre-populated with programs, most of which you won't have installed. See the [Openbox Wiki](http://openbox.org/wiki/Help:Menus#Static_menus) for editing instructions. On Debian/Raspberry Pi, this is a dynamic menu that is automatically populated from your installed .desktop files, so you shouldn't need to edit anything. + +You can also install taskbars and various other graphical interfaces for Openbox, however, this is not recommended. The default interface is clean and the right-click menu provides enough basic functionality for maintenance of your HTPC. + +#### Keybinds +[Keybinds](http://openbox.org/wiki/Help:Bindings) can be set in the `rc.xml` file, which maps a keypress to an [Action](http://openbox.org/wiki/Help:Actions). The "Close" action closes the active window, which is very useful for an HTPC. It allows you to quit the current application and return back to the launcher by using a button on your remote. For example, you can use the F10 key to quit the current application like so: +```bash +nano ~/.config/openbox/rc.xml +``` +In the `` section, paste the following: +```xml + + + +``` +This assumes that you have a key on your TV remote that maps to F10. + +### Install Applications +The last step is to install your desired applications. Edit your Flex Launcher configuration file to add menu entries for each of the applications. See the [configuration file documentation](https://complexlogic.github.io/flex-launcher/configuration) for more details. + +## HTPC as Audio Receiver +You can use your HTPC as a smart audio receiver for listening to music or podcasts on your living room speakers. + +### Bluetooth +If your HTPC has Bluetooth connectivity, you can use it as a receiver and play audio from your smartphone or other device. PulseAudio has support for the A2DP Bluetooth profile via the BlueZ stack. Make sure you have the module installed. It is packaged as `pulseaudio-bluetooth` on Arch and `pulseaudio-module-bluetooth` on Debian/Raspberry Pi. Also, make sure that the systemd bluetooth service is enabled. + +You will need to pair your device with your HTPC. This is best done with the `bluetoothctl` utility while you are logged in via SSH. Instructions can be found on the [Arch Wiki](https://wiki.archlinux.org/title/Bluetooth#Pairing) (applicable to all distros, not just Arch). + +The pairing only needs to be performed once. Subsequent connections can be initiated from the Bluetooth settings on your mobile device. Make sure to check on your device that audio output is enabled for the connection. Then, you can test playing some audio. PulseAudio should send the audio to your default sink without any additional configuration. + +### Spotify Connect +Spotify has a feature called Spotify Connect that allows Premium subscribers to stream audio to another device from the app. [Librespot](https://github.com/librespot-org/librespot) implements Spotify Connect as a Linux daemon. With Librespot, you can play Spotify audio on your HTPC, but control it from your smartphone. + +First, install Librespot. I recommend building it from source. The instructions are available on GitHub. + +I manage Librespot with a systemd user service: +```bash +mkdir -p ~/.config/systemd/user +nano ~/.config/systemd/user/librespot.service +``` +Paste the following into the file: +```ini +[Unit] +Description=Librespot Spotify Connect daemon +After=network-online.target +Wants=network-online.target +Wants=pulseaudio.service +After=pulseaudio.service + +[Service] +ExecStart=/usr/bin/librespot \ + --backend pulseaudio \ + --bitrate 320 \ + --device-type computer \ + --disable-audio-cache \ + --name "HTPC" + +[Install] +WantedBy=network-online.target +``` +Verify that the path of the `librespot` executable is the same on your system, or change it if necessary. The `--bitrate` controls the bitrate of the streamed audio in kbps. The `--name` controls which name your HTPC will show up as in the Spotify app devices menu. Change them if desired. + +If you want Librespot to always run, you can simply enable the service: +```bash +systemctl --user enable librespot +``` +If you want Librespot to stop when you launch an application, you will have to use scripts and manually start and stop it: +```bash +systemctl --user start librespot +systemctl --user stop librespot +``` +Verify Librespot is running with: +```bash +systemctl --user status librespot +``` +Then, try to connect to it with the Spotify app on your phone. + +## Using an IR Remote +Some mini PCs have an integrated IR receiver, which can receive signals from a conventional TV remote. The Linux kernel has built-in support for decoding IR signals, which allows you to translate them into keyboard presses. Before starting, make sure the IR receiver is enabled in your motherboard settings. + +The IR signal decoding can be controlled with the `ir-keytable` utility. This program is packaged with `v4l-utils` on most distros. First, check to see that your IR receiver is recognized: +```bash +sudo ir-keytable +``` +The result should look something like this: +``` +Found /sys/class/rc/rc0/ with: + Name: ITE8708 CIR transceiver + Driver: ite-cir + Default keymap: rc-rc6-mce + Input device: /dev/input/event3 + LIRC device: /dev/lirc0 + Supported kernel protocols: lirc rc-5 rc-5-sz jvc sony nec sanyo mce_kbd rc-6 sharp xmp imon rc-mm + Enabled kernel protocols: lirc nec rc-6 + bus: 25, vendor/product: 1283:0000, version: 0x0000 + Repeat delay = 500 ms, repeat period = 125 ms +``` +Take note of the supported kernel protocols. Test the receiving with `-t`: +```bash +ir-keytable -v -t -p irc,rc-5,rc-5-sz,jvc,sony,nec,sanyo,mce_kbd,rc-6,sharp,xmp,imon,rc-mm +``` +The protocols specified with `-p` should be comma delimited, and must *not* contain a space. You should update the protocols of the above command based on your output of `sudo ir-keytable` in the previous step. + +The `-t` mode will output information for each received signal. Point your remote at the receiver and press various buttons to verify that the signals are being received. Take note of the protocol and the hex values for each button. You will need to create a mapping file that contains the corresponding hex value for each button name. Here is a simple example: +``` +#table RCA, type: rc-6 +0x800f741e KEY_UP +0x800f741f KEY_DOWN +0x800f7420 KEY_LEFT +0x800f7421 KEY_RIGHT +0x800f7422 KEY_ENTER +0x800f7423 KEY_BACKSPACE +0x800f7425 KEY_ESC +``` +The table name is arbitrary, but the `type` must match one of the supported protocols from the previous step. You can find a comprehensive list of valid button names by running: +```bash +irrecord -l +``` +Save your mapping file as `/etc/rc_keymaps/default.txt`. Then, test the mapping: +```bash +sudo ir-keytable -c -w /etc/rc_keymaps/default.txt +``` +Open a program on your HTPC, press the mapped buttons on your remote, and verify that the proper keys are being triggered on the HTPC. + +### Make the Mapping Persistent +The mapping will not persist between reboots. Therefore, you will need to set up a way to run the `ir-keytable` command automatically after boot. One way to do that is with a systemd service. +```bash +sudo nano /etc/systemd/system/remote-setup.service +``` +Paste the following into the file: +```ini +[Unit] +Description=Set up IR remotes +After=multi-user.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/ir-keytable -c -w /etc/rc_keymaps/default.txt + +[Install] +WantedBy=multi-user.target +``` +Save the file, then enable the service: +```bash +sudo systemctl enable remote-setup +``` +Reboot your HTPC and verify that the remote still works. diff --git a/docs/setup_windows.md b/docs/setup_windows.md new file mode 100644 index 0000000..20bdb32 --- /dev/null +++ b/docs/setup_windows.md @@ -0,0 +1,56 @@ +--- +layout: default +title: Windows Setup Guide +--- +# Windows Setup Guide +## Table of Contents +1. [Overview](#overview) +2. [Editing Your Config File](#editing-your-config-file) +3. [Autostarting](#autostarting) +4. [Autologin](#autologin) +5. [Disable Lock Screen](#disable-lock-screen) +6. [Kiosk Mode Setup](#kiosk-mode-setup) + +## Overview +This page contains tips for setting up Flex Launcher on Windows-based systems. These instructions are tailored for Windows 10, but should apply to Windows 11 as well. + +## Editing Your Config File +I recommend editing your configuration file with a development-oriented text editor such as [Notepad++](https://notepad-plus-plus.org/) instead of the default Windows Notepad program. These text editors have color syntax highlighting for the INI format which makes sections, keys, values, and comments visually distinct. + +Several settings require a path as a value. In Windows Explorer, hold shift on your keyboard, then right click on a file or folder and select "Copy as path" from the context menu to copy the file path. You can then paste the path into your configuration file so you don't have to type the path manually. + +## Autostarting +In a typical HTPC setup, Flex Launcher should be configured to autostart after login. Right click `flex-launcher.exe` and selct "Create shortcut" in the context menu. Use Windows key + R to bring up the run box, then type `shell:startup` and press enter. This will bring up a Windows Explorer window of your autostart folder. Move the shortcut created in the previous step into this folder, which will cause Flex Launcher to autostart. + +Make sure that no other graphical programs are set to autostart. The window creation order of autostarted programs is not easily predictable, so if there is another program that creates a window *after* Flex Launcher, it could cause Flex Launcher to lose the window focus. + +## Autologin +Autologin should be configured to prevent users from having to enter a PIN/password with a keyboard after boot. Use Windows key + R to bring up the run box, then type `netplwiz` and press enter. In the resulting window, uncheck the box that says "Users must enter a user name and password to use this computer", then click apply. In the dialog box that pops up, enter the user name that you want to sign in automatically, and the password for it. + +## Disable Lock Screen +If you plan to use the `:sleep` special command to put your HTPC to sleep, you will be greeted by the Windows lock screen by default after your HTPC wakes up. This requires you to enter your PIN/password with a keyboard, even if autologin was configured. The lock screen should be disabled so that you return back to Flex Launcher immediately after waking from sleep. + +Open the Settings app, then select Accounts, then select Sign-in options. Under the "Require sign-in" section, change the drop down menu value to "Never". + +## Kiosk Mode Setup +"Kiosk Mode" typically refers to a user interface which resembles an embedded-style device that performs only a single function (as opposed to a multitasking PC with a desktop interface). This section contains instructions to set up my interpretation of a "Kiosk Mode" interface for a Windows HTPC. Before starting, make sure you have [set up autologin](#autologin) and [disabled your lock screen](#disable-lock-screen) per the previous sections. + +In Windows Explorer, navigate to the folder that contains Flex Launcher. Hold shift on your keyboard, then right click on `flex-launcher.exe` and select "Copy as path" from the context menu. Use Windows key + R to bring up the run box, then type `gpedit.msc` and press enter. In the left pane under "User Configuration", select "Administrative Templates", then "System". In the right pane, double click "Custom User Interface". Change the radio button to "Enabled". Then, under "Interface file name", paste the path to `flex-launcher.exe` that was copied in the previous step. Press OK to confirm the changes. This will cause Flex Launcher to replace the Windows desktop as your default graphical user interface. + +Running Flex Launcher in this manner changes the working directory. The default config file that ships with Flex Launcher uses relative paths from the directory containing `flex-launcher.exe`, which will no longer be valid. Therefore, you should convert all relative paths in your config file to absolute paths, e.g. `.\assets\icons\kodi.png` should instead be `C:\path\to\assets\icons\kodi.png`, otherwise images will not load properly. + +Reboot your HTPC for the change to take effect. + +### Restoring the Desktop +It may be desirable to use the desktop interface occasionally, e.g. during maintenance or installation of new software. You can get your desktop back by running `explorer.exe`. This can be accomplished in a few ways. + +Using the `QuitCmd` setting in your config file, you can automatically restore your desktop after you quit Flex Launcher: +```ini +[General] +... +QuitCmd=:fork explorer.exe +``` + +To manually restore the desktop, hold Ctrl + Shift + Esc to bring up Task Manager. Then, select File->Run new task from the toolbar. Type `explorer.exe` and press OK. + +You can permanently switch back to the desktop as your default interface by navigating to Custom User Interface in the Group Policy editor as above, and disabling it. \ No newline at end of file diff --git a/extra/docs/compilation_guide.md b/extra/docs/compilation_guide.md deleted file mode 100755 index a40f0f4..0000000 --- a/extra/docs/compilation_guide.md +++ /dev/null @@ -1,98 +0,0 @@ - # Compiling Flex Launcher -
    - Table of Contents -
      -
    1. - Overview -
    2. -
    3. - Linux -
    4. -
    5. - Windows -
    -
    - -## Overview - Flex Launcher builds natively on Linux and Windows, and features a cross-platform CMake build system. The following external dependencies are required: - - SDL >= 2.0.14 - - SDL_image >=2.0.5 - - SDL_ttf >= 2.0.15 - -## Linux -Flex Launcher on Linux builds with GCC. This guide assumes you already have the tools Git, CMake, and GCC installed on your system. If not, consult your distro's documentation. - -First, install the dependencies. The steps to do so are dependent on your distro: - -### APT-based Distributions (Debian, Ubuntu, Mint, Raspberry Pi OS etc.) -``` -sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev -``` - -### Pacman-based Distributions (Arch, Manjaro, etc.) -``` -sudo pacman -S sdl2 sdl2_image sdl2_ttf -``` - -### Building -Clone the master repo and create a build directory: -``` -git clone https://github.com/complexlogic/flex-launcher.git -cd flex-launcher -mkdir build && cd build -``` -Generate the Makefile: -``` -cmake -Wno-dev .. -``` -If you're building on Raspberry Pi, it's recommended to pass -DRPI=1 to cmake, which tweaks the default configuration to be more Pi-centric. - -Build and test the program: -``` -make -./flex-launcher -``` -Optionally, install it into your system directories: -``` -sudo make install -``` -By default, this will install the program and assets with a prefix of /usr/local. If you wish to use a different prefix, re-run the cmake generation step with the -DCMAKE_INSTALL_PREFIX flag. - -## Windows -Flex Launcher on Windows builds with Visual Studio, and uses [vcpkg](https://vcpkg.io/en/index.html) to manage the dependencies. Before starting, make sure the following steps are completed: -- Visual Studio is installed. The Community Edition is available for download without charge from Microsoft's website. The following tools and features for Visual Studio are required: - - C++ core desktop features - - Latest MSVC - - Latest Windows SDK - - C++ CMake tools -- Git is installed and in your PATH environment variable -- CMake is installed and in your PATH environment variable - -### Building -Clone the master repo and create a build directory: -``` -git clone https://github.com/complexlogic/flex-launcher.git -cd flex-launcher -mkdir build && cd build -``` -Build the dependencies with vcpkg: -``` -git clone https://github.com/microsoft/vcpkg -.\vcpkg\bootstrap-vcpkg.bat -disableMetrics -.\vcpkg\vcpkg install sdl2 sdl2-image[libjpeg-turbo,libwebp] sdl2-ttf --triplet=x64-windows-static -``` -Generate the Visual Studio project files: -``` -cmake -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE=".\vcpkg\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET="x64-windows-static" .. -``` -If you're using a different version of Visual Studio than above, then change the generator output. - -Build and test the program: -``` -cmake --build . -.\Debug\flex-launcher.exe -``` -Optionally, generate a clean zipped install package which may then be extracted to a directory of your choosing: -``` -cmake --build . --config Release --target package -``` diff --git a/extra/docs/configuration.md b/extra/docs/configuration.md deleted file mode 100755 index af633c0..0000000 --- a/extra/docs/configuration.md +++ /dev/null @@ -1,493 +0,0 @@ -# Configuring Flex Launcher - -
    - Table of Contents -
      -
    1. - Overview -
    2. -
    3. - Settings -
    4. -
    5. - Creating Menus - -
    6. - Clock -
    7. -
    8. - Screensaver -
    9. -
    10. - Hotkeys -
    11. -
    12. - Gamepad Controls -
    13. -
    -
    - - -## Overview -Flex Launcher uses an [INI file](https://en.wikipedia.org/wiki/INI_file) to configure settings and menus. The INI file consists of sections enclosed in square brackets, and in each section there are entries which consist of a key and a value. Example: -``` -[Section] -Key1=value -Key2=value -... -``` -A line can be commented out by using the # character at the beginning of the line, which will cause the line to be ignored by the program. Here are a few things to note about the configuration settings for Flex Launcher: -- All keys and values are case sensitive. -- Full UTF-8 character set is supported for titles. -- The following image formats are supported: JPEG, PNG, and WebP -- Relative paths are evaluated with respect to the *current working directory*, which may not be the same as the directory that the config file is located in. It is recommended to use absolute paths whenever possible to eliminate any confusion. -- Color is specified in 24 bit RGB HEX format, *without* a 0x or # prefix e.g. the color red should be FF0000. HEX color pickers can be easily found online to assist color choices. -- Several settings allow for values to be specified in pixels *or* as a percentage of another value. In this case, if no percent sign is detected it will be interpreted as pixels, and if the percent sign is present, than it will be interpreted as a percent value e.g. "5" means 5 pixels and "5%" means 5 percent. -## Settings -Every config file must have a section titled "Settings". Within this section, the following keys may be used to control the behavior of Flex Launcher: - - -
    - List of settings - -
    - -#### DefaultMenu -This is the title of the main menu that shows when Flex Launcher is started. The value *must* match the name of one of your menu sections, or there will be an error and Flex Launcher will refuse to start. See the [Creating Menus](#creating-menus) section for more information. - -#### MaxButtons -The maximum number of buttons that can be displayed on the screen. If your menu has more entries than this value, they will be split into multiple pages. A value of 3-5 is sensible for a typical TV size and viewing distance. - -Default: 4 -#### BackgroundMode -Defines what mode the background will be. Possible values: "Color", "Image", and "Slideshow" -- Color: The background will be a solid color. -- Image: The background will be an image. -- Slideshow: The background will be a series of images displayed in random order, with a fading transition between each image. - -Default: Color -#### BackgroundColor -When ```BackgroundMode``` is set to "Color", this setting defines the color of the background. - -Default: 000000 (Black) - -#### BackgroundImage -When ```BackgroundMode``` is set to "Image", this setting defines the image to be displayed in the background. The value should be a path to an image file. If the image is not the same resolution as your desktop, it will be stretched accordingly. - -#### SlideshowDirectory -When ```BackgroundMode``` is set to "Slideshow", this setting defines the directory (folder) which contains the images to display in the background. The value should be a path to a directory on your filesystem. The number of images that may be scanned from the directory is limited to 250. - -#### SlideshowImageDuration -When ```BackgroundMode``` is set to "Slideshow", this setting defines the amount of time in seconds to display each image. Must be an integer value. - -Default: 30 - -#### SlideshowTransitionTime -When ```BackgroundMode``` is set to "Slideshow", this setting defines the amount of time in seconds that the next background image will fade in. The fading transition may be disabled by setting this to 0, which will yield a "hard" transition between images. Decimal values are acceptable. - -Default: 1.5 - -#### IconSize -The width and height of icons on the screen in pixels. If an icon is not the same resolution, it will be stretched accordingly. - -Default: 256 - -#### IconSpacing -Distance between the menu entry icons, in pixels or percent of the screen width. - -Default: 5% - -#### TitleFont -Defines the font to use for the titles of the menu entries. The value should be the path to a TrueType (TTF) font file. Non-TTF font formats are not supported. Flex Launcher ships with a handful of libre fonts. - -Default: OpenSans - -#### TitleFontSize -Defines the font size of each menu entry title. - -Default: 36 - -#### TitleColor -Defines the color of the menu entry titles. - -Default: FFFFFF (White) - -#### TitleOpacity -Defines the opacity of the menu entry titles. Must be a percent value. - -Default: 100% - -#### TitleOversizeMode -Defines the behavior when the width of a menu entry title exceeds the width of its icon (which is defined in ```IconSize```). Possible values: "Truncate", "Shrink", and "None" -- Truncate: Truncates the title at the maximum width and adds "..." to the end. -- Shrink: Shrinks oversized titles to a smaller font size than ```TitleFontSize``` so that the entire title fits within the maximum width. -- None: No action is taken to limit the width of titles. Overlaps with other titles may occur, and it is the user's responsibility to manually handle any such case. - -Default: Truncate - -#### TitlePadding -Defines the vertical spacing between an icon and its title, in pixels. - -Default: 20 - -#### HighlightColor -Defines the color of the highlight cursor. - -Default: FFFFFF (White) - -#### HighlightOpacity -Defines the opacity of the highlight cursor. Must be a percent value. - -Default: 25% - -#### HighlightCornerRadius -Defines the corner radius of the highlight cursor, in pixels. A value of 0 will yield a plain rectangle. Increasing the value will yield a rounded rectangle with increasingly round corners. - -Default: 0 - -#### HighlightVPadding -Defines the amount of vertical distance that the highlight cursor extends beyond the top and bottom of the menu entry icon, in pixels. - -Default: 30 - -#### HighlightHPadding -Defines the amount of horizontal distance that the highlight cursor extends beyond the left and right of the menu entry icon, in pixels. - -Default: 30 - -#### ButtonCenterline -Defines the vertical centering of the menu entries in percent of the screen height. A value of 50% will cause the buttons to be centered halfway in the screen. Increasing the value will lower the buttons, and lowering it will raise them. - -Default: 50% - -#### ScrollIndicators -Defines whether scroll indicators will be enabled in the event that a menu has multiple pages of entries. The scroll indicators are arrows which appear in the bottom left or bottom right of the screen to inform the user that the are other pages of buttons to scroll to. This setting is a boolean "true" or "false". - -Default: true - -#### ScrollIndicatorColor -Defines the color of the scroll indicators. - -Default: FFFFFF (White) - -#### ScrollIndicatorOpacity -Defines the opacity of the scroll indicators. Must be a percent value. - -Default: 100% - -#### OnLaunch -Defines the action that Flex Launcher will take upon the launch of an application. Possible values: "Hide", "None", and "Blank" -- Hide: Flex Launcher will hide its window while the application is running, and then show itself again after the application has closed. This avoids window focus conflicts between the launcher and the application, but will cause the desktop to be briefly visible when an application is launched. -- None: Flex Launcher will maintain its window, and the launched application will have to draw itself over Flex Launcher. The desktop will never be visible, but this mode could cause window focusing conflicts depending on your configuration. This setting should only be used if all your applications launch in a fullscreen mode. -- Blank: Same as "None", except Flex Launcher will change to a blank, black screen. - -Default: Hide - -#### ResetOnBack -Defines whether Flex Launcher will remember the previous entry position when going back to a previous menu. If set to true, the highlight will be reset to the first entry in the menu when going back. This setting is a boolean "true" or "false". - -Default: false - -## Creating Menus -At least one menu must be defined in the configuration file, and the title must match the ```DefaultMenu``` setting value. The title of the menu is the section name. Any title may be used that is not reserved for another section, such as "Settings", and "Gamepad". The entries of the menu are implemented as key=value pairs. The name of the key will be ignored by the program, and is therefore arbtrary. However, it is recommended to pick something intutitive such as Entry1, Entry2, Entry3, etc. The entry information is contained in the value. - -Each entry value contains 3 parts of information in order: the title, the icon image path, and the command to run when the button is clicked. These are delimited by semicolons: -``` -Entry=title;icon_path;command -``` -The command is typically one of the following: -1. The path to the program executable that you want to launch -2. Windows: the path to a program shortcut (.lnk file) -3. Linux: the path to a [.desktop file](desktop-files-linux-only) -4. A [special command](#special-commands) -5. The path to an executable script, in the case that you want to perform multiple actions upon program launch. - - A simple example menu titled ```Media``` is shown below: -``` -[Media] -Entry1=Kodi;C:\Pictures\Icons\kodi.png;"C:\Program Shortcuts\kodi.lnk" -Entry2=Netflix;C:\Pictures\Icons\netflix.png;"C:\Program Shortcuts\netflix.lnk" -Entry3=Plex;C:\Pictures\Icons\plex.png;"C:\Program Shortcuts\plex.lnk" -Entry4=Back;C:\Pictures\Icons\back.png;:back -``` - -### Special Commands -Special commands are commands that are internal to Flex Launcher and begin with a colon. The following is a list of special commands: - -#### :submenu -Change to a different menu. Requires a menu title as an argument. For example, the command ```:submenu Games``` will change to the menu ```Games```. The argument must be a valid menu title that is defined elsewhere in the config file. - -#### :back -Go back to the previous menu. - -#### :home -Change to the menu defined in the ```DefaultMenu``` setting. - -#### :quit -Quit Flex Launcher. - -#### :left -Move the highlight cursor left. - -#### :right -Move the highlight cursor right. - -#### :select -Press enter on the current selection. This special command is only available as a gamepad command, it is forbidden for menu entries. - -#### :shutdown -Shut down the computer.1 - -#### :restart -Restart the computer.1 - -#### :sleep -Put the computer to sleep.1 - -1 *Linux: Only works in systemd-based distros. Non-systemd distro users need to implement the command manually for their init system.* - -### Desktop Files (Linux Only) -If the application you want to launch was installed via your distro's package manager, a .desktop file was most likely provided. The command to launch a Linux application can simply be the path to its .desktop file, and Flex Launcher will run the Exec command that the developers have specified in the file. Desktop files are located in /usr/share/applications. - -#### Desktop Actions -Some .desktop files contain "Actions", which affect how the program is launched. An action may be specified by delimiting it from the path to the .desktop file with a semicolon. For example, Steam has a mode called "Big Picture Mode", which provides an interface similar to a game console and is ideal for a living room PC. The action in the .desktop file is called "BigPicture". A sample menu entry to launch Steam in Big Picture mode is shown below: -``` -Entry1=Steam;/path/to/steamicon.png;/usr/share/applications/steam.desktop;BigPicture -``` - -## Clock -Flex Launcher contains a clock widget, which displays the current time, and, optionally, the current date. The following settings may be used to control the behavior of the clock. - -#### Enabled -Defines whether or not the clock is enabled. This setting is a boolean "true" or "false". - -Default: false - -#### ShowDate -Defines whether or not the current date should be shown in addition to the current time. This setting is a boolean "true" or "false". - -Default: false - -#### Alignment -Defines which side of the screen the clock text should align to. Possible values: "Left" and "Right" - -Default: Left - -#### Font -Defines the font to use for the clock text. The value should be the path to a TrueType (TTF) font file. - -Default: SourceSansPro - -#### FontSize -Defines the font size of the clock text - -Default: 50 - -#### Margin -Defines the distance of the clock text from the top and side of the screen, in pixels or percent of the screen height. - -Default: 5% - -#### Color -Defines the color of the clock text. - -Default: FFFFFF (White) - -#### Opacity -Defines the opacity of the clock text. Must be a percent value. - -Default: 100% - -#### TimeFormat -Defines the format of the current time. Possible values: "24hr", "12hr", and "Auto" -- 24hr: The clock will be a 24 hour format. -- 12hr: The clock will be a 12 hour format, including AM/PM designation in your locale. -- Auto: Automatically determine the time format based on your system locale. - -Default: Auto - -#### DateFormat -Defines the order of the month and day in the date. Possible values: "Little", "Big", "Auto" -- Little: The day will come before the month. -- Big: The month will come before the day. -- Auto: Automatically determine the date format based on your system locale. - -#### IncludeWeekday -Defines whether the date format should include the abbreviated weekday in your system locale. This setting is a boolean "true" or "false" - -Default: true - -## Screensaver -Flex Launcher contains a screensaver feature, which will dim the screen after the input has been idle for the specified amount of time. Here are the settings that control the behavior of the screensaver - -#### Enabled -Defines whether or not the screensaver is enabled. This setting is a boolean "true" or "false". - -Default: false - -#### IdleTime -Defines the amount of time in seconds that the input should be idle before activating the screensaver - -Default: 300 (5 minutes) - -#### Intensity -Defines the amount to dim the screen. Must be a percent value. - -Default: 70% - -#### PauseSlideshow -When ```BackgroundMode``` is set to "Slideshow", this setting defines whether or not the slideshow should be paused while the screensaver is active. This setting is a boolean "true" or "false". - -Default: true - -## Hotkeys -Flex Launch supports configurable hotkeys, which executes a command when a specified key is pressed. Each hotkey consists of a key=value pair, where the key is an arbitrary name, and the value contains the SDL keycode of the hotkey and the command to run when it is pressed, delimited by a semicolon: -``` -Hotkey=keycode;command -``` -The keycode is a HEX value *without* any 0x or # prefix. There are two ways to find a keycode for a given key. The first is to use the [lookup table provided by SDL](https://wiki.libsdl.org/SDLKeycodeLookup). The name of each key is in the right column of the table, and the corresponding HEX keycode is in the center column. The second is to [run Flex Launcher in debug mode](../../README.md#debugging), press the key, then check the log. For each keystroke, the name of the key will be printed and the HEX value will be in parenthesis next to it. - -Any key can be set as a hotkey, except keys that are reserved for the default controls: the left/right arrow keys, enter/return, and backspace. Hotkeys may be used to "speed dial" your favorite applications, or to add controls via [special commands](#special-commands). As an example configuration below, the first hotkey is mapped to F1 and will launch Kodi when it is pressed, and the second hotkey is mapped to F12 and will cause Flex Launcher to quit when it is pressed: -``` -[Hotkeys] -Hotkey1=4000003A;"C:\Program Shortcuts\kodi.lnk" -Hotkey2=40000045;:quit -``` - -## Gamepad Controls -Flex Launcher has built-in support for gamepad controls through SDL. All settings for gamepads will be in a section titled ```Gamepad```. Within the section, there are key=value pairs which define the gamepad settings and the commands to be run when a button or axis is pressed. - -### Settings -The following settings are available in the ```Gamepad``` section to define the behavior of gamepads - -#### Enabled -Defines whether or not gamepad controls are enabled. This setting is a boolean "true" or "false". - -Default: false - -#### DeviceIndex -Defines the device index of the gamepad in SDL. In most cases this is not needed and should be left commented out. - -Default: 0 - -#### ControllerMappingsFile -A path to a text file that contains 1 or more controller mappings to override the default. This is usually not necessary, but if you want to change the mapping for your controller, or there is no default mapping for your controller in SDL, it can be specified via this interface. A community database of mappings for many common controllers can be found [here](https://github.com/gabomdq/SDL_GameControllerDB). Alternatively, you may create a custom mapping using a GUI tool such as the [SDL2 Gamepad Tool](https://generalarcade.com/gamepadtool/). - -### Controls -The controls are defined in key=value pairs, where the key is the name of the axis or button that is pressed, and the value is the command that is to be run, which is typically a [special command](#special-commands). An axis is an analog stick or a trigger. For analog sticks, negative (-) represents left for the x axis and up for the y axis, and postive (+) represents right for the x axis and down for the y axis. - -The [SDL GameController](https://wiki.libsdl.org/CategoryGameController) interface is an abstraction which conceptualizes a controller as having an Xbox-style layout. The mapping names in SDL are based on the *location* of the buttons on an Xbox controller, and may not correspond to the actual labelling of the buttons on your controller. For example, ```ButtonA``` is for the "bottom" button, ```ButtonB``` is for the "right" button of the 4 main control buttons. If you have a Playstation-style controller, those mapping names will correspond to the X button and the Circle button, respectively. - -The default controls in Flex Launcher allow the user to move the highlight cursor left and right by using the left stick or the DPad, select an entry by pressing A, and go back to the previous menu by pressing B. These controls are simple and will suffice for the vast majority of use cases. - -The following axis and buttons are available for control in Flex Launcher: -- LStickX- -- LStickX+ -- LStickY- -- LStickY+ -- RStickX- -- RStickX+ -- RStickY- -- RStickY+ -- LTrigger -- RTrigger -- ButtonA -- ButtonB -- ButtonX -- ButtonY -- ButtonBack -- ButtonGuide -- ButtonStart -- ButtonLeftStick -- ButtonRightStick -- ButtonLeftShoulder -- ButtonRightShoulder -- ButtonDPadUp -- ButtonDPadDown -- ButtonDPadLeft -- ButtonDPadRight diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d1defce..8f8137d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,42 +1,43 @@ -#Build instructions +#Build main launcher executable file +set(SOURCES + launcher.c + launcher.h + util.c + util.h + image.c + image.h + debug.c + debug.h + clock.c + clock.h +) if (UNIX) - add_executable(${EXECUTABLE_TITLE} "launcher.c") + add_executable(${EXECUTABLE_TITLE} ${SOURCES}) endif () if (WIN32) set(APP_ICON_RESOURCE_WINDOWS "${PROJECT_SOURCE_DIR}/config/${EXECUTABLE_TITLE}.rc") - add_executable(${EXECUTABLE_TITLE} "launcher.c" ${APP_ICON_RESOURCE_WINDOWS}) - set_target_properties(${EXECUTABLE_TITLE} PROPERTIES WIN32_EXECUTABLE TRUE) + set(MANIFEST_FILE "${PROJECT_BINARY_DIR}/${EXECUTABLE_TITLE}.manifest") + add_executable(${EXECUTABLE_TITLE} WIN32 ${SOURCES} ${MANIFEST_FILE} ${APP_ICON_RESOURCE_WINDOWS}) + set_property(TARGET ${EXECUTABLE_TITLE} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") endif() -set (EXECUTABLE_OUTPUT_PATH "${PROJECT_BINARY_DIR}") -target_include_directories(${EXECUTABLE_TITLE} PUBLIC "${PROJECT_BINARY_DIR}") -add_library(util "util.c") -target_include_directories(util PUBLIC "${PROJECT_BINARY_DIR}") -add_library(image "image.c") -target_include_directories(image PUBLIC "${PROJECT_BINARY_DIR}") -add_library(ldebug "debug.c") -target_include_directories(ldebug PUBLIC "${PROJECT_BINARY_DIR}") -add_library(clock "clock.c") -target_include_directories(clock PUBLIC "${PROJECT_BINARY_DIR}") -# Configure main header file -configure_file("${PROJECT_SOURCE_DIR}/src/launcher_config.h.in" "${PROJECT_BINARY_DIR}/launcher_config.h") +# Build libraries +add_subdirectory("platform") -#SDL includes and libs +# Link libraries together +target_link_libraries(${EXECUTABLE_TITLE} platform) if (UNIX) - target_include_directories(util PUBLIC "${SDL2_INCLUDE_DIRS}") - target_include_directories(image PUBLIC "${SDL2_INCLUDE_DIRS}") - target_include_directories(ldebug PUBLIC "${SDL2_INCLUDE_DIRS}") - target_include_directories(clock PUBLIC "${SDL2_INCLUDE_DIRS}" "${SDL2_TTF_INCLUDE_DIRS}") - target_include_directories(${EXECUTABLE_TITLE} PUBLIC "${SDL2_INCLUDE_DIRS}" "${SDL2_IMAGE_INCLUDE_DIRS}" "${SDL2_TTF_INCLUDE_DIRS}") - target_link_libraries(${EXECUTABLE_TITLE} image util ldebug clock platform inih SDL2::Main SDL2::Image SDL2::TTF m) -endif () -if (WIN32) - target_link_libraries(util SDL2::SDL2 SDL2::SDL2main SDL2::SDL2-static) - target_link_libraries(image SDL2::SDL2 SDL2::SDL2main SDL2::SDL2-static) - target_link_libraries(ldebug SDL2::SDL2 SDL2::SDL2main SDL2::SDL2-static) - target_link_libraries(clock SDL2::SDL2 SDL2::SDL2main SDL2::SDL2-static SDL2::SDL2_ttf) - target_link_libraries(${EXECUTABLE_TITLE} image ldebug clock platform inih util SDL2::SDL2 SDL2::SDL2main SDL2::SDL2-static SDL2::SDL2_image SDL2::SDL2_ttf) + target_link_libraries(${EXECUTABLE_TITLE} PkgConfig::SDL2 PkgConfig::SDL2_IMAGE PkgConfig::SDL2_TTF PkgConfig::INIH m) +else () + target_link_libraries(${EXECUTABLE_TITLE} + $ + $,SDL2::SDL2,SDL2::SDL2-static> + $,SDL2_image::SDL2_image,SDL2_image::SDL2_image-static> + $,SDL2_ttf::SDL2_ttf,SDL2_ttf::SDL2_ttf-static> + ${INIH} + ${GETOPT} + PowrProf + ) + target_include_directories(${EXECUTABLE_TITLE} PUBLIC ${INIH_INCLUDE_DIR} ${GETOPT_INCLUDE_DIR}) endif () - -add_subdirectory("external") -add_subdirectory("platform") +target_include_directories(${EXECUTABLE_TITLE} SYSTEM PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/external") diff --git a/src/clock.c b/src/clock.c new file mode 100644 index 0000000..d961710 --- /dev/null +++ b/src/clock.c @@ -0,0 +1,321 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "launcher.h" +#include +#include "util.h" +#include "image.h" +#include "clock.h" +#include "debug.h" +#include "platform/platform.h" + +static void calculate_text_metrics(TTF_Font *font, const char *text, int *h, int *x_offset); +static void calculate_clock_geometry(Clock *clk); +static void format_time(Clock *clk); +static void format_date(Clock *clk); +static void calculate_clock_positioning(Clock *clk); + +extern Config config; +extern State state; +extern Geometry geo; + +// A function to calculate height and x offset of text +static void calculate_text_metrics(TTF_Font *font, const char *text, int *h, int *x_offset) +{ + int ymin = 0; + int ymax = 0; + int xmin, xmax, xadvance; + int current_ymin, current_ymax; + char *p = (char*) text; + Uint16 code_point; + int bytes; + + // Get text height, x offset + while (*p != '\0') { + code_point = get_unicode_code_point(p, &bytes); + TTF_GlyphMetrics(font, + code_point, + &xmin, + &xmax, + ¤t_ymin, + ¤t_ymax, + &xadvance + ); + if (current_ymax > ymax) + ymax = current_ymax; + if (current_ymin < ymin) + ymin = current_ymin; + if (p == text && config.clock_alignment == ALIGNMENT_LEFT) + *x_offset = xmin; + + p += bytes; + } + if (config.clock_alignment == ALIGNMENT_RIGHT) + *x_offset = xadvance - xmax; + *h = ymax - ymin; +} + +// A function to calculate the spacing and offsets of the clock text +static void calculate_clock_geometry(Clock *clk) +{ + int line_skip = TTF_FontLineSkip(clk->text_info.font); + int h_time, h_date; + + // Calculate text height and x offset + calculate_text_metrics(clk->text_info.font, + clk->time_string, + &h_time, + &clk->x_offset_time + ); + + if (config.clock_show_date) { + calculate_text_metrics(clk->text_info.font, + clk->date_string, + &h_date, + &clk->x_offset_date + ); + + int spacing = (int) (CLOCK_SPACING_FACTOR * (float) h_time); + clk->y_advance = spacing + h_time; + } + + // Calculate y offset from margin + clk->y_offset = (line_skip - h_time) / 2; +} + +// A function to get the current time from the operating system +void get_time(Clock *clk) +{ + int previous_min = 0; + int previous_day = 0; + if (clk->time_info != NULL) { + previous_min = clk->time_info->tm_min; + previous_day = clk->time_info->tm_mday; + } + + // Get current time + time(&clk->current_time); + clk->time_info = localtime(&clk->current_time); + + // Set render flags if time and/or date changed + if (clk->time_info == NULL || previous_min != clk->time_info->tm_min) { + clk->render_time = true; + if (clk->time_info == NULL || previous_day != clk->time_info->tm_mday) + clk->render_date = true; + } +} + +// A function to format the current time according to user settings +static void format_time(Clock *clk) +{ + char *format = NULL; + if (clk->time_format == FORMAT_TIME_24HR) + format = TIME_STRING_24HR; + else + format = TIME_STRING_12HR; + strftime(clk->time_string, + sizeof(clk->time_string), + format, + clk->time_info + ); +} + +// A function to format the current date according to user settings +static void format_date(Clock *clk) +{ + char *format = NULL; + + // Get date format + if (clk->date_format == FORMAT_DATE_LITTLE) + format = DATE_STRING_LITTLE; + else + format = DATE_STRING_BIG; + char weekday[MAX_CLOCK_CHARS + 1]; + char date[MAX_CLOCK_CHARS + 1]; + size_t bytes = sizeof(clk->date_string); + + // Get weekday name + if (config.clock_include_weekday) { + strftime(weekday, + sizeof(weekday), + "%a ", + clk->time_info + ); + } + else + weekday[0] = '\0'; + copy_string(clk->date_string, + weekday, + sizeof(clk->date_string) + ); + bytes -= strlen(weekday); + if (bytes) { + // Get date + strftime(date, + sizeof(date), + format, + clk->time_info + ); + bytes -= strlen(date); + strncat(clk->date_string, + date, + bytes + ); + } +} + +// A function to calculate the x and y coordinates of the clock text +static void calculate_clock_positioning(Clock *clk) +{ + if (config.clock_alignment == ALIGNMENT_LEFT) { + clk->time_rect.x = config.clock_margin - clk->x_offset_time; + if (config.clock_show_date) + clk->date_rect.x = config.clock_margin - clk->x_offset_date; + } + else { + clk->time_rect.x = geo.screen_width - config.clock_margin - clk->time_rect.w + clk->x_offset_time; + if (config.clock_show_date) + clk->date_rect.x = geo.screen_width - config.clock_margin - clk->date_rect.w + clk->x_offset_date; + } + clk->time_rect.y = config.clock_margin - clk->y_offset; + if (config.clock_show_date) + clk->date_rect.y = clk->time_rect.y + clk->y_advance; +} + +// A function to initialize the clock +void init_clock(Clock *clk) +{ + // Initialize clock structure + clk->text_info = (TextInfo) { + .font = NULL, + .font_size = (int) config.clock_font_size, + .font_path = &config.clock_font_path, + .color = &config.clock_font_color, + .shadow = config.clock_shadows, + .oversize_mode = OVERSIZE_NONE + }; + clk->time_format = config.clock_time_format; + clk->date_format = config.clock_date_format; + clk->time_info = NULL; + if (config.clock_shadows) { + clk->text_info.shadow_color = &config.clock_shadow_color; + calculate_shadow_alpha(clk->text_info); + } + else + clk->text_info.shadow_color = NULL; + + // Load the font + int error = load_font(&clk->text_info, FILENAME_DEFAULT_CLOCK_FONT); + if (error) { + config.clock_enabled = false; + return; + } + + // Get time and format it into a string + get_time(clk); + if (clk->time_format == FORMAT_TIME_AUTO || clk->date_format == FORMAT_DATE_AUTO) { + char region[3]; + memset(region, '\0', sizeof(region)); + get_region(region); + if (clk->time_format == FORMAT_TIME_AUTO) + clk->time_format = get_time_format(region); + if (config.clock_show_date && clk->date_format == FORMAT_DATE_AUTO) + clk->date_format = get_date_format(region); + } + + // Render the time and date + format_time(clk); + clk->time_texture = render_text_texture(clk->time_string, + &clk->text_info, + &clk->time_rect, + NULL + ); + if (config.clock_show_date) { + format_date(clk); + clk->date_texture = render_text_texture(clk->date_string, + &clk->text_info, + &clk->date_rect, + NULL + ); + } + + // Calculate geometry + calculate_clock_geometry(clk); + calculate_clock_positioning(clk); + clk->render_time = false; + clk->render_date = false; +} + +// A function to render the time to image +void render_clock(Clock *clk) +{ + format_time(clk); + clk->time_surface = render_text(clk->time_string, + &clk->text_info, + &clk->time_rect, + NULL + ); + if (clk->render_date) { + format_date(clk); + clk->date_surface = render_text(clk->date_string, + &clk->text_info, + &clk->date_rect, + NULL + ); + calculate_clock_geometry(clk); + } + calculate_clock_positioning(clk); + state.clock_ready = true; +} + +// A functio nto render the time to image in a separate thread +int render_clock_async(void *data) +{ + Clock *clk = (Clock*) data; + render_clock(clk); + return 0; +} + +// A function to get the time format for a region +TimeFormat get_time_format(const char *region) +{ + const char *countries[] = { + "US", + "CA", + "GB", + "AU", + "NZ", + "IN" + }; + TimeFormat format = FORMAT_TIME_24HR; + for (size_t i = 0; i < sizeof(countries) / sizeof(countries[0]); i++) { + if (!strcmp(region, countries[i])) { + format = FORMAT_TIME_12HR; + break; + } + } + return format; +} + +// A function to get the date format for a region +DateFormat get_date_format(const char *region) +{ + const char *countries[] = { + "US", + "JP", + "CN" + }; + DateFormat format = FORMAT_DATE_LITTLE; + for (size_t i = 0; i < sizeof(countries) / sizeof(countries[0]); i++) { + if (!strcmp(region, countries[i])) { + format = FORMAT_DATE_BIG; + break; + } + } + return format; +} diff --git a/src/clock.h b/src/clock.h new file mode 100644 index 0000000..32f9475 --- /dev/null +++ b/src/clock.h @@ -0,0 +1,32 @@ +#define MAX_CLOCK_CHARS 20 +#define CLOCK_SPACING_FACTOR 0.5F + +// Clock +typedef struct { + SDL_Surface *time_surface; + SDL_Surface *date_surface; + SDL_Texture *time_texture; + SDL_Texture *date_texture; + SDL_Rect time_rect; + SDL_Rect date_rect; + TextInfo text_info; + time_t current_time; + struct tm *time_info; + int x_offset_time; + int x_offset_date; + int y_offset; + int y_advance; + char time_string[MAX_CLOCK_CHARS + 1]; + char date_string[MAX_CLOCK_CHARS + 1]; + TimeFormat time_format; + DateFormat date_format; + bool render_time; + bool render_date; +} Clock; + +void init_clock(Clock *clk); +void get_time(Clock *clk); +void render_clock(Clock *clk); +int render_clock_async(void *data); +TimeFormat get_time_format(const char *region); +DateFormat get_date_format(const char *region); diff --git a/src/debug.c b/src/debug.c index f3cead7..9b43586 100644 --- a/src/debug.c +++ b/src/debug.c @@ -8,338 +8,257 @@ #include #include "util.h" #include "debug.h" +#include "platform/platform.h" #ifdef __unix__ #include "platform/unix.h" #endif -extern config_t config; -extern SDL_RWops *log_file; +static int init_log(void); + +extern Config config; +extern FILE *log_file; // A function to initialize the logging subsystem static int init_log() { - // Determine log path - char log_file_path[MAX_PATH_CHARS + 1]; - #ifdef __unix__ - char log_file_directory[MAX_PATH_CHARS + 1]; - join_paths(log_file_directory, 4, getenv("HOME"), ".local", "share", EXECUTABLE_TITLE); - make_directory(log_file_directory); - join_paths(log_file_path, 2, log_file_directory, FILENAME_LOG); - #else - join_paths(log_file_path, 2, config.exe_path, FILENAME_LOG); - #endif + // Determine log path + char log_file_path[MAX_PATH_CHARS + 1]; +#ifdef __unix__ + char log_file_directory[MAX_PATH_CHARS + 1]; + join_paths(log_file_directory, sizeof(log_file_directory), 4, getenv("HOME"), ".local", "share", EXECUTABLE_TITLE); + make_directory(log_file_directory); + join_paths(log_file_path, sizeof(log_file_path), 2, log_file_directory, FILENAME_LOG); +#else + join_paths(log_file_path, sizeof(log_file_path), 2, config.exe_path, FILENAME_LOG); +#endif - // Open log - log_file = SDL_RWFromFile(log_file_path, "w"); - if (log_file == NULL) { - #ifdef __unix__ - printf("Failed to create log file\n"); - #endif - quit(1); - } - #ifdef __unix__ - if (config.debug) { - printf("Debug mode enabled\nLog is outputted to %s\n", log_file_path); - } - #endif - return 0; + // Open log + log_file = fopen(log_file_path, "wb"); + if (log_file == NULL) { +#ifdef __unix__ + printf("Failed to create log file"); +#endif + quit(EXIT_FAILURE); + } +print_version(log_file); +fputs("\n", log_file); +print_compiler_info(log_file); +fputs("\n", log_file); +#ifdef __unix__ + if (config.debug) + printf("Debug mode enabled\nLog is outputted to %s\n", log_file_path); +#endif + return 0; } // A function to output a printf-style formatted line to the log -void output_log(log_level_t log_level, const char *format, ...) +void output_log(LogLevel log_level, const char *format, ...) { - // Don't output a debug message if we aren't in debug mode - if (log_level == LOGLEVEL_DEBUG && !config.debug) { - return; - } + // Don't output a debug message if we aren't in debug mode + if (log_level == LOGLEVEL_DEBUG && !config.debug) + return; - // Initialize logging if not already initialized - else if (log_file == NULL) { - init_log(); - } - - // Output log - static char buffer[MAX_LOG_LINE_BYTES]; - va_list args; - va_start(args, format); - int length = vsnprintf(buffer, MAX_LOG_LINE_BYTES - 1, format, args); - SDL_RWwrite(log_file, buffer, 1, length); - - #ifdef __unix__ - if (log_level > LOGLEVEL_DEBUG) { - fputs(buffer, stderr); - } - #endif - va_end(args); + // Initialize logging if not already initialized + if (log_file == NULL) + init_log(); + + // Output log + static char buffer[MAX_LOG_LINE_BYTES]; + va_list args; + va_start(args, format); + size_t length = (size_t) vsnprintf(buffer, MAX_LOG_LINE_BYTES - 1, format, args); + fwrite(buffer, 1, length, log_file); + if (config.debug) + fflush(log_file); + +#ifdef __unix__ + if (log_level > LOGLEVEL_DEBUG) + fputs(buffer, stderr); +#endif + va_end(args); + + if (log_level == LOGLEVEL_FATAL) + quit(EXIT_FAILURE); } -// A function to print the parsed settings to the command line + +void print_compiler_info(FILE *stream) +{ + fputs("Build date: " __DATE__ "\n", stream); +#ifdef __GNUC__ + fprintf(stream, "Compiler: GCC %u.%u\n", __GNUC__, __GNUC_MINOR__); +#endif +#ifdef _MSC_VER + fprintf(stream, "Compiler: Microsoft C/C++ %.2f\n", (float) _MSC_VER / 100.0f); +#endif + +} + +// A function to print the parsed settings to the log void debug_settings() { - output_log(LOGLEVEL_DEBUG, "======================== Settings ========================\n"); - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_DEFAULT_MENU,config.default_menu); - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_MAX_BUTTONS,config.max_buttons); - if (config.background_mode == MODE_COLOR) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_BACKGROUND_MODE,"Color"); - } - else if (config.background_mode == MODE_IMAGE) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_BACKGROUND_MODE,"Image"); - } - else if (config.background_mode == MODE_SLIDESHOW) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_BACKGROUND_MODE,"Slideshow"); - } - output_log(LOGLEVEL_DEBUG, "%s R: %i\n",SETTING_BACKGROUND_COLOR,config.background_color.r); - output_log(LOGLEVEL_DEBUG, "%s G: %i\n",SETTING_BACKGROUND_COLOR,config.background_color.g); - output_log(LOGLEVEL_DEBUG, "%s B: %i\n",SETTING_BACKGROUND_COLOR,config.background_color.b); - if (config.background_image != NULL) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_BACKGROUND_IMAGE,config.background_image); - } - if (config.slideshow_directory != NULL) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SLIDESHOW_DIRECTORY ,config.slideshow_directory); - } - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_SLIDESHOW_IMAGE_DURATION, config.slideshow_image_duration / 1000); - output_log(LOGLEVEL_DEBUG, "%s: %f\n",SETTING_SLIDESHOW_TRANSITION_TIME, ((float) config.slideshow_transition_time) / 1000.0f); - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_ICON_SIZE,config.icon_size); - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_ICON_SPACING,config.icon_spacing); - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_TITLE_FONT,config.title_font_path); - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_TITLE_FONT_SIZE,config.font_size); - output_log(LOGLEVEL_DEBUG, "%s R: %i\n",SETTING_TITLE_COLOR,config.title_color.r); - output_log(LOGLEVEL_DEBUG, "%s G: %i\n",SETTING_TITLE_COLOR,config.title_color.g); - output_log(LOGLEVEL_DEBUG, "%s B: %i\n",SETTING_TITLE_COLOR,config.title_color.b); - output_log(LOGLEVEL_DEBUG, "%s A: %i\n",SETTING_TITLE_COLOR,config.title_color.a); - if (config.title_oversize_mode == MODE_TEXT_TRUNCATE) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_TITLE_OVERSIZE_MODE,"Truncate"); - } - else if (config.title_oversize_mode == MODE_TEXT_SHRINK) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_TITLE_OVERSIZE_MODE,"Shrink"); - } - else if (config.title_oversize_mode == MODE_TEXT_NONE) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_TITLE_OVERSIZE_MODE,"None"); - } - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_TITLE_PADDING,config.title_padding); - output_log(LOGLEVEL_DEBUG, "%s R: %i\n",SETTING_HIGHLIGHT_COLOR,config.highlight_color.r); - output_log(LOGLEVEL_DEBUG, "%s G: %i\n",SETTING_HIGHLIGHT_COLOR,config.highlight_color.g); - output_log(LOGLEVEL_DEBUG, "%s B: %i\n",SETTING_HIGHLIGHT_COLOR,config.highlight_color.b); - output_log(LOGLEVEL_DEBUG, "%s A: %i\n",SETTING_HIGHLIGHT_COLOR,config.highlight_color.a); - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_HIGHLIGHT_CORNER_RADIUS,config.highlight_rx); - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_HIGHLIGHT_VPADDING,config.highlight_vpadding); - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_HIGHLIGHT_HPADDING,config.highlight_hpadding); - if (config.scroll_indicators) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SCROLL_INDICATORS,"true"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SCROLL_INDICATORS,"false"); - } - output_log(LOGLEVEL_DEBUG, "%s R: %i\n",SETTING_SCROLL_INDICATOR_COLOR,config.scroll_indicator_color.r); - output_log(LOGLEVEL_DEBUG, "%s G: %i\n",SETTING_SCROLL_INDICATOR_COLOR,config.scroll_indicator_color.g); - output_log(LOGLEVEL_DEBUG, "%s B: %i\n",SETTING_SCROLL_INDICATOR_COLOR,config.scroll_indicator_color.b); - output_log(LOGLEVEL_DEBUG, "%s A: %i\n",SETTING_SCROLL_INDICATOR_COLOR,config.scroll_indicator_color.a); - if (config.on_launch == MODE_ON_LAUNCH_HIDE) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_ON_LAUNCH,"Hide"); - } - else if (config.on_launch == MODE_ON_LAUNCH_BLANK) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_ON_LAUNCH,"Blank"); - } - else if (config.on_launch == MODE_ON_LAUNCH_NONE) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_ON_LAUNCH,"None"); - } - if (config.reset_on_back) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_RESET_ON_BACK,"true"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_RESET_ON_BACK,"false"); - } - output_log(LOGLEVEL_DEBUG, "========================== Clock ==========================\n"); - if (config.clock_enabled) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_ENABLED, "true"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_ENABLED, "false"); - } - if (config.clock_show_date) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_SHOW_DATE, "true"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_SHOW_DATE, "false"); - } - if (config.clock_alignment == ALIGNMENT_LEFT) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_ALIGNMENT, "Left"); - } - else if (config.clock_alignment == ALIGNMENT_RIGHT) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_ALIGNMENT, "Right"); - } - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_FONT, config.clock_font_path); - output_log(LOGLEVEL_DEBUG, "%s: %u\n", SETTING_CLOCK_FONT_SIZE,config.clock_font_size); - output_log(LOGLEVEL_DEBUG, "%s: %i\n",SETTING_CLOCK_MARGIN, config.clock_margin); - output_log(LOGLEVEL_DEBUG, "%s R: %i\n", SETTING_CLOCK_COLOR, config.clock_color.r); - output_log(LOGLEVEL_DEBUG, "%s G: %i\n", SETTING_CLOCK_COLOR, config.clock_color.g); - output_log(LOGLEVEL_DEBUG, "%s B: %i\n", SETTING_CLOCK_COLOR, config.clock_color.b); - output_log(LOGLEVEL_DEBUG, "%s A: %i\n", SETTING_CLOCK_COLOR, config.clock_color.a); - if (config.clock_time_format == FORMAT_TIME_12HR) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_TIME_FORMAT, "12hr"); - } - else if (config.clock_time_format == FORMAT_TIME_24HR) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_TIME_FORMAT, "24hr"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_TIME_FORMAT, "Auto"); - } - if (config.clock_date_format == FORMAT_DATE_LITTLE) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_DATE_FORMAT, "Little"); - } - else if (config.clock_date_format == FORMAT_DATE_BIG) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_DATE_FORMAT, "Big"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_DATE_FORMAT, "Auto"); - } - if (config.clock_include_weekday) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_INCLUDE_WEEKDAY, "true"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_CLOCK_INCLUDE_WEEKDAY, "false"); - } + log_debug("======================= General ========================\n"); + DEBUG_STR(SETTING_DEFAULT_MENU, config.default_menu); + DEBUG_BOOL(SETTING_VSYNC, config.vsync); + DEBUG_INT(SETTING_FPS_LIMIT, config.fps_limit); + DEBUG_INT(SETTING_APPLICATION_TIMEOUT, config.application_timeout / 1000); + DEBUG_MODE(SETTING_ON_LAUNCH, MODE_SETTING_ON_LAUNCH, config.on_launch); + DEBUG_BOOL(SETTING_WRAP_ENTRIES, config.wrap_entries); + DEBUG_BOOL(SETTING_RESET_ON_BACK, config.reset_on_back); + DEBUG_BOOL(SETTING_MOUSE_SELECT, config.mouse_select); + DEBUG_BOOL(SETTING_INHIBIT_OS_SCREENSAVER, config.inhibit_os_screensaver); + DEBUG_STR(SETTING_STARTUP_CMD, config.startup_cmd); + DEBUG_STR(SETTING_QUIT_CMD, config.quit_cmd); + log_debug(""); + + log_debug("===================== Background =======================\n"); + DEBUG_MODE(SETTING_BACKGROUND_MODE, MODE_SETTING_BACKGROUND, config.background_mode); + DEBUG_COLOR(SETTING_BACKGROUND_COLOR, config.background_color); + DEBUG_STR(SETTING_BACKGROUND_IMAGE, config.background_image); + DEBUG_STR(SETTING_SLIDESHOW_DIRECTORY, config.slideshow_directory); + DEBUG_INT(SETTING_SLIDESHOW_IMAGE_DURATION, config.slideshow_image_duration / 1000); + DEBUG_FLOAT(SETTING_SLIDESHOW_TRANSITION_TIME, ((float) config.slideshow_transition_time) / 1000.0f); + DEBUG_BOOL(SETTING_BACKGROUND_OVERLAY, config.background_overlay); + DEBUG_COLOR(SETTING_BACKGROUND_OVERLAY_COLOR, config.background_overlay_color); + log_debug(""); + + log_debug("======================= Layout =========================\n"); + DEBUG_INT(SETTING_MAX_BUTTONS, config.max_buttons); + DEBUG_INT(SETTING_ICON_SIZE, config.icon_size); + DEBUG_INT(SETTING_ICON_SPACING, config.icon_spacing); + DEBUG_STR(SETTING_VCENTER, config.vcenter[0] != '\0' ? config.vcenter : "50%"); + log_debug(""); + + log_debug("======================== Titles ========================\n"); + DEBUG_BOOL(SETTING_TITLES_ENABLED, config.titles_enabled); + DEBUG_STR(SETTING_TITLE_FONT, config.title_font_path); + DEBUG_INT(SETTING_TITLE_FONT_SIZE, config.title_font_size); + DEBUG_COLOR(SETTING_TITLE_FONT_COLOR, config.title_font_color); + DEBUG_BOOL(SETTING_TITLE_SHADOWS, config.title_shadows); + DEBUG_COLOR(SETTING_TITLE_SHADOW_COLOR, config.title_shadow_color); + DEBUG_MODE(SETTING_TITLE_OVERSIZE_MODE, MODE_SETTING_OVERSIZE, config.title_oversize_mode); + DEBUG_INT(SETTING_TITLE_PADDING, config.title_padding); + log_debug(""); + + log_debug("====================== Highlight =======================\n"); + DEBUG_COLOR(SETTING_HIGHLIGHT_FILL_COLOR, config.highlight_fill_color); + DEBUG_INT(SETTING_HIGHLIGHT_OUTLINE_SIZE, config.highlight_outline_size); + DEBUG_COLOR(SETTING_HIGHLIGHT_OUTLINE_COLOR, config.highlight_outline_color); + DEBUG_INT(SETTING_HIGHLIGHT_CORNER_RADIUS, config.highlight_rx); + DEBUG_INT(SETTING_HIGHLIGHT_VPADDING, config.highlight_vpadding); + DEBUG_INT(SETTING_HIGHLIGHT_HPADDING, config.highlight_hpadding); + log_debug(""); + + log_debug("================== Scroll Indicators ===================\n"); + DEBUG_BOOL(SETTING_SCROLL_INDICATORS, config.scroll_indicators); + DEBUG_COLOR(SETTING_SCROLL_INDICATOR_FILL_COLOR, config.scroll_indicator_fill_color); + DEBUG_INT(SETTING_SCROLL_INDICATOR_OUTLINE_SIZE, config.scroll_indicator_outline_size); + DEBUG_COLOR(SETTING_SCROLL_INDICATOR_OUTLINE_COLOR, config.scroll_indicator_outline_color); + log_debug(""); + + log_debug("======================== Clock =========================\n"); + DEBUG_BOOL(SETTING_CLOCK_ENABLED, config.clock_enabled); + DEBUG_BOOL(SETTING_CLOCK_SHOW_DATE, config.clock_show_date); + DEBUG_MODE(SETTING_CLOCK_ALIGNMENT, MODE_SETTING_ALIGNMENT, config.clock_alignment); + DEBUG_STR(SETTING_CLOCK_FONT, config.clock_font_path); + DEBUG_INT(SETTING_CLOCK_FONT_SIZE, config.clock_font_size); + DEBUG_INT(SETTING_CLOCK_MARGIN, config.clock_margin); + DEBUG_COLOR(SETTING_CLOCK_FONT_COLOR, config.clock_font_color); + DEBUG_BOOL(SETTING_CLOCK_SHADOWS, config.clock_shadows); + DEBUG_COLOR(SETTING_CLOCK_SHADOW_COLOR, config.clock_shadow_color); + DEBUG_MODE(SETTING_CLOCK_TIME_FORMAT, MODE_SETTING_TIME_FORMAT, config.clock_time_format); + DEBUG_MODE(SETTING_CLOCK_DATE_FORMAT, MODE_SETTING_DATE_FORMAT, config.clock_date_format); + DEBUG_BOOL(SETTING_CLOCK_INCLUDE_WEEKDAY, config.clock_include_weekday); + log_debug(""); - output_log(LOGLEVEL_DEBUG, "======================= Screensaver =======================\n"); - if (config.screensaver_enabled) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SCREENSAVER_ENABLED, "true"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SCREENSAVER_ENABLED, "false"); - } - output_log(LOGLEVEL_DEBUG, "%s: %i\n", SETTING_SCREENSAVER_IDLE_TIME, config.screensaver_idle_time / 1000); - if(strlen(config.screensaver_intensity_str)) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SCREENSAVER_INTENSITY, config.screensaver_intensity_str); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SCREENSAVER_INTENSITY, DEFAULT_SCREENSAVER_INTENSITY); - } - if (config.screensaver_pause_slideshow) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SCREENSAVER_PAUSE_SLIDESHOW, "true"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_SCREENSAVER_PAUSE_SLIDESHOW, "false"); - } + log_debug("===================== Screensaver ======================\n"); + DEBUG_BOOL(SETTING_SCREENSAVER_ENABLED, config.screensaver_enabled); + DEBUG_INT(SETTING_SCREENSAVER_IDLE_TIME, config.screensaver_idle_time / 1000); + DEBUG_STR(SETTING_SCREENSAVER_INTENSITY, config.screensaver_intensity_str[0] != '\0' ? config.screensaver_intensity_str : DEFAULT_SCREENSAVER_INTENSITY); + DEBUG_BOOL(SETTING_SCREENSAVER_PAUSE_SLIDESHOW, config.screensaver_pause_slideshow); + log_debug(""); } // A function to print the parsed menu entries to the command line -void debug_menu_entries(menu_t *first_menu, int num_menus) +void debug_menu_entries(Menu *first_menu, size_t num_menus) { - if (first_menu == NULL) { - output_log(LOGLEVEL_DEBUG, "No valid menus found\n"); - return; - } - output_log(LOGLEVEL_DEBUG, "======================= Menu Entries =======================\n"); - menu_t *menu = first_menu; - entry_t *entry; - for (int i = 0; i < num_menus; i ++) { - output_log(LOGLEVEL_DEBUG, "Menu Name: %s\n",menu->name); - output_log(LOGLEVEL_DEBUG, "Number of Entries: %i\n",menu->num_entries); - entry = menu->first_entry; - for (int j = 0; j < menu->num_entries; j++) { - output_log(LOGLEVEL_DEBUG, "Entry %i Title: %s\n",j,entry->title); - output_log(LOGLEVEL_DEBUG, "Entry %i Icon Path: %s\n",j,entry->icon_path); - output_log(LOGLEVEL_DEBUG, "Entry %i Command: %s\n",j,entry->cmd); - if (j != menu->num_entries - 1) { - output_log(LOGLEVEL_DEBUG, "\n"); - } - entry = entry->next; + if (first_menu == NULL) { + log_debug("No valid menus found"); + return; } - if (i != num_menus - 1) { - output_log(LOGLEVEL_DEBUG, "----------------------------------------------------------\n"); + log_debug("======================= Menu Entries =======================\n"); + Menu *menu = first_menu; + Entry *entry; + for (size_t i = 0; i < num_menus; i ++) { + log_debug("Menu Name: %s",menu->name); + log_debug("Number of Entries: %i",menu->num_entries); + entry = menu->first_entry; + for (size_t j = 0; j < menu->num_entries; j++) { + log_debug("Entry %i Title: %s",j,entry->title); + log_debug("Entry %i Icon Path: %s",j,entry->icon_path); + log_debug("Entry %i Command: %s",j,entry->cmd); + if (j != menu->num_entries - 1) + log_debug(""); + entry = entry->next; + } + if (i != num_menus - 1) { + log_debug("----------------------------------------------------------"); + } + menu = menu->next; } - menu = menu->next; - } - output_log(LOGLEVEL_DEBUG, "\n"); + log_debug(""); } -void debug_gamepad(gamepad_control_t *gamepad_controls) +void debug_gamepad(GamepadControl *gamepad_controls) { - output_log(LOGLEVEL_DEBUG, "======================== Gamepad ========================\n"); - if (config.gamepad_enabled) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_GAMEPAD_ENABLED, "true"); - } - else { - output_log(LOGLEVEL_DEBUG, "%s: %s\n",SETTING_GAMEPAD_ENABLED, "false"); - } - output_log(LOGLEVEL_DEBUG, "%s: %i\n", SETTING_GAMEPAD_DEVICE, config.gamepad_device); - if (config.gamepad_mappings_file != NULL) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", SETTING_GAMEPAD_MAPPINGS_FILE, config.gamepad_mappings_file); - } - for (gamepad_control_t *i = gamepad_controls; i != NULL; i = i->next) { - output_log(LOGLEVEL_DEBUG, "%s: %s\n", i->label, i->cmd); - } + log_debug("======================= Gamepad ========================\n"); + DEBUG_BOOL(SETTING_GAMEPAD_ENABLED, config.gamepad_enabled); + DEBUG_INT(SETTING_GAMEPAD_DEVICE, config.gamepad_device); + DEBUG_STR(SETTING_GAMEPAD_MAPPINGS_FILE, config.gamepad_mappings_file); + for (GamepadControl *i = gamepad_controls; i != NULL; i = i->next) + log_debug("%-25s %s", i->label, i->cmd); + log_debug(""); } -void debug_hotkeys(hotkey_t *hotkeys) +void debug_hotkeys(Hotkey *hotkeys) { - if (hotkeys == NULL) { - output_log(LOGLEVEL_DEBUG, "No hotkeys detected\n"); - return; - } - output_log(LOGLEVEL_DEBUG, "======================== Hotkeys =========================\n"); - int index = 0; - for (hotkey_t *i = hotkeys; i != NULL; i = i->next) { - output_log(LOGLEVEL_DEBUG, "Hotkey %i Keycode: %X\n", index, i->keycode); - output_log(LOGLEVEL_DEBUG, "Hotkey %i Command: %s\n", index, i->cmd); - index++; - } + if (hotkeys == NULL) { + log_debug("No hotkeys detected"); + return; + } + log_debug("======================== Hotkeys =========================\n"); + int index = 0; + for (Hotkey *i = hotkeys; i != NULL; i = i->next) { + log_debug("Hotkey %i Keycode: %X", index, i->keycode); + log_debug("Hotkey %i Command: %s", index, i->cmd); + index++; + } + log_debug(""); } // A function to debug the parsed slideshow files -void debug_slideshow(slideshow_t *slideshow) +void debug_slideshow(Slideshow *slideshow) { - output_log(LOGLEVEL_DEBUG, "======================== Slideshow ========================\n"); - output_log(LOGLEVEL_DEBUG, - "Found %i images in directory %s:\n", - slideshow->num_images, - config.slideshow_directory); - for (int i = 0; i < slideshow->num_images; i++) { - output_log(LOGLEVEL_DEBUG, "%s\n", slideshow->images[slideshow->order[i]]); - } + log_debug("======================== Slideshow ========================"); + log_debug("Found %i images in directory %s:", + slideshow->num_images, + config.slideshow_directory + ); + for (int i = 0; i < slideshow->num_images; i++) + log_debug(" %s", slideshow->images[slideshow->order[i]]); } // A function to debug the video settings void debug_video(SDL_Renderer *renderer, SDL_DisplayMode *display_mode) { - output_log(LOGLEVEL_DEBUG, "===================== Video Information =====================\n"); - output_log(LOGLEVEL_DEBUG, "Resolution: %ix%i\n", display_mode->w, display_mode->h); - output_log(LOGLEVEL_DEBUG, "Refresh rate: %i Hz\n", display_mode->refresh_rate); - output_log(LOGLEVEL_DEBUG, "Video driver: %s\n", SDL_GetCurrentVideoDriver()); - SDL_RendererInfo info; - SDL_GetRendererInfo(renderer, &info); - output_log(LOGLEVEL_DEBUG, "Supported Texture formats:\n"); - for(int i = 0; i < info.num_texture_formats; i++) { - output_log(LOGLEVEL_DEBUG, "%s\n", SDL_GetPixelFormatName(info.texture_formats[i])); - } -} - -// A function to print the current button geometry to the command line -void debug_button_positions(entry_t *entry, menu_t *current_menu, geometry_t *geo) -{ - output_log(LOGLEVEL_DEBUG, "================= Menu \"%s\" Page %i =================\n", - current_menu->name, - current_menu->page); - int left_margin; - int right_margin; - for (int i = 0; i < geo->num_buttons; i++) { - if (i == 0) { - left_margin = entry->icon_rect.x; - } - else if (i == geo->num_buttons - 1) { - right_margin = geo->screen_width - (entry->icon_rect.x + entry->icon_rect.w); - } - output_log(LOGLEVEL_DEBUG, "Button %i:\n",i); - output_log(LOGLEVEL_DEBUG, "Icon X1: %i\n",entry->icon_rect.x); - output_log(LOGLEVEL_DEBUG, "Icon X2: %i\n",entry->icon_rect.x + entry->icon_rect.w); - output_log(LOGLEVEL_DEBUG, "Icon Y1: %i\n",entry->icon_rect.y); - output_log(LOGLEVEL_DEBUG, "Icon Y2: %i\n",entry->icon_rect.y + entry->icon_rect.h); - output_log(LOGLEVEL_DEBUG, "Text X1: %i\n",entry->text_rect.x); - output_log(LOGLEVEL_DEBUG, "Text X2: %i\n",entry->text_rect.x + entry->icon_rect.w); - output_log(LOGLEVEL_DEBUG, "Text Y1: %i\n",entry->text_rect.y); - output_log(LOGLEVEL_DEBUG, "Text Y2: %i\n\n",entry->text_rect.y + entry->icon_rect.h); - entry = entry->next; - } - output_log(LOGLEVEL_DEBUG, "Total Buttons: %i\n",geo->num_buttons); - output_log(LOGLEVEL_DEBUG, "Left Margin: %i\n",left_margin); - output_log(LOGLEVEL_DEBUG, "Right Margin: %i\n",right_margin); + log_debug("================== Video Information ===================\n"); + log_debug("Resolution: %ix%i", display_mode->w, display_mode->h); + log_debug("Refresh rate: %i Hz", display_mode->refresh_rate); + log_debug("Video driver: %s", SDL_GetCurrentVideoDriver()); + log_debug(""); + SDL_RendererInfo info; + SDL_GetRendererInfo(renderer, &info); + log_debug("Supported Texture formats:"); + for(size_t i = 0; i < info.num_texture_formats; i++) + log_debug(" %s", SDL_GetPixelFormatName(info.texture_formats[i])); + log_debug(""); } diff --git a/src/debug.h b/src/debug.h index adf73eb..a41f23d 100644 --- a/src/debug.h +++ b/src/debug.h @@ -1,15 +1,32 @@ typedef enum { - LOGLEVEL_DEBUG = 0, - LOGLEVEL_ERROR, - LOGLEVEL_FATAL -} log_level_t; + LOGLEVEL_DEBUG = 0, + LOGLEVEL_ERROR, + LOGLEVEL_FATAL +} LogLevel; -static int init_log(void); -void output_log(log_level_t log_level, const char *format, ...); +void output_log(LogLevel log_level, const char *format, ...); +void print_compiler_info(FILE *stream); void debug_video(SDL_Renderer *renderer, SDL_DisplayMode *display_mode); void debug_settings(void); -void debug_gamepad(gamepad_control_t *gamepad_controls); -void debug_hotkeys(hotkey_t *hotkeys); -void debug_menu_entries(menu_t *first_menu, int num_menus); -void debug_slideshow(slideshow_t *slideshow); -void debug_button_positions(entry_t *entry, menu_t *current_menu, geometry_t *geo); +void debug_gamepad(GamepadControl *gamepad_controls); +void debug_hotkeys(Hotkey *hotkeys); +void debug_menu_entries(Menu *first_menu, size_t num_menus); +void debug_slideshow(Slideshow *slideshow); +void debug_button_positions(Entry *entry, Menu *current_menu, Geometry *geo); + +#ifdef _WIN32 +#define endline "\r\n" +#else +#define endline "\n" +#endif + +#define log_debug(msg, ...) output_log(LOGLEVEL_DEBUG, msg endline, ##__VA_ARGS__) +#define log_error(msg, ...) output_log(LOGLEVEL_ERROR, "" msg endline, ##__VA_ARGS__) +#define log_fatal(msg, ...) output_log(LOGLEVEL_FATAL, "" msg endline, ##__VA_ARGS__) + +#define DEBUG_COLOR(setting_name, color) log_debug("%-25s #%.2X%.2X%.2X%.2X", setting_name ":", color.r, color.g, color.b, color.a) +#define DEBUG_MODE(setting_name, type, value) log_debug("%-25s %s", setting_name ":", get_mode_setting(type, value)) +#define DEBUG_BOOL(setting_name, value) log_debug("%-25s %s", setting_name ":", value ? "true" : "false"); +#define DEBUG_STR(setting_name, value) log_debug("%-25s %s", setting_name ":", value != NULL ? value : "(null)") +#define DEBUG_INT(setting_name, value) log_debug("%-25s %i", setting_name ":", value) +#define DEBUG_FLOAT(setting_name, value) log_debug("%-25s %.2f", setting_name ":", value) diff --git a/src/external/CMakeLists.txt b/src/external/CMakeLists.txt deleted file mode 100644 index 0a717da..0000000 --- a/src/external/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_library(inih "ini.c") diff --git a/src/external/ini.c b/src/external/ini.c deleted file mode 100755 index f8a3ea3..0000000 --- a/src/external/ini.c +++ /dev/null @@ -1,298 +0,0 @@ -/* inih -- simple .INI file parser - -SPDX-License-Identifier: BSD-3-Clause - -Copyright (C) 2009-2020, Ben Hoyt - -inih is released under the New BSD license (see LICENSE.txt). Go to the project -home page for more info: - -https://github.com/benhoyt/inih - -*/ - -#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) -#define _CRT_SECURE_NO_WARNINGS -#endif - -#include -#include -#include - -#include "ini.h" - -#if !INI_USE_STACK -#if INI_CUSTOM_ALLOCATOR -#include -void* ini_malloc(size_t size); -void ini_free(void* ptr); -void* ini_realloc(void* ptr, size_t size); -#else -#include -#define ini_malloc malloc -#define ini_free free -#define ini_realloc realloc -#endif -#endif - -#define MAX_SECTION 50 -#define MAX_NAME 50 - -/* Used by ini_parse_string() to keep track of string parsing state. */ -typedef struct { - const char* ptr; - size_t num_left; -} ini_parse_string_ctx; - -/* Strip whitespace chars off end of given string, in place. Return s. */ -static char* rstrip(char* s) -{ - char* p = s + strlen(s); - while (p > s && isspace((unsigned char)(*--p))) - *p = '\0'; - return s; -} - -/* Return pointer to first non-whitespace char in given string. */ -static char* lskip(const char* s) -{ - while (*s && isspace((unsigned char)(*s))) - s++; - return (char*)s; -} - -/* Return pointer to first char (of chars) or inline comment in given string, - or pointer to NUL at end of string if neither found. Inline comment must - be prefixed by a whitespace character to register as a comment. */ -static char* find_chars_or_comment(const char* s, const char* chars) -{ -#if INI_ALLOW_INLINE_COMMENTS - int was_space = 0; - while (*s && (!chars || !strchr(chars, *s)) && - !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { - was_space = isspace((unsigned char)(*s)); - s++; - } -#else - while (*s && (!chars || !strchr(chars, *s))) { - s++; - } -#endif - return (char*)s; -} - -/* Similar to strncpy, but ensures dest (size bytes) is - NUL-terminated, and doesn't pad with NULs. */ -static char* strncpy0(char* dest, const char* src, size_t size) -{ - /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */ - size_t i; - for (i = 0; i < size - 1 && src[i]; i++) - dest[i] = src[i]; - dest[i] = '\0'; - return dest; -} - -/* See documentation in header file. */ -int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, - void* user) -{ - /* Uses a fair bit of stack (use heap instead if you need to) */ -#if INI_USE_STACK - char line[INI_MAX_LINE]; - int max_line = INI_MAX_LINE; -#else - char* line; - size_t max_line = INI_INITIAL_ALLOC; -#endif -#if INI_ALLOW_REALLOC && !INI_USE_STACK - char* new_line; - size_t offset; -#endif - char section[MAX_SECTION] = ""; - char prev_name[MAX_NAME] = ""; - - char* start; - char* end; - char* name; - char* value; - int lineno = 0; - int error = 0; - -#if !INI_USE_STACK - line = (char*)ini_malloc(INI_INITIAL_ALLOC); - if (!line) { - return -2; - } -#endif - -#if INI_HANDLER_LINENO -#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) -#else -#define HANDLER(u, s, n, v) handler(u, s, n, v) -#endif - - /* Scan through stream line by line */ - while (reader(line, (int)max_line, stream) != NULL) { -#if INI_ALLOW_REALLOC && !INI_USE_STACK - offset = strlen(line); - while (offset == max_line - 1 && line[offset - 1] != '\n') { - max_line *= 2; - if (max_line > INI_MAX_LINE) - max_line = INI_MAX_LINE; - new_line = ini_realloc(line, max_line); - if (!new_line) { - ini_free(line); - return -2; - } - line = new_line; - if (reader(line + offset, (int)(max_line - offset), stream) == NULL) - break; - if (max_line >= INI_MAX_LINE) - break; - offset += strlen(line + offset); - } -#endif - - lineno++; - - start = line; -#if INI_ALLOW_BOM - if (lineno == 1 && (unsigned char)start[0] == 0xEF && - (unsigned char)start[1] == 0xBB && - (unsigned char)start[2] == 0xBF) { - start += 3; - } -#endif - start = lskip(rstrip(start)); - - if (strchr(INI_START_COMMENT_PREFIXES, *start)) { - /* Start-of-line comment */ - } -#if INI_ALLOW_MULTILINE - else if (*prev_name && *start && start > line) { - /* Non-blank line with leading whitespace, treat as continuation - of previous name's value (as per Python configparser). */ - if (!HANDLER(user, section, prev_name, start) && !error) - error = lineno; - } -#endif - else if (*start == '[') { - /* A "[section]" line */ - end = find_chars_or_comment(start + 1, "]"); - if (*end == ']') { - *end = '\0'; - strncpy0(section, start + 1, sizeof(section)); - *prev_name = '\0'; -#if INI_CALL_HANDLER_ON_NEW_SECTION - if (!HANDLER(user, section, NULL, NULL) && !error) - error = lineno; -#endif - } - else if (!error) { - /* No ']' found on section line */ - error = lineno; - } - } - else if (*start) { - /* Not a comment, must be a name[=:]value pair */ - end = find_chars_or_comment(start, "=:"); - if (*end == '=' || *end == ':') { - *end = '\0'; - name = rstrip(start); - value = end + 1; -#if INI_ALLOW_INLINE_COMMENTS - end = find_chars_or_comment(value, NULL); - if (*end) - *end = '\0'; -#endif - value = lskip(value); - rstrip(value); - - /* Valid name[=:]value pair found, call handler */ - strncpy0(prev_name, name, sizeof(prev_name)); - if (!HANDLER(user, section, name, value) && !error) - error = lineno; - } - else if (!error) { - /* No '=' or ':' found on name[=:]value line */ -#if INI_ALLOW_NO_VALUE - *end = '\0'; - name = rstrip(start); - if (!HANDLER(user, section, name, NULL) && !error) - error = lineno; -#else - error = lineno; -#endif - } - } - -#if INI_STOP_ON_FIRST_ERROR - if (error) - break; -#endif - } - -#if !INI_USE_STACK - ini_free(line); -#endif - - return error; -} - -/* See documentation in header file. */ -int ini_parse_file(FILE* file, ini_handler handler, void* user) -{ - return ini_parse_stream((ini_reader)fgets, file, handler, user); -} - -/* See documentation in header file. */ -int ini_parse(const char* filename, ini_handler handler, void* user) -{ - FILE* file; - int error; - - file = fopen(filename, "r"); - if (!file) - return -1; - error = ini_parse_file(file, handler, user); - fclose(file); - return error; -} - -/* An ini_reader function to read the next line from a string buffer. This - is the fgets() equivalent used by ini_parse_string(). */ -static char* ini_reader_string(char* str, int num, void* stream) { - ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream; - const char* ctx_ptr = ctx->ptr; - size_t ctx_num_left = ctx->num_left; - char* strp = str; - char c; - - if (ctx_num_left == 0 || num < 2) - return NULL; - - while (num > 1 && ctx_num_left != 0) { - c = *ctx_ptr++; - ctx_num_left--; - *strp++ = c; - if (c == '\n') - break; - num--; - } - - *strp = '\0'; - ctx->ptr = ctx_ptr; - ctx->num_left = ctx_num_left; - return str; -} - -/* See documentation in header file. */ -int ini_parse_string(const char* string, ini_handler handler, void* user) { - ini_parse_string_ctx ctx; - - ctx.ptr = string; - ctx.num_left = strlen(string); - return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler, - user); -} diff --git a/src/external/ini.h b/src/external/ini.h deleted file mode 100755 index 4981c26..0000000 --- a/src/external/ini.h +++ /dev/null @@ -1,157 +0,0 @@ -/* inih -- simple .INI file parser - -SPDX-License-Identifier: BSD-3-Clause - -Copyright (C) 2009-2020, Ben Hoyt - -inih is released under the New BSD license (see LICENSE.txt). Go to the project -home page for more info: - -https://github.com/benhoyt/inih - -*/ - -#ifndef INI_H -#define INI_H - -/* Make this header file easier to include in C++ code */ -#ifdef __cplusplus -extern "C" { -#endif - -#include - -/* Nonzero if ini_handler callback should accept lineno parameter. */ -#ifndef INI_HANDLER_LINENO -#define INI_HANDLER_LINENO 0 -#endif - -/* Typedef for prototype of handler function. */ -#if INI_HANDLER_LINENO -typedef int (*ini_handler)(void* user, const char* section, - const char* name, const char* value, - int lineno); -#else -typedef int (*ini_handler)(void* user, const char* section, - const char* name, const char* value); -#endif - -/* Typedef for prototype of fgets-style reader function. */ -typedef char* (*ini_reader)(char* str, int num, void* stream); - -/* Parse given INI-style file. May have [section]s, name=value pairs - (whitespace stripped), and comments starting with ';' (semicolon). Section - is "" if name=value pair parsed before any section heading. name:value - pairs are also supported as a concession to Python's configparser. - - For each name=value pair parsed, call handler function with given user - pointer as well as section, name, and value (data only valid for duration - of handler call). Handler should return nonzero on success, zero on error. - - Returns 0 on success, line number of first error on parse error (doesn't - stop on first error), -1 on file open error, or -2 on memory allocation - error (only when INI_USE_STACK is zero). -*/ -int ini_parse(const char* filename, ini_handler handler, void* user); - -/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't - close the file when it's finished -- the caller must do that. */ -int ini_parse_file(FILE* file, ini_handler handler, void* user); - -/* Same as ini_parse(), but takes an ini_reader function pointer instead of - filename. Used for implementing custom or string-based I/O (see also - ini_parse_string). */ -int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, - void* user); - -/* Same as ini_parse(), but takes a zero-terminated string with the INI data -instead of a file. Useful for parsing INI data from a network socket or -already in memory. */ -int ini_parse_string(const char* string, ini_handler handler, void* user); - -/* Nonzero to allow multi-line value parsing, in the style of Python's - configparser. If allowed, ini_parse() will call the handler with the same - name for each subsequent line parsed. */ -#ifndef INI_ALLOW_MULTILINE -#define INI_ALLOW_MULTILINE 1 -#endif - -/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of - the file. See https://github.com/benhoyt/inih/issues/21 */ -#ifndef INI_ALLOW_BOM -#define INI_ALLOW_BOM 1 -#endif - -/* Chars that begin a start-of-line comment. Per Python configparser, allow - both ; and # comments at the start of a line by default. */ -#ifndef INI_START_COMMENT_PREFIXES -#define INI_START_COMMENT_PREFIXES ";#" -#endif - -/* Nonzero to allow inline comments (with valid inline comment characters - specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match - Python 3.2+ configparser behaviour. */ -#ifndef INI_ALLOW_INLINE_COMMENTS -#define INI_ALLOW_INLINE_COMMENTS 1 -#endif -#ifndef INI_INLINE_COMMENT_PREFIXES -#define INI_INLINE_COMMENT_PREFIXES ";" -#endif - -/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */ -#ifndef INI_USE_STACK -#define INI_USE_STACK 1 -#endif - -/* Maximum line length for any line in INI file (stack or heap). Note that - this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */ -#ifndef INI_MAX_LINE -#define INI_MAX_LINE 500 -#endif - -/* Nonzero to allow heap line buffer to grow via realloc(), zero for a - fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is - zero. */ -#ifndef INI_ALLOW_REALLOC -#define INI_ALLOW_REALLOC 0 -#endif - -/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK - is zero. */ -#ifndef INI_INITIAL_ALLOC -#define INI_INITIAL_ALLOC 200 -#endif - -/* Stop parsing on first error (default is to keep parsing). */ -#ifndef INI_STOP_ON_FIRST_ERROR -#define INI_STOP_ON_FIRST_ERROR 0 -#endif - -/* Nonzero to call the handler at the start of each new section (with - name and value NULL). Default is to only call the handler on - each name=value pair. */ -#ifndef INI_CALL_HANDLER_ON_NEW_SECTION -#define INI_CALL_HANDLER_ON_NEW_SECTION 0 -#endif - -/* Nonzero to allow a name without a value (no '=' or ':' on the line) and - call the handler with value NULL in this case. Default is to treat - no-value lines as an error. */ -#ifndef INI_ALLOW_NO_VALUE -#define INI_ALLOW_NO_VALUE 0 -#endif - -/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory - allocation functions (INI_USE_STACK must also be 0). These functions must - have the same signatures as malloc/free/realloc and behave in a similar - way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */ -#ifndef INI_CUSTOM_ALLOCATOR -#define INI_CUSTOM_ALLOCATOR 0 -#endif - - -#ifdef __cplusplus -} -#endif - -#endif /* INI_H */ diff --git a/src/external/nanosvg.h b/src/external/nanosvg.h index 3511664..60a3238 100755 --- a/src/external/nanosvg.h +++ b/src/external/nanosvg.h @@ -72,6 +72,7 @@ extern "C" { */ enum NSVGpaintType { + NSVG_PAINT_UNDEF = -1, NSVG_PAINT_NONE = 0, NSVG_PAINT_COLOR = 1, NSVG_PAINT_LINEAR_GRADIENT = 2, @@ -119,7 +120,7 @@ typedef struct NSVGgradient { } NSVGgradient; typedef struct NSVGpaint { - char type; + signed char type; union { unsigned int color; NSVGgradient* gradient; @@ -143,14 +144,17 @@ typedef struct NSVGshape float opacity; // Opacity of the shape. float strokeWidth; // Stroke width (scaled). float strokeDashOffset; // Stroke dash offset (scaled). - float strokeDashArray[8]; // Stroke dash array (scaled). - char strokeDashCount; // Number of dash values in dash array. + float strokeDashArray[8]; // Stroke dash array (scaled). + char strokeDashCount; // Number of dash values in dash array. char strokeLineJoin; // Stroke join type. char strokeLineCap; // Stroke cap type. float miterLimit; // Miter limit char fillRule; // Fill rule, see NSVGfillRule. unsigned char flags; // Logical or of NSVG_FLAGS_* flags float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + char fillGradient[64]; // Optional 'id' of fill gradient + char strokeGradient[64]; // Optional 'id' of stroke gradient + float xform[6]; // Root transformation for fill/stroke gradient NSVGpath* paths; // Linked list of paths in the image. struct NSVGshape* next; // Pointer to next shape, or NULL if last element. } NSVGshape; @@ -181,12 +185,11 @@ void nsvgDelete(NSVGimage* image); #endif #endif -#endif // NANOSVG_H - #ifdef NANOSVG_IMPLEMENTATION #include #include +#include #include #define NSVG_PI (3.14159265358979323846264338327f) @@ -395,7 +398,7 @@ typedef struct NSVGgradientData { char id[64]; char ref[64]; - char type; + signed char type; union { NSVGlinearData linear; NSVGradialData radial; @@ -611,7 +614,7 @@ static void nsvg__curveBounds(float* bounds, float* curve) } } -static NSVGparser* nsvg__createParser() +static NSVGparser* nsvg__createParser(void) { NSVGparser* p; p = (NSVGparser*)malloc(sizeof(NSVGparser)); @@ -815,9 +818,8 @@ static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) return NULL; } -static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, char* paintType) +static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, float *xform, signed char* paintType) { - NSVGattrib* attr = nsvg__getAttr(p); NSVGgradientData* data = NULL; NSVGgradientData* ref = NULL; NSVGgradientStop* stops = NULL; @@ -892,7 +894,7 @@ static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const f } nsvg__xformMultiply(grad->xform, data->xform); - nsvg__xformMultiply(grad->xform, attr->xform); + nsvg__xformMultiply(grad->xform, xform); grad->spread = data->spread; memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop)); @@ -956,6 +958,9 @@ static void nsvg__addShape(NSVGparser* p) memset(shape, 0, sizeof(NSVGshape)); memcpy(shape->id, attr->id, sizeof shape->id); + memcpy(shape->fillGradient, attr->fillGradient, sizeof shape->fillGradient); + memcpy(shape->strokeGradient, attr->strokeGradient, sizeof shape->strokeGradient); + memcpy(shape->xform, attr->xform, sizeof shape->xform); scale = nsvg__getAverageScale(attr->xform); shape->strokeWidth = attr->strokeWidth * scale; shape->strokeDashOffset = attr->strokeDashOffset * scale; @@ -991,13 +996,7 @@ static void nsvg__addShape(NSVGparser* p) shape->fill.color = attr->fillColor; shape->fill.color |= (unsigned int)(attr->fillOpacity*255) << 24; } else if (attr->hasFill == 2) { - float inv[6], localBounds[4]; - nsvg__xformInverse(inv, attr->xform); - nsvg__getLocalBounds(localBounds, shape, inv); - shape->fill.gradient = nsvg__createGradient(p, attr->fillGradient, localBounds, &shape->fill.type); - if (shape->fill.gradient == NULL) { - shape->fill.type = NSVG_PAINT_NONE; - } + shape->fill.type = NSVG_PAINT_UNDEF; } // Set stroke @@ -1008,12 +1007,7 @@ static void nsvg__addShape(NSVGparser* p) shape->stroke.color = attr->strokeColor; shape->stroke.color |= (unsigned int)(attr->strokeOpacity*255) << 24; } else if (attr->hasStroke == 2) { - float inv[6], localBounds[4]; - nsvg__xformInverse(inv, attr->xform); - nsvg__getLocalBounds(localBounds, shape, inv); - shape->stroke.gradient = nsvg__createGradient(p, attr->strokeGradient, localBounds, &shape->stroke.type); - if (shape->stroke.gradient == NULL) - shape->stroke.type = NSVG_PAINT_NONE; + shape->stroke.type = NSVG_PAINT_UNDEF; } // Set flags @@ -1195,6 +1189,19 @@ static const char* nsvg__parseNumber(const char* s, char* it, const int size) return s; } +static const char* nsvg__getNextPathItemWhenArcFlag(const char* s, char* it) +{ + it[0] = '\0'; + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '0' || *s == '1') { + it[0] = *s++; + it[1] = '\0'; + return s; + } + return s; +} + static const char* nsvg__getNextPathItem(const char* s, char* it) { it[0] = '\0'; @@ -1215,35 +1222,66 @@ static const char* nsvg__getNextPathItem(const char* s, char* it) static unsigned int nsvg__parseColorHex(const char* str) { - unsigned int c = 0, r = 0, g = 0, b = 0; - int n = 0; - str++; // skip # - // Calculate number of characters. - while(str[n] && !nsvg__isspace(str[n])) - n++; - if (n == 6) { - sscanf(str, "%x", &c); - } else if (n == 3) { - sscanf(str, "%x", &c); - c = (c&0xf) | ((c&0xf0) << 4) | ((c&0xf00) << 8); - c |= c<<4; - } - r = (c >> 16) & 0xff; - g = (c >> 8) & 0xff; - b = c & 0xff; - return NSVG_RGB(r,g,b); + unsigned int r=0, g=0, b=0; + if (sscanf(str, "#%2x%2x%2x", &r, &g, &b) == 3 ) // 2 digit hex + return NSVG_RGB(r, g, b); + if (sscanf(str, "#%1x%1x%1x", &r, &g, &b) == 3 ) // 1 digit hex, e.g. #abc -> 0xccbbaa + return NSVG_RGB(r*17, g*17, b*17); // same effect as (r<<4|r), (g<<4|g), .. + return NSVG_RGB(128, 128, 128); } +// Parse rgb color. The pointer 'str' must point at "rgb(" (4+ characters). +// This function returns gray (rgb(128, 128, 128) == '#808080') on parse errors +// for backwards compatibility. Note: other image viewers return black instead. + static unsigned int nsvg__parseColorRGB(const char* str) { - int r = -1, g = -1, b = -1; - char s1[32]="", s2[32]=""; - sscanf(str + 4, "%d%[%%, \t]%d%[%%, \t]%d", &r, s1, &g, s2, &b); - if (strchr(s1, '%')) { - return NSVG_RGB((r*255)/100,(g*255)/100,(b*255)/100); - } else { - return NSVG_RGB(r,g,b); + int i; + unsigned int rgbi[3]; + float rgbf[3]; + // try decimal integers first + if (sscanf(str, "rgb(%u, %u, %u)", &rgbi[0], &rgbi[1], &rgbi[2]) != 3) { + // integers failed, try percent values (float, locale independent) + const char delimiter[3] = {',', ',', ')'}; + str += 4; // skip "rgb(" + for (i = 0; i < 3; i++) { + while (*str && (nsvg__isspace(*str))) str++; // skip leading spaces + if (*str == '+') str++; // skip '+' (don't allow '-') + if (!*str) break; + rgbf[i] = nsvg__atof(str); + + // Note 1: it would be great if nsvg__atof() returned how many + // bytes it consumed but it doesn't. We need to skip the number, + // the '%' character, spaces, and the delimiter ',' or ')'. + + // Note 2: The following code does not allow values like "33.%", + // i.e. a decimal point w/o fractional part, but this is consistent + // with other image viewers, e.g. firefox, chrome, eog, gimp. + + while (*str && nsvg__isdigit(*str)) str++; // skip integer part + if (*str == '.') { + str++; + if (!nsvg__isdigit(*str)) break; // error: no digit after '.' + while (*str && nsvg__isdigit(*str)) str++; // skip fractional part + } + if (*str == '%') str++; else break; + while (nsvg__isspace(*str)) str++; + if (*str == delimiter[i]) str++; + else break; + } + if (i == 3) { + rgbi[0] = roundf(rgbf[0] * 2.55f); + rgbi[1] = roundf(rgbf[1] * 2.55f); + rgbi[2] = roundf(rgbf[2] * 2.55f); + } else { + rgbi[0] = rgbi[1] = rgbi[2] = 128; + } } + // clip values as the CSS spec requires + for (i = 0; i < 3; i++) { + if (rgbi[i] > 255) rgbi[i] = 255; + } + return NSVG_RGB(rgbi[0], rgbi[1], rgbi[2]); } typedef struct NSVGNamedColor { @@ -1651,9 +1689,9 @@ static void nsvg__parseUrl(char* id, const char* str) { int i = 0; str += 4; // "url("; - if (*str == '#') + if (*str && *str == '#') str++; - while (i < 63 && *str != ')') { + while (i < 63 && *str && *str != ')') { id[i] = *str++; i++; } @@ -2249,7 +2287,11 @@ static void nsvg__parsePath(NSVGparser* p, const char** attr) nargs = 0; while (*s) { - s = nsvg__getNextPathItem(s, item); + item[0] = '\0'; + if ((cmd == 'A' || cmd == 'a') && (nargs == 3 || nargs == 4)) + s = nsvg__getNextPathItemWhenArcFlag(s, item); + if (!*item) + s = nsvg__getNextPathItem(s, item); if (!*item) break; if (cmd != '\0' && nsvg__isCoordinate(item)) { if (nargs < 10) @@ -2594,7 +2636,7 @@ static void nsvg__parseSVG(NSVGparser* p, const char** attr) } } -static void nsvg__parseGradient(NSVGparser* p, const char** attr, char type) +static void nsvg__parseGradient(NSVGparser* p, const char** attr, signed char type) { int i; NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); @@ -2919,6 +2961,36 @@ static void nsvg__scaleToViewbox(NSVGparser* p, const char* units) } } +static void nsvg__createGradients(NSVGparser* p) +{ + NSVGshape* shape; + + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + if (shape->fill.type == NSVG_PAINT_UNDEF) { + if (shape->fillGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->fill.gradient = nsvg__createGradient(p, shape->fillGradient, localBounds, shape->xform, &shape->fill.type); + } + if (shape->fill.type == NSVG_PAINT_UNDEF) { + shape->fill.type = NSVG_PAINT_NONE; + } + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + if (shape->strokeGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->stroke.gradient = nsvg__createGradient(p, shape->strokeGradient, localBounds, shape->xform, &shape->stroke.type); + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + shape->stroke.type = NSVG_PAINT_NONE; + } + } + } +} + NSVGimage* nsvgParse(char* input, const char* units, float dpi) { NSVGparser* p; @@ -2932,6 +3004,9 @@ NSVGimage* nsvgParse(char* input, const char* units, float dpi) nsvg__parseXML(input, nsvg__startElement, nsvg__endElement, nsvg__content, p); + // Create gradients after all definitions have been parsed + nsvg__createGradients(p); + // Scale to viewBox nsvg__scaleToViewbox(p, units); @@ -3018,4 +3093,6 @@ void nsvgDelete(NSVGimage* image) free(image); } -#endif +#endif // NANOSVG_IMPLEMENTATION + +#endif // NANOSVG_H diff --git a/src/external/nanosvgrast.h b/src/external/nanosvgrast.h index b740c31..90d42e9 100755 --- a/src/external/nanosvgrast.h +++ b/src/external/nanosvgrast.h @@ -25,6 +25,8 @@ #ifndef NANOSVGRAST_H #define NANOSVGRAST_H +#include "nanosvg.h" + #ifndef NANOSVGRAST_CPLUSPLUS #ifdef __cplusplus extern "C" { @@ -47,7 +49,7 @@ typedef struct NSVGrasterizer NSVGrasterizer; */ // Allocated rasterizer context. -NSVGrasterizer* nsvgCreateRasterizer(); +NSVGrasterizer* nsvgCreateRasterizer(void); // Rasterizes SVG image, returns RGBA image (non-premultiplied alpha) // r - pointer to rasterizer context @@ -72,11 +74,11 @@ void nsvgDeleteRasterizer(NSVGrasterizer*); #endif #endif -#endif // NANOSVGRAST_H - #ifdef NANOSVGRAST_IMPLEMENTATION #include +#include +#include #define NSVG__SUBSAMPLES 5 #define NSVG__FIXSHIFT 10 @@ -112,7 +114,7 @@ typedef struct NSVGmemPage { } NSVGmemPage; typedef struct NSVGcachedPaint { - char type; + signed char type; char spread; float xform[6]; unsigned int colors[256]; @@ -148,7 +150,7 @@ struct NSVGrasterizer int width, height, stride; }; -NSVGrasterizer* nsvgCreateRasterizer() +NSVGrasterizer* nsvgCreateRasterizer(void) { NSVGrasterizer* r = (NSVGrasterizer*)malloc(sizeof(NSVGrasterizer)); if (r == NULL) goto error; @@ -329,6 +331,7 @@ static float nsvg__normalize(float *x, float* y) } static float nsvg__absf(float x) { return x < 0 ? -x : x; } +static float nsvg__roundf(float x) { return (x >= 0) ? floorf(x + 0.5) : ceilf(x - 0.5); } static void nsvg__flattenCubicBez(NSVGrasterizer* r, float x1, float y1, float x2, float y2, @@ -351,8 +354,8 @@ static void nsvg__flattenCubicBez(NSVGrasterizer* r, dx = x4 - x1; dy = y4 - y1; - d2 = nsvg__absf(((x2 - x4) * dy - (y2 - y4) * dx)); - d3 = nsvg__absf(((x3 - x4) * dy - (y3 - y4) * dx)); + d2 = nsvg__absf((x2 - x4) * dy - (y2 - y4) * dx); + d3 = nsvg__absf((x3 - x4) * dy - (y3 - y4) * dx); if ((d2 + d3)*(d2 + d3) < r->tessTol * (dx*dx + dy*dy)) { nsvg__addPathPoint(r, x4, y4, type); @@ -870,10 +873,10 @@ static NSVGactiveEdge* nsvg__addActive(NSVGrasterizer* r, NSVGedge* e, float sta // STBTT_assert(e->y0 <= start_point); // round dx down to avoid going too far if (dxdy < 0) - z->dx = (int)(-floorf(NSVG__FIX * -dxdy)); + z->dx = (int)(-nsvg__roundf(NSVG__FIX * -dxdy)); else - z->dx = (int)floorf(NSVG__FIX * dxdy); - z->x = (int)floorf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); + z->dx = (int)nsvg__roundf(NSVG__FIX * dxdy); + z->x = (int)nsvg__roundf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); // z->x -= off_x * FIX; z->ey = e->y1; z->next = 0; @@ -956,7 +959,7 @@ static float nsvg__clampf(float a, float mn, float mx) { return a < mn ? mn : (a static unsigned int nsvg__RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { - return (r) | (g << 8) | (b << 16) | (a << 24); + return ((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16) | ((unsigned int)a << 24); } static unsigned int nsvg__lerpRGBA(unsigned int c0, unsigned int c1, float u) @@ -1280,9 +1283,10 @@ static void nsvg__initPaint(NSVGcachedPaint* cache, NSVGpaint* paint, float opac if (grad->nstops == 0) { for (i = 0; i < 256; i++) cache->colors[i] = 0; - } if (grad->nstops == 1) { + } else if (grad->nstops == 1) { + unsigned int color = nsvg__applyOpacity(grad->stops[0].color, opacity); for (i = 0; i < 256; i++) - cache->colors[i] = nsvg__applyOpacity(grad->stops[i].color, opacity); + cache->colors[i] = color; } else { unsigned int ca, cb = 0; float ua, ub, du, u; @@ -1406,7 +1410,8 @@ void nsvgRasterize(NSVGrasterizer* r, } // Rasterize edges - qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule nsvg__initPaint(&cache, &shape->fill, shape->opacity); @@ -1432,7 +1437,8 @@ void nsvgRasterize(NSVGrasterizer* r, } // Rasterize edges - qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule nsvg__initPaint(&cache, &shape->stroke, shape->opacity); @@ -1449,4 +1455,6 @@ void nsvgRasterize(NSVGrasterizer* r, r->stride = 0; } -#endif +#endif // NANOSVGRAST_IMPLEMENTATION + +#endif // NANOSVGRAST_H diff --git a/src/image.c b/src/image.c index 87f90a1..df4a7c8 100644 --- a/src/image.c +++ b/src/image.c @@ -10,14 +10,14 @@ #include "image.h" #include "util.h" #include "debug.h" -#include "external/ini.h" +#include #define NANOSVG_IMPLEMENTATION -#include "external/nanosvg.h" +#include #define NANOSVGRAST_IMPLEMENTATION -#include "external/nanosvgrast.h" +#include -extern config_t config; -extern state_t state; +extern Config config; +extern State state; extern SDL_Renderer *renderer; extern SDL_Texture *background_texture; NSVGrasterizer *rasterizer = NULL; @@ -25,337 +25,395 @@ NSVGrasterizer *rasterizer = NULL; // A function to initalize SVG rasterization int init_svg() { - rasterizer = nsvgCreateRasterizer(); - if (rasterizer == NULL) { - output_log(LOGLEVEL_FATAL, "Fatal Error: Could not initialize SVG rasterizer.\n"); - return 1; - } - return 0; + rasterizer = nsvgCreateRasterizer(); + if (rasterizer == NULL) { + log_fatal("Could not initialize SVG rasterizer."); + return 1; + } + return 0; } // A function to quit the SVG subsystem void quit_svg() { - nsvgDeleteRasterizer(rasterizer); + nsvgDeleteRasterizer(rasterizer); } // A function to load the next slideshow background from the struct -SDL_Surface *load_next_slideshow_background(slideshow_t *slideshow, bool transition) +SDL_Surface *load_next_slideshow_background(Slideshow *slideshow, bool transition) { - SDL_Surface *surface = NULL; - int initial_index = slideshow->i; - int attempts = 0; - do { - // Increment slideshow background index and load background - (slideshow->i)++; - if (slideshow->i >= slideshow->num_images) { - slideshow->i = 0; - } - surface = IMG_Load(slideshow->images[slideshow->order[slideshow->i]]); + SDL_Surface *surface = NULL; + int initial_index = slideshow->i; + int attempts = 0; + do { + // Increment slideshow background index and load background + (slideshow->i)++; + if (slideshow->i >= slideshow->num_images) + slideshow->i = 0; + surface = IMG_Load(slideshow->images[slideshow->order[slideshow->i]]); + + // If the loaded image has no alpha channel (e.g. JPEG), create one + // so that we can have transparency for the background transition + if (surface != NULL && surface->format->format == SDL_PIXELFORMAT_RGB24 && transition) { + SDL_Surface *tmp = SDL_CreateRGBSurfaceWithFormat(0, + surface->w, + surface->h, + 32, + SDL_PIXELFORMAT_ARGB8888 + ); + Uint32 color = SDL_MapRGBA(tmp->format, 0, 0, 0, 0xFF); + SDL_FillRect(tmp, NULL, color); + SDL_BlitSurface(surface, NULL, tmp, NULL); + SDL_FreeSurface(surface); + surface = tmp; + attempts++; + } + } while (surface == NULL && slideshow->i != initial_index && attempts < slideshow->num_images); - // If the loaded image has no alpha channel (e.g. JPEG), create one - // so that we can have transparency for the background transition - if (surface != NULL && surface->format->format == SDL_PIXELFORMAT_RGB24 && transition) { - SDL_Surface *tmp = SDL_CreateRGBSurfaceWithFormat(0, - surface->w, - surface->h, - 32, - SDL_PIXELFORMAT_ARGB8888); - Uint32 color = SDL_MapRGBA(tmp->format, 0, 0, 0, 0xFF); - SDL_FillRect(tmp, NULL, color); - SDL_BlitSurface(surface, NULL, tmp, NULL); - SDL_FreeSurface(surface); - surface = tmp; - attempts++; - } - } while (surface == NULL && slideshow->i != initial_index && attempts < slideshow->num_images); - - // Switch to color background mode if we failed to load any image from the array - if (surface == NULL) { - output_log(LOGLEVEL_ERROR, - "Error: Could not load any image from slideshow directory %s\n" - "Changing background to color mode\n", - config.slideshow_directory - ); - quit_slideshow(); - config.background_mode = MODE_COLOR; - set_draw_color(); - } - - // If only one image in the entire slideshow array was valid, switch to - // single image background mode - else if (slideshow->i == initial_index && surface != NULL) { - output_log(LOGLEVEL_ERROR, - "Error: Could only load one image from slideshow directory %s\n" - "Changing background to single image mode\n", - config.slideshow_directory - ); - background_texture = SDL_CreateTextureFromSurface(renderer, surface); - config.background_mode = MODE_IMAGE; - } - return surface; + // Switch to color background mode if we failed to load any image from the array + if (surface == NULL) { + log_error( + "Could not load any image from slideshow directory %s\n" + "Changing background to color mode", + config.slideshow_directory + ); + quit_slideshow(); + config.background_mode = BACKGROUND_COLOR; + set_draw_color(); + } + + // If only one image in the entire slideshow array was valid, switch to + // single image background mode + else if (slideshow->i == initial_index && surface != NULL) { + log_error( + "Could only load one image from slideshow directory %s\n" + "Changing background to single image mode", + config.slideshow_directory + ); + background_texture = SDL_CreateTextureFromSurface(renderer, surface); + config.background_mode = BACKGROUND_IMAGE; + } + return surface; } // A function to load a new slideshow background in a separate thread int load_next_slideshow_background_async(void *data) { - slideshow_t *slideshow = (slideshow_t*) data; - slideshow->transition_surface = load_next_slideshow_background(slideshow, true); - state.slideshow_background_rendering = false; - state.slideshow_background_ready = true; - return 0; + Slideshow *slideshow = (Slideshow*) data; + slideshow->transition_surface = load_next_slideshow_background(slideshow, true); + state.slideshow_background_rendering = false; + state.slideshow_background_ready = true; + return 0; } // A function to load a texture from a file SDL_Texture *load_texture_from_file(const char *path) { - SDL_Surface *surface = NULL; - SDL_Texture *texture = NULL; - if (path != NULL) { - surface = IMG_Load(path); - if (surface == NULL) { - output_log(LOGLEVEL_ERROR, - "Error: Could not load image %s\n%s\n", - path, - IMG_GetError() - ); + SDL_Surface *surface = NULL; + SDL_Texture *texture = NULL; + if (path != NULL) { + surface = IMG_Load(path); + if (surface == NULL) { + log_error( + "Could not load image %s\n%s", + path, + IMG_GetError() + ); + } + else + texture = load_texture(surface); } - else { - texture = load_texture(surface); - } - } - return texture; + return texture; } -// A function to load a texture from a SDL surface +// A function to load a texture from a SDL surface SDL_Texture *load_texture(SDL_Surface *surface) { - SDL_Texture *texture = NULL; - if (surface == NULL) { - return; - } - - //Convert surface to screen format - texture = SDL_CreateTextureFromSurface(renderer, surface); - if (texture == NULL) { - output_log(LOGLEVEL_ERROR, "Error: Could not create texture %s\n", SDL_GetError()); - } - SDL_FreeSurface(surface); - return texture; + SDL_Texture *texture = NULL; + if (surface == NULL) + return NULL; + + //Convert surface to screen format + texture = SDL_CreateTextureFromSurface(renderer, surface); + if (texture == NULL) + log_error("Could not create texture %s", SDL_GetError()); + SDL_FreeSurface(surface); + return texture; } // A function to rasterize an SVG from an existing text buffer -SDL_Texture *rasterize_svg(const char *buffer, int w, int h, SDL_Rect *rect) +SDL_Texture *rasterize_svg(char *buffer, int w, int h, SDL_Rect *rect) { - NSVGimage *image = NULL; - unsigned char *pixel_buffer = NULL; - int width, height, pitch; - float scale; - - // Parse SVG to NSVGimage struct - image = nsvgParse(buffer, "px", 96.0f); - if (image == NULL) { - output_log(LOGLEVEL_ERROR, "Error: could not open SVG image.\n"); - return NULL; - } - - // Calculate scaling and dimensions - if (w == -1 && h == -1) { - scale = 1.0f; - width = (int) image->width; - height = (int) image->height; - } - else if (w == -1 && h != -1) { - scale = (float) h / (float) image->height; - width = (int) ceil((double) image->width * (double) scale); - height = h; - } - else if (w != -1 && h == -1) { - scale = (float) w / (float) image->width; - width = w; - height = (int) ceil((double) image->height * (double) scale); - } - else { - scale = (float) w / (float) image->width; - width = w; - height = h; - } - - // Allocate memory - pitch = 4*width; - pixel_buffer = malloc(4*width*height); - if (pixel_buffer == NULL) { - output_log(LOGLEVEL_ERROR, "Error: Could not alloc SVG pixel buffer.\n"); - return NULL; - } - - // Rasterize image - nsvgRasterize(rasterizer, image, 0, 0, scale, pixel_buffer, width, height, pitch); - SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixel_buffer, - width, - height, - 32, - pitch, - COLOR_MASKS - ); - SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); - if (rect != NULL) { - rect->w = width; - rect->h = height; - } - free(pixel_buffer); - SDL_FreeSurface(surface); - nsvgDelete(image); - return texture; + NSVGimage *image = NULL; + unsigned char *pixel_buffer = NULL; + int width, height, pitch; + float scale; + + // Parse SVG to NSVGimage struct + image = nsvgParse(buffer, "px", 96.0f); + if (image == NULL) { + log_error("could not open SVG image."); + return NULL; + } + + // Calculate scaling and dimensions + if (w == -1 && h == -1) { + scale = 1.0f; + width = (int) image->width; + height = (int) image->height; + } + else if (w == -1 && h != -1) { + scale = (float) h / (float) image->height; + width = (int) ceil((double) image->width * (double) scale); + height = h; + } + else if (w != -1 && h == -1) { + scale = (float) w / (float) image->width; + width = w; + height = (int) ceil((double) image->height * (double) scale); + } + else { + scale = (float) w / (float) image->width; + width = w; + height = h; + } + + // Allocate memory + pitch = 4*width; + pixel_buffer = malloc((size_t) (4*width*height)); + if (pixel_buffer == NULL) { + log_error("Could not alloc SVG pixel buffer."); + return NULL; + } + + // Rasterize image + nsvgRasterize(rasterizer, image, 0, 0, scale, pixel_buffer, width, height, pitch); + SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixel_buffer, + width, + height, + 32, + pitch, + COLOR_MASKS + ); + SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); + if (rect != NULL) { + rect->w = width; + rect->h = height; + } + free(pixel_buffer); + SDL_FreeSurface(surface); + nsvgDelete(image); + return texture; } -// A function to rasterize an SVG from a file -SDL_Texture *rasterize_svg_from_file(const char *path, int w, int h, SDL_Rect *rect) +// A function to render the highlight for the buttons +SDL_Texture *render_highlight(int width, int height, SDL_Rect *rect) { - SDL_Texture *texture = NULL; - char *buffer = NULL; - read_file(path, &buffer); - if (buffer == NULL) { - output_log(LOGLEVEL_ERROR, "Error: Could not read file \"%s\"\n", path); - } - else { - texture = rasterize_svg(buffer, w, h, rect); + // Insert user config variables into SVG-formatted text buffer + char *buffer = NULL; + char *outline_buffer = NULL; + if (config.highlight_outline_size) { + float stroke_opacity = ((float) config.highlight_outline_color.a) / 255.0f; + format_highlight_outline(&outline_buffer, + config.highlight_outline_size, + config.highlight_outline_color, + stroke_opacity + ); + } + else + outline_buffer = ""; + + float fill_opacity = ((float) config.highlight_fill_color.a) / 255.0f; + format_highlight(&buffer, + width, + height, + config.highlight_rx, + config.highlight_fill_color, + fill_opacity, + outline_buffer + ); + + // Rasterize the SVG + SDL_Texture *texture = rasterize_svg(buffer, -1, -1, rect); + + // Cleanup free(buffer); - } - return texture; + if (config.highlight_outline_size) + free(outline_buffer); + + return texture; } -// A function to render the highlight for the buttons -SDL_Texture *render_highlight(int width, int height, unsigned int rx, SDL_Rect *rect) +// A function to render the scroll indicators +void render_scroll_indicators(Scroll *scroll, int height, Geometry *geo) { - // Insert user config variables into SVG-formatted text buffer - char buffer[500]; - sprintf(buffer, SVG_HIGHLIGHT, width, height, width, height, rx); - - // Rasterize the SVG - SDL_Texture *texture = rasterize_svg(buffer, -1, -1, rect); - - // Set color - SDL_SetTextureColorMod(texture, - config.highlight_color.r, - config.highlight_color.g, - config.highlight_color.b); - SDL_SetTextureAlphaMod(texture,config.highlight_color.a); - return texture; + // Format the SVG + char *buffer = NULL; + float opacity = (float) config.scroll_indicator_fill_color.a / 255.0f; + format_scroll_indicator(&buffer, + config.scroll_indicator_fill_color, + config.scroll_indicator_outline_size, + config.scroll_indicator_outline_color, + opacity + ); + + // Rasterize the SVG + scroll->texture = rasterize_svg(buffer, + -1, + height, + &scroll->rect_right + ); + free(buffer); + scroll->rect_left.w = scroll->rect_right.w; + scroll->rect_left.h = scroll->rect_right.h; + if (scroll->texture == NULL) { + log_error("Could not render scroll indicator, disabling feature"); + free(scroll); + config.scroll_indicators = false; + return; + } + + // Calculate screen position + scroll->rect_right.y = geo->screen_height - geo->screen_margin - scroll->rect_right.h; + scroll->rect_right.x = geo->screen_width - geo->screen_margin - scroll->rect_right.w; + scroll->rect_left.y = scroll->rect_right.y; + scroll->rect_left.x = geo->screen_margin; } -// A function to render title text for an entry -SDL_Surface *render_text(const char *text, text_info_t *info, SDL_Rect *rect, int *text_height) +// A function to render text +SDL_Surface *render_text(const char *text, TextInfo *info, SDL_Rect *rect, int *text_height) { - TTF_Font *output_font = NULL; - TTF_Font *reduced_font = NULL; // Font for Shrink text oversize mode - int w, h; + TTF_Font *output_font = NULL; + TTF_Font *reduced_font = NULL; // Font for Shrink text oversize mode + int w, h; + + // Copy text into new buffer in case we need to manipulate it + char *text_buffer = strdup(text); + + // Calculate size of the rendered title + TTF_SizeUTF8(info->font, text_buffer, &w, &h); - // Copy text into new buffer in case we need to manipulate it - char *text_buffer; - copy_string(&text_buffer, text); + // If title is too large to fit + if (info->oversize_mode != OVERSIZE_NONE && w > info->max_width) { - // Calculate size of the rendered title - int title_length = strlen(text_buffer); - TTF_SizeUTF8(info->font, text_buffer, &w, &h); + // Truncate mode: + if (info->oversize_mode == OVERSIZE_TRUNCATE) { + utf8_truncate(text_buffer, w, info->max_width); + TTF_SizeUTF8(info->font, text_buffer, &w, &h); + } - // If title is too large to fit - if (info->oversize_mode != MODE_TEXT_NONE && w > info->max_width) { + // Shrink mode: + else if (info->oversize_mode == OVERSIZE_SHRINK) { + int reduced_font_size = (int) info->font_size - 1; + reduced_font = TTF_OpenFont(*info->font_path, reduced_font_size); + TTF_SizeUTF8(reduced_font, text_buffer, &w, &h); + + // Keep trying smaller font until it fits + while (w > info->max_width && reduced_font_size > 0) { + TTF_CloseFont(reduced_font); + reduced_font = NULL; + reduced_font_size--; + reduced_font = TTF_OpenFont(*info->font_path, reduced_font_size); + TTF_SizeUTF8(reduced_font, text_buffer, &w, &h); + } + + if (reduced_font_size) + output_font = reduced_font; + else + reduced_font = NULL; + } + } + if (reduced_font == NULL) + output_font = info->font; - // Truncate mode: - if (info->oversize_mode == MODE_TEXT_TRUNCATE) { - utf8_truncate(text_buffer, w, info->max_width); - TTF_SizeUTF8(info->font, text_buffer, &w, &h); + // Render surface + SDL_Surface *surface = NULL; + if (info->shadow) { + int shadow_offset = h / 40; + if (shadow_offset < 2) + shadow_offset = 2; + SDL_Surface *foreground = TTF_RenderUTF8_Blended(output_font, + text_buffer, + *info->color + ); + SDL_Surface *shadow = TTF_RenderUTF8_Blended(output_font, + text_buffer, + *info->shadow_color + ); + surface = SDL_CreateRGBSurfaceWithFormat(0, + foreground->w + shadow_offset, + foreground->h + shadow_offset, + 32, + SDL_PIXELFORMAT_ARGB8888 + ); + Uint32 color = SDL_MapRGBA(surface->format, 0, 0, 0, 0); + SDL_FillRect(surface, NULL, color); + SDL_Rect shadow_rect = {shadow_offset, shadow_offset, shadow->w, shadow->h}; + SDL_BlitSurface(shadow, NULL, surface, &shadow_rect); + SDL_Rect rect = {0, 0, foreground->w, foreground->h}; + SDL_BlitSurface(foreground, NULL, surface, &rect); + SDL_FreeSurface(foreground); + SDL_FreeSurface(shadow); } + else + surface = TTF_RenderUTF8_Blended(output_font, + text_buffer, + *info->color + ); - // Shrink mode: - else if (info->oversize_mode == MODE_TEXT_SHRINK) { - int reduced_font_size = info->font_size - 1; - reduced_font = TTF_OpenFont(*info->font_path, reduced_font_size); - TTF_SizeUTF8(reduced_font, text_buffer, &w, &h); + // Set geometry + rect->w = surface->w; + rect->h = surface->h; + if (info->oversize_mode == OVERSIZE_SHRINK && text_height != NULL) + *text_height = h; - // Keep trying smaller font until it fits - while (w > info->max_width && reduced_font_size > 0) { + // Clean up + if (reduced_font != NULL) TTF_CloseFont(reduced_font); - reduced_font = NULL; - reduced_font_size--; - reduced_font = TTF_OpenFont(*info->font_path, reduced_font_size); - TTF_SizeUTF8(reduced_font, text_buffer, &w, &h); - } - - // Set vertical offset so reduced font title remains vertically centered - // with other titles - if (reduced_font_size) { - output_font = reduced_font; - } - else { - reduced_font = NULL; - } - } - } - - // Set geometry - rect->w = w; - rect->h = h; - if (info->oversize_mode == MODE_TEXT_SHRINK && text_height != NULL) { - *text_height = h; - } - if (reduced_font == NULL) { - output_font = info->font; - } - - // Render texture - SDL_Surface *surface = TTF_RenderUTF8_Blended(output_font, - text_buffer, - *info->color - ); - if (reduced_font != NULL) { - TTF_CloseFont(reduced_font); - } - free(text_buffer); - return surface; + free(text_buffer); + + return surface; } // A function to render text into a texture -SDL_Texture *render_text_texture(const char *text, text_info_t *info, SDL_Rect *rect, int *text_height) +SDL_Texture *render_text_texture(const char *text, TextInfo *info, SDL_Rect *rect, int *text_height) { - SDL_Surface *surface = render_text(text, info, rect, text_height); - return load_texture(surface); + SDL_Surface *surface = render_text(text, info, rect, text_height); + return load_texture(surface); } // A function to load a font from a file -int load_font(text_info_t *info, const char *default_font) +int load_font(TextInfo *info, const char *default_font) { - char *font_path = *info->font_path; - // Load user specified font - if (font_path != NULL) { - info->font = TTF_OpenFont(font_path, info->font_size); - } - - // Try to load default font if we failed loading from config file - if (info->font == NULL) { - output_log(LOGLEVEL_ERROR, "Error: Could not initialize font from config file\n"); - char *prefixes[2]; - char fonts_exe_buffer[MAX_PATH_CHARS + 1]; - prefixes[0] = join_paths(fonts_exe_buffer, 3, config.exe_path, PATH_ASSETS_EXE, PATH_FONTS_EXE); - #ifdef __unix__ - prefixes[1] = PATH_FONTS_SYSTEM; - #else - prefixes[1] = PATH_FONTS_RELATIVE; - #endif - char *default_font_path = find_file(default_font, 2, prefixes); - - // Replace user font with default in config - if (default_font_path != NULL) { - info->font = TTF_OpenFont(default_font_path, info->font_size); - free(font_path); - copy_string(info->font_path, default_font_path); - free(default_font_path); - } + char *font_path = *info->font_path; + // Load user specified font + if (font_path != NULL) + info->font = TTF_OpenFont(font_path, info->font_size); + + // Try to load default font if we failed loading from config file if (info->font == NULL) { - output_log(LOGLEVEL_FATAL, "Fatal Error: Could not load default font\n"); - return 1; + log_error("Could not initialize font from config file"); + const char *prefixes[2]; + char fonts_exe_buffer[MAX_PATH_CHARS + 1]; + prefixes[0] = join_paths(fonts_exe_buffer, sizeof(fonts_exe_buffer), 3, config.exe_path, PATH_ASSETS_EXE, PATH_FONTS_EXE); +#ifdef __unix__ + prefixes[1] = PATH_FONTS_SYSTEM; +#else + prefixes[1] = PATH_FONTS_RELATIVE; +#endif + char *default_font_path = find_file(default_font, 2, prefixes); + + // Replace user font with default in config + if (default_font_path != NULL) { + info->font = TTF_OpenFont(default_font_path, info->font_size); + free(font_path); + *(info->font_path) = strdup(default_font_path); + free(default_font_path); + } + if (info->font == NULL) { + log_fatal("Could not load default font"); + return 1; + } } - } - return 0; -} \ No newline at end of file + return 0; +} diff --git a/src/image.h b/src/image.h index d6661ff..b32d612 100644 --- a/src/image.h +++ b/src/image.h @@ -1,24 +1,36 @@ // Dynamic SVG generation for highlight -#define SVG_HIGHLIGHT "" +#define HIGHLIGHT_OUTLINE_FORMAT " stroke-width=\"%i\" stroke=\"#%02X%02X%02X\" stroke-opacity=\"%.2f\"" +#define HIGHLIGHT_FORMAT "" +#define SCROLL_INDICATOR_FORMAT " " +#define SHADOW_OPACITY_MULTIPLIER 0.75F + +// Macro functions +#define format_highlight_outline(buffer, outline_size, outline_color, outline_opacity) sprintf_alloc(buffer, HIGHLIGHT_OUTLINE_FORMAT, outline_size, outline_color.r, outline_color.g, outline_color.b, outline_opacity) +#define format_highlight(buffer, width, height, corner_radius, fill_color, fill_opacity, outline_buffer) sprintf_alloc(buffer, HIGHLIGHT_FORMAT, width, height, width, height, corner_radius, fill_color.r, fill_color.g, fill_color.b, fill_opacity, outline_buffer) +#define format_scroll_indicator(buffer, fill_color, outline_size, outline_color, opacity) sprintf_alloc(buffer, SCROLL_INDICATOR_FORMAT, fill_color.r, fill_color.g, fill_color.b, opacity, outline_color.r, outline_color.g, outline_color.b, outline_size, opacity) +#define calculate_shadow_alpha(x) x.shadow_color->a = (Uint8) (SHADOW_OPACITY_MULTIPLIER * (float) x.color->a) typedef struct { - TTF_Font *font; - int font_size; - const char **font_path; - SDL_Color *color; - int max_width; - mode oversize_mode; -} text_info_t; + TTF_Font *font; + int font_size; + char **font_path; + SDL_Color *color; + bool shadow; + SDL_Color *shadow_color; + int max_width; + ModeOversize oversize_mode; +} TextInfo; int init_svg(void); -int load_font(text_info_t *info, const char *default_font); +int load_font(TextInfo *info, const char *default_font); void quit_svg(void); -SDL_Surface *load_next_slideshow_background(slideshow_t *slideshow, bool transition); +void render_scroll_indicators(Scroll *scroll, int height, Geometry *geo); +SDL_Surface *load_next_slideshow_background(Slideshow *slideshow, bool transition); int load_next_slideshow_background_async(void *data); SDL_Texture *load_texture(SDL_Surface *surface); SDL_Texture *load_texture_from_file(const char *path); -SDL_Texture *rasterize_svg(const char *buffer, int w, int h, SDL_Rect *rect); +SDL_Texture *rasterize_svg(char *buffer, int w, int h, SDL_Rect *rect); SDL_Texture *rasterize_svg_from_file(const char *path, int w, int h, SDL_Rect *rect); -SDL_Texture *render_highlight(int width, int height, unsigned int rx, SDL_Rect *rect); -SDL_Surface *render_text(const char *text, text_info_t *info, SDL_Rect *rect, int *text_height); -SDL_Texture *render_text_texture(const char *text, text_info_t *info, SDL_Rect *rect, int *text_height); \ No newline at end of file +SDL_Texture *render_highlight(int width, int height, SDL_Rect *rect); +SDL_Surface *render_text(const char *text, TextInfo *info, SDL_Rect *rect, int *text_height); +SDL_Texture *render_text_texture(const char *text, TextInfo *info, SDL_Rect *rect, int *text_height); diff --git a/src/launcher.c b/src/launcher.c index 718eb61..f31763a 100755 --- a/src/launcher.c +++ b/src/launcher.c @@ -16,1277 +16,1440 @@ #include "clock.h" #include "platform/platform.h" +static void init_sdl(void); +static void init_sdl_image(void); +static void create_window(void); +static void init_sdl_ttf(void); +static int load_menu(Menu *menu, bool set_back_menu, bool reset_position); +static int load_menu_by_name(const char *menu_name, bool set_back_menu, bool reset_position); +static void update_slideshow(void); +static void resume_slideshow(void); +static void update_screensaver(void); +static void update_clock(bool block); +static void init_slideshow(void); +static void init_screensaver(void); +static void calculate_button_geometry(Entry *entry, int buttons); +static void render_buttons(Menu *menu); +static void move_left(void); +static void move_right(void); +static void load_submenu(const char *submenu); +static void load_back_menu(Menu *menu); +static void draw_screen(void); +static void handle_keypress(SDL_Keysym *key); +static void execute_command(const char *command); +static void poll_gamepad(void); +static void init_gamepad(Gamepad **gamepad, int device_index); +static void connect_gamepad(int device_index, bool open, bool raise_error); +static void disconnect_gamepad(int id, bool disconnect, bool remove); +static void open_controller(Gamepad *gamepad, bool raise_error); +static void cleanup(void); + // Initialize default settings -config_t config = { - .background_image = NULL, - .slideshow_directory = NULL, - .title_font_path = NULL, - .font_size = DEFAULT_FONT_SIZE, - .title_color.r = DEFAULT_TITLE_COLOR_R, - .title_color.g = DEFAULT_TITLE_COLOR_G, - .title_color.b = DEFAULT_TITLE_COLOR_B, - .title_color.a = DEFAULT_TITLE_COLOR_A, - .background_mode = MODE_COLOR, - .background_color.r = DEFAULT_BACKGROUND_COLOR_R, - .background_color.g = DEFAULT_BACKGROUND_COLOR_G, - .background_color.b = DEFAULT_BACKGROUND_COLOR_B, - .background_color.a = 0xFF, - .icon_size = DEFAULT_ICON_SIZE, - .default_menu = NULL, - .highlight_color.r = DEFAULT_HIGHLIGHT_COLOR_R, - .highlight_color.g = DEFAULT_HIGHLIGHT_COLOR_G, - .highlight_color.b = DEFAULT_HIGHLIGHT_COLOR_B, - .highlight_color.a = DEFAULT_HIGHLIGHT_COLOR_A, - .highlight_rx = DEFAULT_HIGHLIGHT_CORNER_RADIUS, - .title_padding = -1, - .max_buttons = DEFAULT_MAX_BUTTONS, - .icon_spacing = -1, - .highlight_vpadding = -1, - .highlight_hpadding = -1, - .title_opacity[0] = '\0', - .highlight_opacity[0] = '\0', - .button_centerline[0] = '\0', - .icon_spacing_str[0] = '\0', - .scroll_indicators = DEFAULT_SCROLL_INDICATORS, - .scroll_indicator_color.r = DEFAULT_SCROLL_INDICATOR_COLOR_R, - .scroll_indicator_color.g = DEFAULT_SCROLL_INDICATOR_COLOR_G, - .scroll_indicator_color.b = DEFAULT_SCROLL_INDICATOR_COLOR_B, - .scroll_indicator_color.a = DEFAULT_SCROLL_INDICATOR_COLOR_A, - .scroll_indicator_opacity[0] = '\0', - .title_oversize_mode = MODE_TEXT_TRUNCATE, - .reset_on_back = DEFAULT_RESET_ON_BACK, - .screensaver_enabled = false, - .screensaver_idle_time = DEFAULT_SCREENSAVER_IDLE_TIME*1000, - .screensaver_intensity_str[0] = '\0', - .screensaver_pause_slideshow = DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW, - .gamepad_enabled = DEFAULT_GAMEPAD_ENABLED, - .gamepad_device = DEFAULT_GAMEPAD_DEVICE, - .gamepad_mappings_file = NULL, - .on_launch = MODE_ON_LAUNCH_HIDE, - .debug = false, - .exe_path = NULL, - .first_menu = NULL, - .num_menus = 0, - .clock_enabled = DEFAULT_CLOCK_ENABLED, - .clock_show_date = DEFAULT_CLOCK_SHOW_DATE, - .clock_alignment = DEFAULT_CLOCK_ALIGNMENT, - .clock_font_path = NULL, - .clock_margin_str[0] = '\0', - .clock_margin = -1, - .clock_color.r = DEFAULT_CLOCK_COLOR_R, - .clock_color.g = DEFAULT_CLOCK_COLOR_G, - .clock_color.b = DEFAULT_CLOCK_COLOR_B, - .clock_color.a = DEFAULT_CLOCK_COLOR_A, - .clock_opacity[0] = '\0', - .clock_font_size = DEFAULT_CLOCK_FONT_SIZE, - .clock_time_format = DEFAULT_CLOCK_TIME_FORMAT, - .clock_date_format = DEFAULT_CLOCK_DATE_FORMAT, - .clock_include_weekday = DEFAULT_CLOCK_INCLUDE_WEEKDAY, - .slideshow_image_duration = DEFAULT_SLIDESHOW_IMAGE_DURATION, - .slideshow_transition_time = DEFAULT_SLIDESHOW_TRANSITION_TIME +Config config = { + .default_menu = NULL, + .background_image = NULL, + .slideshow_directory = NULL, + .title_font_path = NULL, + .vsync = true, + .fps_limit = -1, + .application_timeout = DEFAULT_APPLICATION_TIMEOUT * 1000, + .titles_enabled = DEFAULT_TITLES_ENABLED, + .title_font_size = DEFAULT_FONT_SIZE, + .title_font_color.r = DEFAULT_TITLE_FONT_COLOR_R, + .title_font_color.g = DEFAULT_TITLE_FONT_COLOR_G, + .title_font_color.b = DEFAULT_TITLE_FONT_COLOR_B, + .title_font_color.a = DEFAULT_TITLE_FONT_COLOR_A, + .title_shadows = DEFAULT_TITLE_SHADOWS, + .title_shadow_color.r = DEFAULT_TITLE_SHADOW_COLOR_R, + .title_shadow_color.g = DEFAULT_TITLE_SHADOW_COLOR_G, + .title_shadow_color.b = DEFAULT_TITLE_SHADOW_COLOR_B, + .title_shadow_color.a = DEFAULT_TITLE_SHADOW_COLOR_A, + .background_mode = BACKGROUND_COLOR, + .background_color.r = DEFAULT_BACKGROUND_COLOR_R, + .background_color.g = DEFAULT_BACKGROUND_COLOR_G, + .background_color.b = DEFAULT_BACKGROUND_COLOR_B, + .chroma_key_color.r = DEFAULT_CHROMA_KEY_COLOR_R, + .chroma_key_color.g = DEFAULT_CHROMA_KEY_COLOR_G, + .chroma_key_color.b = DEFAULT_CHROMA_KEY_COLOR_B, + .chroma_key_color.a = DEFAULT_CHROMA_KEY_COLOR_A, + .background_color.a = 0xFF, + .background_overlay = DEFAULT_BACKGROUND_OVERLAY, + .background_overlay_color.r = DEFAULT_BACKGROUND_OVERLAY_COLOR_R, + .background_overlay_color.g = DEFAULT_BACKGROUND_OVERLAY_COLOR_G, + .background_overlay_color.b = DEFAULT_BACKGROUND_OVERLAY_COLOR_B, + .background_overlay_color.a = DEFAULT_BACKGROUND_OVERLAY_COLOR_A, + .background_overlay_opacity[0] = '\0', + .highlight = true, + .icon_size = DEFAULT_ICON_SIZE, + .highlight_fill_color.r = DEFAULT_HIGHLIGHT_FILL_COLOR_R, + .highlight_fill_color.g = DEFAULT_HIGHLIGHT_FILL_COLOR_G, + .highlight_fill_color.b = DEFAULT_HIGHLIGHT_FILL_COLOR_B, + .highlight_fill_color.a = DEFAULT_HIGHLIGHT_FILL_COLOR_A, + .highlight_outline_color.r = DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R, + .highlight_outline_color.g = DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G, + .highlight_outline_color.b = DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B, + .highlight_outline_color.a = DEFAULT_HIGHLIGHT_OUTLINE_COLOR_A, + .highlight_outline_size = DEFAULT_HIGHLIGHT_OUTLINE_SIZE, + .highlight_rx = DEFAULT_HIGHLIGHT_CORNER_RADIUS, + .title_padding = -1, + .max_buttons = DEFAULT_MAX_BUTTONS, + .icon_spacing = -1, + .highlight_vpadding = -1, + .highlight_hpadding = -1, + .title_opacity[0] = '\0', + .highlight_fill_opacity[0] = '\0', + .highlight_outline_opacity[0] = '\0', + .vcenter[0] = '\0', + .icon_spacing_str[0] = '\0', + .scroll_indicators = DEFAULT_SCROLL_INDICATORS, + .scroll_indicator_fill_color.r = DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R, + .scroll_indicator_fill_color.g = DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G, + .scroll_indicator_fill_color.b = DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B, + .scroll_indicator_fill_color.a = DEFAULT_SCROLL_INDICATOR_FILL_COLOR_A, + .scroll_indicator_outline_size = DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE, + .scroll_indicator_outline_color.r = DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R, + .scroll_indicator_outline_color.g = DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G, + .scroll_indicator_outline_color.b = DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B, + .scroll_indicator_outline_color.a = DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_A, + .scroll_indicator_opacity[0] = '\0', + .title_oversize_mode = OVERSIZE_TRUNCATE, + .wrap_entries = DEFAULT_WRAP_ENTRIES, + .reset_on_back = DEFAULT_RESET_ON_BACK, + .mouse_select = DEFAULT_MOUSE_SELECT, + .inhibit_os_screensaver = DEFAULT_INHIBIT_OS_SCREENSAVER, + .startup_cmd = NULL, + .quit_cmd = NULL, + .screensaver_enabled = false, + .screensaver_idle_time = DEFAULT_SCREENSAVER_IDLE_TIME*1000, + .screensaver_intensity_str[0] = '\0', + .screensaver_pause_slideshow = DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW, + .gamepad_enabled = DEFAULT_GAMEPAD_ENABLED, + .gamepad_device = DEFAULT_GAMEPAD_DEVICE, + .gamepad_mappings_file = NULL, + .on_launch = ON_LAUNCH_BLANK, + .debug = false, + .exe_path = NULL, + .first_menu = NULL, + .num_menus = 0, + .clock_enabled = DEFAULT_CLOCK_ENABLED, + .clock_show_date = DEFAULT_CLOCK_SHOW_DATE, + .clock_alignment = DEFAULT_CLOCK_ALIGNMENT, + .clock_font_path = NULL, + .clock_margin_str[0] = '\0', + .clock_margin = -1, + .clock_font_color.r = DEFAULT_CLOCK_FONT_COLOR_R, + .clock_font_color.g = DEFAULT_CLOCK_FONT_COLOR_G, + .clock_font_color.b = DEFAULT_CLOCK_FONT_COLOR_B, + .clock_font_color.a = DEFAULT_CLOCK_FONT_COLOR_A, + .clock_shadows = DEFAULT_CLOCK_SHADOWS, + .clock_shadow_color.r = DEFAULT_CLOCK_SHADOW_COLOR_R, + .clock_shadow_color.g = DEFAULT_CLOCK_SHADOW_COLOR_G, + .clock_shadow_color.b = DEFAULT_CLOCK_SHADOW_COLOR_B, + .clock_shadow_color.a = DEFAULT_CLOCK_SHADOW_COLOR_A, + .clock_opacity[0] = '\0', + .clock_font_size = DEFAULT_CLOCK_FONT_SIZE, + .clock_time_format = DEFAULT_CLOCK_TIME_FORMAT, + .clock_date_format = DEFAULT_CLOCK_DATE_FORMAT, + .clock_include_weekday = DEFAULT_CLOCK_INCLUDE_WEEKDAY, + .slideshow_image_duration = DEFAULT_SLIDESHOW_IMAGE_DURATION, + .slideshow_transition_time = DEFAULT_SLIDESHOW_TRANSITION_TIME }; -state_t state = { - .screen_updates = false, - .slideshow_transition = false, - .slideshow_background_ready = false, - .slideshow_paused = false, - .screensaver_active = false, - .screensaver_transition = false, - .clock_rendering = false, - .clock_ready = false -}; +// Initialize default states +State state = { false }; // Global variables -SDL_Window *window = NULL; -SDL_Renderer *renderer = NULL; +SDL_Window *window = NULL; +SDL_Renderer *renderer = NULL; +SDL_Texture *background_texture = NULL; +SDL_Texture *background_overlay = NULL; +Menu *default_menu = NULL; +Menu *current_menu = NULL; +Entry *current_entry = NULL; +Highlight *highlight = NULL; +Scroll *scroll = NULL; +Slideshow *slideshow = NULL; +Screensaver *screensaver = NULL; +FILE *log_file = NULL; +Gamepad *gamepads = NULL; +GamepadControl *gamepad_controls = NULL; +Hotkey *hotkeys = NULL; +Clock *clk = NULL; +TTF_Font *clock_font = NULL; +SDL_Thread *Slideshowhread = NULL; +SDL_Thread *clock_thread = NULL; +SDL_Event event; SDL_SysWMinfo wm_info; SDL_DisplayMode display_mode; -SDL_RWops *log_file = NULL; -text_info_t title_info; -launcher_clock_t *launcher_clock = NULL; -TTF_Font *clock_font = NULL; -menu_t *default_menu = NULL; -menu_t *current_menu = NULL; // Current selected menu -entry_t *current_entry = NULL; // Current selected entry -gamepad_control_t *gamepad_controls = NULL; -hotkey_t *hotkeys = NULL; -ticks_t ticks; -SDL_GameController *gamepad = NULL; -SDL_Thread *slideshow_thread = NULL; -SDL_Thread *clock_thread = NULL; -int delay_period; -int repeat_period; -scroll_t *scroll = NULL; -slideshow_t *slideshow = NULL; -screensaver_t *screensaver = NULL; -SDL_Texture *background_texture = NULL; // Background texture (image only) -highlight_t *highlight = NULL; // Pointer containing highlight texture and coordinates -geometry_t geo; // Struct containing screen geometry for the current page of buttons +TextInfo title_info; +Ticks ticks; +Geometry geo; +Uint32 refresh_period; +Uint32 delay_period; +Uint32 repeat_period; + // A function to initialize SDL -static int init_sdl() -{ - // Set flags, hints - int sdl_flags = SDL_INIT_VIDEO; - int img_flags = IMG_INIT_PNG | IMG_INIT_JPG | IMG_INIT_WEBP; - #ifdef __unix__ - SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); - #endif - SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY,"1"); - if (config.gamepad_enabled) { - sdl_flags |= SDL_INIT_GAMECONTROLLER; - delay_period = GAMEPAD_REPEAT_DELAY / POLLING_PERIOD; - repeat_period = GAMEPAD_REPEAT_INTERVAL / POLLING_PERIOD; - } - - // Initialize SDL - if (SDL_Init(sdl_flags) < 0) - { - output_log(LOGLEVEL_FATAL, - "Fatal Error: Could not initialize SDL\n%s\n", - SDL_GetError() - ); - return 1; - } - - // Create window, hide mouse cursor - window = SDL_CreateWindow(PROJECT_NAME, - SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, - 0, - 0, - SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS - ); - if (window == NULL) { - output_log(LOGLEVEL_FATAL, - "Fatal Error: Could not create SDL Window\n%s\n", - SDL_GetError() - ); - return 1; - } - SDL_ShowCursor(SDL_DISABLE); - - // Create HW accelerated renderer, get screen resolution for geometry calculations - renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); - SDL_GetCurrentDisplayMode(0, &display_mode); - geo.screen_width = display_mode.w; - geo.screen_height = display_mode.h; - geo.screen_margin = (int) (SCREEN_MARGIN * (float) geo.screen_height); - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - if (renderer == NULL) { - output_log(LOGLEVEL_FATAL, - "Fatal Error: Could not initialize renderer\n%s\n", - SDL_GetError() - ); - return 1; - } - - // Set background color - set_draw_color(); - - // Initialize SDL_image - if (!(IMG_Init(img_flags) & img_flags)) { - output_log(LOGLEVEL_FATAL, - "Fatal Error: Could not initialize SDL_image\n%s\n", - IMG_GetError() - ); - return 1; - } - #ifdef _WIN32 - SDL_VERSION(&wm_info.version); - SDL_GetWindowWMInfo(window, &wm_info); - #endif - return 0; +static void init_sdl() +{ + // Set flags, hints + Uint32 sdl_flags = SDL_INIT_VIDEO; +#ifdef __unix__ + SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); +#endif + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); + SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, config.inhibit_os_screensaver ? "0" : "1"); + if (config.gamepad_enabled) + sdl_flags |= SDL_INIT_GAMECONTROLLER; + + // Initialize SDL + if (SDL_Init(sdl_flags) < 0) + log_fatal("Could not initialize SDL\n%s", SDL_GetError()); + + SDL_GetDesktopDisplayMode(0, &display_mode); + geo.screen_width = display_mode.w; + geo.screen_height = display_mode.h; + refresh_period = 1000 / (Uint32) display_mode.refresh_rate; + geo.screen_margin = (int) (SCREEN_MARGIN * (float) geo.screen_height); +} + +// A function to create the window and renderer +static void create_window() +{ + window = SDL_CreateWindow(PROJECT_NAME, + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + 0, + 0, + SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS + ); + if (window == NULL) + log_fatal("Could not create SDL Window\n%s", SDL_GetError()); + SDL_ShowCursor(SDL_DISABLE); + + // Create HW accelerated renderer, get screen resolution for geometry calculations + Uint32 renderer_flags = SDL_RENDERER_ACCELERATED; + if (!config.vsync) { + if (config.fps_limit > MIN_FPS_LIMIT && config.fps_limit <= display_mode.refresh_rate) + refresh_period = 1000 / (Uint32) config.fps_limit; + else + config.vsync = true; + } + if (config.vsync) { + refresh_period = 1000 / (Uint32) display_mode.refresh_rate; + renderer_flags |= SDL_RENDERER_PRESENTVSYNC; + } + if (config.gamepad_enabled) { + delay_period = GAMEPAD_REPEAT_DELAY / refresh_period; + repeat_period = GAMEPAD_REPEAT_INTERVAL / refresh_period; + if (!repeat_period) + repeat_period = 1; + } + if (slideshow != NULL) + slideshow->transition_change_rate = 255.0f / ((float) config.slideshow_transition_time / (float) refresh_period); + + renderer = SDL_CreateRenderer(window, -1, renderer_flags); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + if (renderer == NULL) + log_fatal("Could not initialize renderer\n%s", SDL_GetError()); + + // Set background color + set_draw_color(); + +#ifdef _WIN32 + SDL_VERSION(&wm_info.version); + SDL_GetWindowWMInfo(window, &wm_info); + if (config.background_mode == BACKGROUND_TRANSPARENT) + make_window_transparent(); +#endif +} + +// A function to initialize the SDL_image library +static void init_sdl_image() +{ + int img_flags = IMG_INIT_PNG | IMG_INIT_JPG | IMG_INIT_WEBP; + if (!(IMG_Init(img_flags) & img_flags)) + log_fatal("Could not initialize SDL_image\n%s", IMG_GetError()); } // A function to set the color of the renderer void set_draw_color() { - if (config.background_mode == MODE_COLOR) { - SDL_SetRenderDrawColor(renderer, - config.background_color.r, - config.background_color.g, - config.background_color.b, - config.background_color.a - ); - } - else { - SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); - } + SDL_Color *color = NULL; + if (config.background_mode == BACKGROUND_COLOR) + color = &config.background_color; + else if (config.background_mode == BACKGROUND_TRANSPARENT) + color = &config.chroma_key_color; + + if (color == NULL) + SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); + else + SDL_SetRenderDrawColor(renderer, + color->r, + color->g, + color->b, + color->a + ); } // A function to initialize SDL's TTF subsystem -static int init_ttf() +static void init_sdl_ttf() { - // Initialize SDL_ttf - if (TTF_Init() == -1) { - output_log(LOGLEVEL_FATAL, - "Fatal Error: Could not initialize SDL_ttf\n%s\n", - TTF_GetError() - ); - return 1; - } - - title_info.font_size = config.font_size; - title_info.font_path = &config.title_font_path; - title_info.max_width = config.icon_size, - title_info.oversize_mode = config.title_oversize_mode; - title_info.color = &config.title_color; - - int error = load_font(&title_info, FILENAME_DEFAULT_FONT); - if (error) { - return error; - } - - TTF_SizeUTF8(title_info.font, "TEST STRING", NULL, &geo.font_height); - return 0; + if (TTF_Init() == -1) + log_fatal("Could not initialize SDL_ttf\n%s", TTF_GetError()); + + title_info = (TextInfo) { + .font_size = (int) config.title_font_size, + .shadow = config.title_shadows, + .font_path = &config.title_font_path, + .max_width = config.icon_size, + .oversize_mode = config.title_oversize_mode, + .color = &config.title_font_color + }; + if (config.title_shadows) { + title_info.shadow_color = &config.title_shadow_color; + calculate_shadow_alpha(title_info); + } + else + title_info.shadow_color = NULL; + + int error = load_font(&title_info, FILENAME_DEFAULT_FONT); + if (error) + log_fatal("Could not load title font"); + geo.font_height = config.titles_enabled ? TTF_FontHeight(title_info.font) : 0; } // A function to close subsystems and free memory before quitting static void cleanup() { - // Wait until all threads have completed - SDL_WaitThread(slideshow_thread, NULL); - SDL_WaitThread(clock_thread, NULL); - - // Destroy renderer and window - if (renderer != NULL) { - SDL_DestroyRenderer(renderer); - renderer = NULL; - } - if (window != NULL) { - SDL_DestroyWindow(window); - window = NULL; - } - - // Quit subsystems - SDL_Quit(); - IMG_Quit(); - TTF_Quit(); - quit_svg(); - if (config.background_mode == MODE_SLIDESHOW) { - quit_slideshow(); - } - - // Close log file if open - if (log_file != NULL) { - SDL_RWclose(log_file); - } - - // Free dynamically allocated memory - free(config.default_menu); - free(config.background_image); - free(config.title_font_path); - free(config.exe_path); - free(config.slideshow_directory); - free(config.clock_font_path); - free(config.gamepad_mappings_file); - free(highlight); - free(scroll); - free(screensaver); - free(launcher_clock); - - // Free menu and entry linked lists - entry_t *entry = NULL; - entry_t *tmp_entry = NULL; - menu_t *menu = config.first_menu; - menu_t *tmp_menu = NULL; - for (int i = 0; i < config.num_menus; i++) { - free(menu->name); - entry = menu->first_entry; - for(int j = 0; j < menu->num_entries; j++) { - free(entry->title); - free(entry->icon_path); - free(entry->cmd); - tmp_entry = entry; - entry = entry->next; - free(tmp_entry); + // Wait until all threads have completed + SDL_WaitThread(Slideshowhread, NULL); + SDL_WaitThread(clock_thread, NULL); + + // Destroy renderer and window + if (renderer != NULL) { + SDL_DestroyRenderer(renderer); + renderer = NULL; + } + if (window != NULL) { + SDL_DestroyWindow(window); + window = NULL; + } + + // Quit subsystems + SDL_Quit(); + IMG_Quit(); + TTF_Quit(); + quit_svg(); + if (config.background_mode == BACKGROUND_SLIDESHOW) + quit_slideshow(); + + // Close log file if open + if (log_file != NULL) + fclose(log_file); + + // Free dynamically allocated memory + free(config.default_menu); + free(config.background_image); + free(config.title_font_path); + free(config.exe_path); + free(config.slideshow_directory); + free(config.clock_font_path); + free(config.gamepad_mappings_file); + free(config.startup_cmd); + free(config.quit_cmd); + free(highlight); + free(scroll); + free(screensaver); + free(clk); + + // Free menu and entry linked lists + Entry *entry = NULL; + Entry *tmp_entry = NULL; + Menu *menu = config.first_menu; + Menu *tmp_menu = NULL; + for (size_t i = 0; i < config.num_menus; i++) { + free(menu->name); + entry = menu->first_entry; + for(size_t j = 0; j < menu->num_entries; j++) { + free(entry->title); + free(entry->icon_path); + free(entry->icon_selected_path); + free(entry->cmd); + tmp_entry = entry; + entry = entry->next; + free(tmp_entry); + } + tmp_menu = menu; + menu = menu->next; + free(tmp_menu); + } + + // Free hotkey linked list + Hotkey *tmp_hotkey = NULL; + for(Hotkey *i = hotkeys; i != NULL; i = i->next) { + free(tmp_hotkey); + free(i->cmd); + tmp_hotkey = i; } - tmp_menu = menu; - menu = menu->next; - free(tmp_menu); - } - - // Free hotkey linked list - hotkey_t *tmp_hotkey = NULL; - for(hotkey_t *i = hotkeys; i != NULL; i = i->next) { free(tmp_hotkey); - free(i->cmd); - tmp_hotkey = i; - } - free(tmp_hotkey); - - // Free gamepad control linked list - gamepad_control_t *tmp_gamepad = NULL; - for (gamepad_control_t *i = gamepad_controls; i != NULL; i = i->next) { + + // Free gamepad control linked list + GamepadControl *tmp_gamepad = NULL; + for (GamepadControl *i = gamepad_controls; i != NULL; i = i->next) { + free(tmp_gamepad); + free(i->cmd); + tmp_gamepad = i; + } free(tmp_gamepad); - free(i->label); - free(i->cmd); - tmp_gamepad = i; - } - free(tmp_gamepad); + + if (config.gamepad_enabled) + disconnect_gamepad(-1, false, true); } // A function to handle key presses from keyboard static void handle_keypress(SDL_Keysym *key) { - if (config.debug) { - output_log(LOGLEVEL_DEBUG, - "Key %s (%X) detected\n", - SDL_GetKeyName(key->sym), - key->sym - ); - } - - // Check default keys - if (key->sym == SDLK_LEFT) { - move_left(); - } - else if (key->sym == SDLK_RIGHT) { - move_right(); - } - else if (key->sym == SDLK_RETURN) { - output_log(LOGLEVEL_DEBUG, - "Selected Entry:\n" - "Title: %s\n" - "Icon Path: %s\n" - "Command: %s\n", - current_entry->title, - current_entry->icon_path, - current_entry->cmd - ); - - execute_command(current_entry->cmd); - } - else if (key->sym == SDLK_BACKSPACE) { - load_back_menu(current_menu); - } - - //Check hotkeys - else { - for (hotkey_t *i = hotkeys; i != NULL; i = i->next) { - if (key->sym == i->keycode) { - execute_command(i->cmd); - break; - } + if (config.debug) + log_debug("Key %s (#%X) detected", SDL_GetKeyName(key->sym), key->sym); + + // Check default keys + if (key->sym == SDLK_LEFT) + move_left(); + else if (key->sym == SDLK_RIGHT) + move_right(); + else if (key->sym == SDLK_RETURN) { + log_debug("Selected Entry:\n" + "Title: %s\n" + "Icon Path: %s\n" + "Command: %s", + current_entry->title, + current_entry->icon_path, + current_entry->cmd + ); + + execute_command(current_entry->cmd); + } + else if (key->sym == SDLK_BACKSPACE) + load_back_menu(current_menu); + + //Check hotkeys + else { + for (Hotkey *i = hotkeys; i != NULL; i = i->next) { + if (key->sym == i->keycode) { + execute_command(i->cmd); + break; + } + } } - } } // A function to quit the slideshow mode in case of error or program exit void quit_slideshow() { - // Free allocated image paths - for (int i = 0; i < slideshow->num_images; i++) { - free(slideshow->images[i]); - } - free(slideshow); + // Free allocated image paths + for (int i = 0; i < slideshow->num_images; i++) + free(slideshow->images[i]); + free(slideshow->images); + free(slideshow->order); + free(slideshow); } // A function to initialize the slideshow background mode static void init_slideshow() { - if (!directory_exists(config.slideshow_directory)) { - output_log(LOGLEVEL_ERROR, - "Error: Slideshow directory %s does not exist\n" - "Switching to color background mode\n", - config.slideshow_directory - ); - config.background_mode = MODE_COLOR; - set_draw_color(); - return; - } - // Allocate and initialize slideshow struct - slideshow = malloc(sizeof(slideshow_t)); - slideshow->i = -1; - slideshow->num_images = 0; - slideshow->transition_texture = NULL; - slideshow->transition_alpha = 0.0f; - slideshow->transition_change_rate = 255.0f / ((float) config.slideshow_transition_time / (float) POLLING_PERIOD); - - // Find background images from directory - int num_images = scan_slideshow_directory(slideshow, config.slideshow_directory); - - // Handle errors - if (num_images == 0) { - output_log(LOGLEVEL_ERROR, - "Error: No images found in slideshow directory %s\n" - "Changing background mode to color\n", - config.slideshow_directory - ); - quit_slideshow(); - config.background_mode = MODE_COLOR; - set_draw_color(); - } - else if (num_images == 1) { - output_log(LOGLEVEL_ERROR, - "Error: Only one image found in slideshow directory %s\n" - "Changing background mode to single image\n", - config.slideshow_directory - ); - background_texture = load_texture_from_file(slideshow->images[0]); - quit_slideshow(); - config.background_mode = MODE_IMAGE; - } - - // Generate array of random numbers for image order, load first image - else { - random_array(slideshow->order, slideshow->num_images); - SDL_Surface *surface = load_next_slideshow_background(slideshow, false); - background_texture = load_texture(surface); - if (config.debug) { - debug_slideshow(slideshow); + if (!directory_exists(config.slideshow_directory)) { + log_error("Slideshow directory '%s' does not exist, " + "Switching to color background mode", + config.slideshow_directory + ); + config.background_mode = BACKGROUND_COLOR; + set_draw_color(); + return; + } + // Allocate and initialize slideshow struct + slideshow = malloc(sizeof(Slideshow)); + *slideshow = (Slideshow) { + .i = -1, + .num_images = 0, + .transition_surface = NULL, + .transition_texture = NULL, + .transition_alpha = 0.f, + .transition_change_rate = 0.f, + .images = NULL, + .order = NULL + }; + + // Find background images from directory + scan_slideshow_directory(slideshow, config.slideshow_directory); + + // Handle errors + if (!slideshow->num_images) { + log_error("No images found in slideshow directory '%s', " + "Changing background mode to color", + config.slideshow_directory + ); + config.background_mode = BACKGROUND_COLOR; + quit_slideshow(); + } + else if (slideshow->num_images == 1) { + log_error("Only one image found in slideshow directory %s" + "Changing background mode to single image", + config.slideshow_directory + ); + free(config.background_image); + config.background_image = strdup(slideshow->images[0]); + config.background_mode = BACKGROUND_IMAGE; + quit_slideshow(); + } + + // Generate array of random numbers for image order, load first image + else { + slideshow->order = malloc(sizeof(int) * (size_t) slideshow->num_images); + random_array(slideshow->order, slideshow->num_images); + if (config.debug) + debug_slideshow(slideshow); } - } } // A function to initialize the screensaver feature static void init_screensaver() { - // Allocate memory for structure - screensaver = malloc(sizeof(screensaver_t)); - - // Convert intensity string to float - char intensity[PERCENT_MAX_CHARS]; - if (config.screensaver_intensity_str[0] != '\0') { - strcpy(intensity, config.screensaver_intensity_str); - } - else { - strcpy(intensity, DEFAULT_SCREENSAVER_INTENSITY); - } - int length = strlen(intensity); - intensity[length - 1] = '\0'; - float percent = atof(intensity); - - // Calculate alpha end value - screensaver->alpha_end_value = 255.0f * percent / 100.0f; - if (screensaver->alpha_end_value < 1.0f) { - output_log(LOGLEVEL_ERROR, "Invalid screensaver intensity value, disabling feature\n"); - config.screensaver_enabled = false; - free(screensaver); - screensaver = NULL; - return; - } - else if (screensaver->alpha_end_value >= 255.0f) { - screensaver->alpha_end_value = 255.0f; - } - screensaver->transition_change_rate = screensaver->alpha_end_value / ((float) SCREENSAVER_TRANSITION_TIME / (float) POLLING_PERIOD); - - // Render texture - SDL_Surface *surface = NULL; - surface = SDL_CreateRGBSurfaceWithFormat(0, - geo.screen_width, - geo.screen_height, - 32, - SDL_PIXELFORMAT_ARGB8888 - ); - Uint32 color = SDL_MapRGBA(surface->format, 0, 0, 0, 0xFF); - SDL_FillRect(surface, NULL, color); - screensaver->texture = load_texture(surface); - screensaver->alpha = 0.0f; - SDL_SetTextureAlphaMod(screensaver->texture, 0.0f); -} + // Allocate memory for structure + screensaver = malloc(sizeof(Screensaver)); + + // Convert intensity string to float + char intensity[PERCENT_MAX_CHARS]; + if (config.screensaver_intensity_str[0] != '\0') + copy_string(intensity, config.screensaver_intensity_str, sizeof(intensity)); + else + copy_string(intensity, DEFAULT_SCREENSAVER_INTENSITY, sizeof(intensity)); + size_t length = strlen(intensity); + intensity[length - 1] = '\0'; + float percent = (float) atof(intensity); + + // Calculate alpha end value + screensaver->alpha_end_value = 255.0f * percent / 100.0f; + if (screensaver->alpha_end_value < 1.0f) { + log_error("Invalid screensaver intensity value, disabling feature"); + config.screensaver_enabled = false; + free(screensaver); + screensaver = NULL; + return; + } + else if (screensaver->alpha_end_value >= 255.0f) + screensaver->alpha_end_value = 255.0f; -// A function to resume th slideshow after a launched application returns -static void resume_slideshow() -{ - ticks.slideshow_load = ticks.main; + screensaver->transition_change_rate = screensaver->alpha_end_value / ((float) SCREENSAVER_TRANSITION_TIME / (float) refresh_period); + + // Render texture + SDL_Surface *surface = NULL; + surface = SDL_CreateRGBSurfaceWithFormat(0, + geo.screen_width, + geo.screen_height, + 32, + SDL_PIXELFORMAT_ARGB8888 + ); + Uint32 color = SDL_MapRGBA(surface->format, 0, 0, 0, 0xFF); + SDL_FillRect(surface, NULL, color); + screensaver->texture = load_texture(surface); + screensaver->alpha = 0.0f; + SDL_SetTextureAlphaMod(screensaver->texture, 0.0f); } -// A function to render the scroll indicators -static void render_scroll_indicators() +// A function to resume the slideshow after a launched application returns +static void resume_slideshow() { - // Calcuate the geometry - scroll = malloc(sizeof(scroll_t)); - scroll->texture = NULL; - int scroll_indicator_height = (int) ((float) geo.screen_height * SCROLL_INDICATOR_HEIGHT); - - // Find scroll indicator file - char *prefixes[2]; - char assets_exe_buffer[MAX_PATH_CHARS + 1]; - prefixes[0] = join_paths(assets_exe_buffer, 2, config.exe_path, PATH_ASSETS_EXE); - #ifdef __unix__ - prefixes[1] = PATH_ASSETS_SYSTEM; - #else - prefixes[1] = PATH_ASSETS_RELATIVE; - #endif - char *scroll_indicator_path = find_file(FILENAME_SCROLL_INDICATOR, 2, prefixes); - - if (scroll_indicator_path == NULL) { - output_log(LOGLEVEL_ERROR, - "Error: Could not find scroll indicator SVG, disabling feature\n"); - free(scroll); - config.scroll_indicators = false; - return; - } - output_log(LOGLEVEL_DEBUG, "Scroll indicator found: %s\n", - scroll_indicator_path); - - // Render the SVG - scroll->texture = rasterize_svg_from_file(scroll_indicator_path, - -1, - scroll_indicator_height, - &scroll->rect_right - ); - scroll->rect_left.w = scroll->rect_right.w; - scroll->rect_left.h = scroll->rect_right.h; - free(scroll_indicator_path); - if (scroll->texture == NULL) { - output_log(LOGLEVEL_ERROR, "Error: Could not render scroll indicator, disabling feature\n"); - free(scroll); - config.scroll_indicators = false; - return; - } - // Calculate screen position - scroll->rect_right.y = geo.screen_height - geo.screen_margin - scroll->rect_right.h; - scroll->rect_right.x = geo.screen_width - geo.screen_margin - scroll->rect_right.w; - scroll->rect_left.y = scroll->rect_right.y; - scroll->rect_left.x = geo.screen_margin; - - // Set color - SDL_SetTextureColorMod(scroll->texture, - config.scroll_indicator_color.r, - config.scroll_indicator_color.g, - config.scroll_indicator_color.b); - SDL_SetTextureAlphaMod(scroll->texture, - config.scroll_indicator_color.a); + ticks.slideshow_load = ticks.main; } // A function to load a menu -static int load_menu(menu_t *menu, bool set_back_menu, bool reset_position) +static int load_menu(Menu *menu, bool set_back_menu, bool reset_position) { - if (menu == NULL) { - return 1; - } - int buttons; - menu_t *previous_menu = current_menu; - - current_menu = menu; - output_log(LOGLEVEL_DEBUG, "Loading menu \"%s\"\n", current_menu->name); - - // Return error if the menu doesn't contain entires - if (current_menu->num_entries == 0) { - output_log(LOGLEVEL_ERROR, - "Error: No valid entries found for Menu \"%s\"", - current_menu->name - ); - return 1; - } - - // Render the menu if not already rendered - if (current_menu->rendered == false) { - render_buttons(current_menu); - } - - // Set menu properties - if (set_back_menu) { - current_menu->back = previous_menu; - } - - if (reset_position) { - current_entry = current_menu->first_entry; - current_menu->root_entry = current_entry; - current_menu->highlight_position = 0; - current_menu->page = 0; - } - else { - current_entry = current_menu->last_selected_entry; - } - - buttons = current_menu->num_entries - (current_menu->page)*config.max_buttons; - if (buttons > config.max_buttons) { - buttons = config.max_buttons; - } - - // Recalculate the screen geometry - calculate_button_geometry(current_menu->root_entry, buttons); - //if (config.debug) { - // debug_button_positions(current_menu->root_entry, current_menu, &geo); - //} - highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; - highlight->rect.y = current_entry->icon_rect.y - config.highlight_vpadding; - - // Output to screen - state.screen_updates = true; - return 0; + if (menu == NULL) + return 1; + + unsigned int buttons; + Menu *previous_menu = current_menu; + + current_menu = menu; + log_debug("Loading menu '%s'", current_menu->name); + + // Return error if the menu doesn't contain entires + if (current_menu->num_entries == 0) { + log_error("No valid entries found for Menu '%s'", current_menu->name); + current_menu = previous_menu; + return 1; + } + + // Render the menu if not already rendered + if (current_menu->rendered == false) + render_buttons(current_menu); + + // Set menu properties + if (set_back_menu) + current_menu->back = previous_menu; + + if (reset_position) { + current_entry = current_menu->first_entry; + current_menu->root_entry = current_entry; + current_menu->highlight_position = 0; + current_menu->page = 0; + } + else + current_entry = current_menu->last_selected_entry; + + buttons = current_menu->num_entries - (current_menu->page)*config.max_buttons; + if (buttons > config.max_buttons) + buttons = config.max_buttons; + + // Recalculate the screen geometry + calculate_button_geometry(current_menu->root_entry, (int) buttons); + if (config.highlight) { + highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; + highlight->rect.y = current_entry->icon_rect.y - config.highlight_vpadding; + } + return 0; } // A function to load a menu by its name static int load_menu_by_name(const char *menu_name, bool set_back_menu, bool reset_position) { - menu_t *menu = get_menu(menu_name); - return load_menu(menu, set_back_menu, reset_position); + Menu *menu = get_menu(menu_name); + return load_menu(menu, set_back_menu, reset_position); } // A function to calculate the layout of the buttons -static void calculate_button_geometry(entry_t *entry, int buttons) +static void calculate_button_geometry(Entry *entry, int buttons) { - // Calculate proper spacing - int button_height = config.icon_size + config.title_padding + geo.font_height; - geo.x_margin = (geo.screen_width - config.icon_size*buttons - - buttons*config.icon_spacing + config.icon_spacing) / 2; - geo.x_advance = config.icon_size + config.icon_spacing; - geo.num_buttons = buttons; - - // Assign values to entries - for (int i = 0; i < geo.num_buttons; i++) { - entry->icon_rect.x = geo.x_margin + i*geo.x_advance; - entry->icon_rect.y = geo.y_margin; - entry->icon_rect.w = config.icon_size; - entry->icon_rect.h = config.icon_size; - entry->text_rect.x = entry->icon_rect.x + - (entry->icon_rect.w - entry->text_rect.w) / 2; - entry->text_rect.y = entry->icon_rect.y + - config.icon_size + entry->title_offset + config.title_padding; - entry = entry->next; - } + // Calculate proper spacing + geo.x_margin = (geo.screen_width - config.icon_size*buttons - + buttons*config.icon_spacing + config.icon_spacing) / 2; + geo.x_advance = config.icon_size + config.icon_spacing; + geo.num_buttons = buttons; + + // Assign values to entries + for (int i = 0; i < geo.num_buttons; i++) { + entry->icon_rect.x = geo.x_margin + i*geo.x_advance; + entry->icon_rect.y = geo.y_margin; + entry->icon_rect.w = config.icon_size; + entry->icon_rect.h = config.icon_size; + entry->text_rect.x = entry->icon_rect.x + + (entry->icon_rect.w - entry->text_rect.w) / 2; + entry->text_rect.y = entry->icon_rect.y + config.icon_size + entry->title_offset + + config.title_padding; + entry = entry->next; + } } // A function to render all buttons (icon and text) for a menu -static void render_buttons(menu_t *menu) +static void render_buttons(Menu *menu) { - entry_t *entry; - int h; - for (entry = menu->first_entry; entry != NULL; entry = entry->next) { - entry->icon = load_texture_from_file(entry->icon_path); - entry->title_texture = render_text_texture(entry->title, - &title_info, - &entry->text_rect, - &h); - if (config.title_oversize_mode == MODE_TEXT_SHRINK && h != geo.font_height) { - entry->title_offset = (geo.font_height - h) / 2; + Entry *entry; + int h; + for (entry = menu->first_entry; entry != NULL; entry = entry->next) { + entry->icon = load_texture_from_file(entry->icon_path); + entry->icon_selected = (entry->icon_selected_path != NULL) ? load_texture_from_file(entry->icon_selected_path) : NULL; + if (config.titles_enabled) { + entry->title_texture = render_text_texture(entry->title, &title_info, &entry->text_rect, &h); + if (config.title_oversize_mode == OVERSIZE_SHRINK && h != geo.font_height) + entry->title_offset = (geo.font_height - h) / 2; + } } - } - menu->rendered = true; -} - -// A function to output all visible buttons to the renderer -static void draw_buttons(entry_t *entry) -{ - for (int i = 0; i < geo.num_buttons; i++) { - SDL_RenderCopy(renderer,entry->icon,NULL,&entry->icon_rect); - SDL_RenderCopy(renderer,entry->title_texture,NULL,&entry->text_rect); - entry = entry-> next; - } + menu->rendered = true; } // A function to move the selection left when clicked by user static void move_left() { - // If we are not in leftmost position, move highlight left - if (current_menu->highlight_position > 0) { - highlight->rect.x -= geo.x_advance; - current_menu->highlight_position--; - current_entry = current_entry->previous; - state.screen_updates = true; - } - - // If we are in leftmost position, but there is a previous page, load the previous page - else if (current_menu->highlight_position == 0 && current_menu->page > 0) { - int buttons = config.max_buttons; - current_entry = current_entry->previous; - current_menu->root_entry = advance_entries(current_menu->root_entry,buttons,DIRECTION_LEFT); - calculate_button_geometry(current_menu->root_entry, buttons); - highlight->rect.x = current_entry->icon_rect.x - - config.highlight_hpadding; - current_menu->page--; - current_menu->highlight_position = buttons - 1; - //if (config.debug) { - // debug_button_positions(current_menu->root_entry, current_menu, &geo); - //} - state.screen_updates = true; - } + // If we are not in leftmost position, move highlight left + if (current_menu->highlight_position > 0) { + if (config.highlight) + highlight->rect.x -= geo.x_advance; + current_menu->highlight_position--; + current_entry = current_entry->previous; + } + + // If we are in leftmost position... + else if (current_menu->highlight_position == 0 && (current_menu->page > 0 || config.wrap_entries)) { + unsigned int buttons; + current_entry = current_entry->previous; + + // Load the previous page if there is a valid previous entry + if (current_entry) { + buttons = config.max_buttons; + current_menu->root_entry = advance_entries(current_menu->root_entry, (int) buttons, DIRECTION_LEFT); + current_menu->page--; + } + + // If the user has the wrap entries setting, select the last entry in the menu + else { + current_entry = advance_entries(current_menu->first_entry, (int) current_menu->num_entries - 1, DIRECTION_RIGHT); + unsigned int num_pages = DIV_ROUND_UP(current_menu->num_entries, config.max_buttons); + current_menu->root_entry = advance_entries(current_menu->root_entry, + (int) ((num_pages - 1 - current_menu->page) * config.max_buttons), + DIRECTION_RIGHT + ); + current_menu->page = num_pages - 1; + buttons = current_menu->num_entries - current_menu->page * config.max_buttons; + } + + calculate_button_geometry(current_menu->root_entry, (int) buttons); + if (config.highlight) + highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; + current_menu->highlight_position = buttons - 1; + } } // A function to move the selection right when clicked by the user static void move_right() { - // If we are not in the rightmost position, move highlight right - if (current_menu->highlight_position < (geo.num_buttons - 1)) { - highlight->rect.x += geo.x_advance; - current_menu->highlight_position++; - current_entry = current_entry->next; - state.screen_updates = true; - } - - // If we are in the rightmost postion, but there are more entries in the menu, load next page - else if (current_menu->highlight_position + current_menu->page*config.max_buttons < - (current_menu->num_entries - 1)) { - int buttons = current_menu->num_entries - - (current_menu->page + 1)*config.max_buttons; - if (buttons > config.max_buttons) { - buttons = config.max_buttons; + // If we are not in the rightmost position, move highlight right + if ((int) current_menu->highlight_position < (geo.num_buttons - 1)) { + if (config.highlight) + highlight->rect.x += geo.x_advance; + current_menu->highlight_position++; + current_entry = current_entry->next; + } + + // If we are in the rightmost postion, but there are more entries in the menu, load next page + else if (current_menu->highlight_position + current_menu->page*config.max_buttons < + (current_menu->num_entries - 1)) { + unsigned int buttons = current_menu->num_entries - (current_menu->page + 1)*config.max_buttons; + if (buttons > config.max_buttons) + buttons = config.max_buttons; + current_entry = current_entry->next; + current_menu->root_entry = current_entry; + calculate_button_geometry(current_menu->root_entry, (int) buttons); + if (config.highlight) + highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; + current_menu->page++; + current_menu->highlight_position = 0; + } + + // If user has the wrap entries setting, reset menu to first entry + else if (config.wrap_entries) { + current_entry = current_menu->first_entry; + current_menu->root_entry = current_entry; + current_menu->highlight_position = 0; + current_menu->page = 0; + if (config.highlight) + highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; + calculate_button_geometry(current_menu->root_entry, (int) MIN(current_menu->num_entries, config.max_buttons)); } - current_entry = current_entry->next; - current_menu->root_entry = current_entry; - calculate_button_geometry(current_menu->root_entry, buttons); - highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; - current_menu->page++; - current_menu->highlight_position = 0; - //if (config.debug) { - // debug_button_positions(current_menu->root_entry, current_menu, &geo); - //} - state.screen_updates = true; - } } // A function to load a submenu static void load_submenu(const char *submenu) { - current_menu->last_selected_entry = current_entry; - load_menu_by_name(submenu, true, true); + current_menu->last_selected_entry = current_entry; + load_menu_by_name(submenu, true, true); } // A function to load the previous menu -static void load_back_menu(menu_t *menu) +static void load_back_menu(Menu *menu) { - load_menu(menu->back, false, config.reset_on_back); + load_menu(menu->back, false, config.reset_on_back); } // A function to update the screen with all visible textures static void draw_screen() { - // Draw background - if (config.background_mode == MODE_COLOR) { + // Draw background SDL_RenderClear(renderer); - } - else { - SDL_RenderCopy(renderer, background_texture, NULL, NULL); - } - - if (config.background_mode == MODE_SLIDESHOW && state.slideshow_transition) { - SDL_RenderCopy(renderer, slideshow->transition_texture, NULL, NULL); - } - - // Draw scroll indicators - if (config.scroll_indicators && - (current_menu->page*config.max_buttons + geo.num_buttons) <= (current_menu->num_entries - 1)) { - SDL_RenderCopy(renderer, scroll->texture, NULL, &scroll->rect_right); - } - if (config.scroll_indicators && current_menu->page > 0) { - SDL_RenderCopyEx(renderer, scroll->texture, NULL, &scroll->rect_left, 0, NULL, SDL_FLIP_HORIZONTAL); - } - - // Draw clock - if (config.clock_enabled) { - SDL_RenderCopy(renderer, launcher_clock->time_texture, NULL, &launcher_clock->time_rect); - if (config.clock_show_date) { - SDL_RenderCopy(renderer, launcher_clock->date_texture, NULL, &launcher_clock->date_rect); + if (!(state.application_launching && config.on_launch == ON_LAUNCH_BLANK)) { + if (config.background_mode == BACKGROUND_IMAGE || config.background_mode == BACKGROUND_SLIDESHOW) + SDL_RenderCopy(renderer, background_texture, NULL, NULL); + + if (config.background_mode == BACKGROUND_SLIDESHOW && state.slideshow_transition) + SDL_RenderCopy(renderer, slideshow->transition_texture, NULL, NULL); + + // Draw background overlay + if (config.background_overlay) + SDL_RenderCopy(renderer, background_overlay, NULL, NULL); + + // Draw scroll indicators + if (config.scroll_indicators && + (current_menu->page*config.max_buttons + (unsigned int) geo.num_buttons) <= (current_menu->num_entries - 1)) + SDL_RenderCopy(renderer, scroll->texture, NULL, &scroll->rect_right); + + if (config.scroll_indicators && current_menu->page > 0) + SDL_RenderCopyEx(renderer, scroll->texture, NULL, &scroll->rect_left, 0, NULL, SDL_FLIP_HORIZONTAL); + + // Draw clock + if (config.clock_enabled) { + SDL_RenderCopy(renderer, clk->time_texture, NULL, &clk->time_rect); + if (config.clock_show_date) + SDL_RenderCopy(renderer, clk->date_texture, NULL, &clk->date_rect); + } + + // Draw highlight + if (config.highlight) + SDL_RenderCopy(renderer, + highlight->texture, + NULL, + &highlight->rect + ); + + // Draw buttons + Entry *entry = current_menu->root_entry; + SDL_Texture *icon; + for (int i = 0; i < geo.num_buttons; i++) { + icon = (entry->icon_selected != NULL && i == (int) current_menu->highlight_position) ? entry->icon_selected : entry->icon; + SDL_RenderCopy(renderer, icon, NULL, &entry->icon_rect); + if (config.titles_enabled) + SDL_RenderCopy(renderer, entry->title_texture, NULL, &entry->text_rect); + entry = entry-> next; + } + + // Draw screensaver + if (state.screensaver_active) + SDL_RenderCopy(renderer, screensaver->texture, NULL, NULL); + } + else + SDL_RenderFillRect(renderer, NULL); + + // Output to screen + SDL_RenderPresent(renderer); + if (!config.vsync) { + Uint32 sleep_time = refresh_period - (SDL_GetTicks() - ticks.main); + if (sleep_time > 0) + SDL_Delay(sleep_time); } - } - - // Draw highlight - SDL_RenderCopy(renderer, - highlight->texture, - NULL, - &highlight->rect - ); - - // Draw buttons - draw_buttons(current_menu->root_entry); - - // Draw screensaver - if (state.screensaver_active) { - SDL_RenderCopy(renderer, screensaver->texture, NULL, NULL); - } - - // Output to screen - SDL_RenderPresent(renderer); - state.screen_updates = false; } // A function to execute the user's command static void execute_command(const char *command) { - // Copy command into separate buffer - char *cmd; - copy_string(&cmd, command); - - // Parse special commands - if (cmd[0] == ':') { - char *delimiter = " "; - char *special_command = strtok(cmd, delimiter); - if (!strcmp(special_command, SCMD_SUBMENU)) { - char *submenu = strtok(NULL, delimiter); - load_submenu(submenu); - } - else if (!strcmp(special_command, SCMD_LEFT)) { - move_left(); - } - else if (!strcmp(special_command, SCMD_RIGHT)) { - move_right(); - } - else if (!strcmp(special_command, SCMD_SELECT)) { - execute_command(current_entry->cmd); - } - else if (!strcmp(special_command, SCMD_HOME)) { - load_menu(default_menu, false, true); - } - else if (!strcmp(special_command, SCMD_BACK)) { - load_back_menu(current_menu); - } - else if (!strcmp(special_command, SCMD_QUIT)) { - quit(0); - } - else if (!strcmp(special_command, SCMD_SHUTDOWN)) { - system(CMD_SHUTDOWN); - } - else if (!strcmp(special_command, SCMD_RESTART)) { - system(CMD_RESTART); + // Copy command into separate buffer + char *cmd = strdup(command); + + // Parse special commands + if (cmd[0] == ':') { + char *delimiter = " "; + char *special_command = strtok(cmd, delimiter); + if (!strcmp(special_command, SCMD_SUBMENU)) { + char *submenu = strtok(NULL, ""); + if (submenu != NULL) + load_submenu(submenu); + } + else if (!strcmp(special_command, SCMD_FORK)) { + char *fork_command = strtok(NULL, ""); + if (fork_command != NULL) + start_process(fork_command, false); + } + else if (!strcmp(special_command, SCMD_LEFT)) + move_left(); + else if (!strcmp(special_command, SCMD_RIGHT)) + move_right(); + else if (!strcmp(special_command, SCMD_SELECT)) + execute_command(current_entry->cmd); + else if (!strcmp(special_command, SCMD_HOME)) + load_menu(default_menu, false, true); + else if (!strcmp(special_command, SCMD_BACK)) + load_back_menu(current_menu); + else if (!strcmp(special_command, SCMD_QUIT)) + quit(EXIT_SUCCESS); + else if (!strcmp(special_command, SCMD_SHUTDOWN)) + scmd_shutdown(); + else if (!strcmp(special_command, SCMD_RESTART)) + scmd_restart(); + else if (!strcmp(special_command, SCMD_SLEEP)) + scmd_sleep(); } - else if (!strcmp(special_command, SCMD_SLEEP)) { - system(CMD_SLEEP); + + // Launch external application + else { + SDL_Delay(50); + if (start_process(cmd, true)) { + state.application_launching = true; + ticks.application_launched = ticks.main; + if (config.on_launch == ON_LAUNCH_BLANK) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0xFF); + else if (config.on_launch == ON_LAUNCH_QUIT) + quit(EXIT_SUCCESS); + } } - } + free(cmd); +} - // Launch external application - else { +// A function to initialize the gamepad struct +static void init_gamepad(Gamepad **gamepad, int device_index) +{ + *gamepad = malloc(sizeof(Gamepad)); + **gamepad = (Gamepad) { + .device_index = device_index, + .id = (int) SDL_JoystickGetDeviceInstanceID(device_index), + .controller = NULL, + .next = NULL, + .previous = NULL, + }; +} - // Perform prelaunch behavior from OnLaunch setting - if (config.on_launch == MODE_ON_LAUNCH_HIDE) { - SDL_HideWindow(window); +// A function to open the SDL controller +static void open_controller(Gamepad *gamepad, bool raise_error) +{ + gamepad->controller = SDL_GameControllerOpen(gamepad->device_index); + if (gamepad->controller == NULL) { + if (raise_error) + log_error("Could not open gamepad at device index %i", config.gamepad_device); + return; } - else if (config.on_launch == MODE_ON_LAUNCH_BLANK) { - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0xFF); - SDL_RenderClear(renderer); - SDL_RenderPresent(renderer); + if (config.debug && raise_error) { + char *mapping = SDL_GameControllerMapping(gamepad->controller); + log_debug("Gamepad Mapping:\n%s", mapping); + SDL_free(mapping); } +} - // Launch application - SDL_Delay(50); - launch_application(cmd); - - // Rebaseline the timing after the program is done - ticks.main = SDL_GetTicks(); - ticks.last_input = ticks.main; +// A function to connect gamepad(s) +static void connect_gamepad(int device_index, bool open, bool raise_error) +{ + if (device_index >= 0) { + Gamepad *gamepad = NULL; + for (gamepad = gamepads; gamepad != NULL; gamepad = gamepad->next) { + if (gamepad->id == device_index) + break; + } + if (gamepad == NULL) { + init_gamepad(&gamepad, device_index); + if (gamepads == NULL) + gamepads = gamepad; - // Post-application updates - if (config.clock_enabled) { - update_clock(true); - } - if (config.background_mode == MODE_SLIDESHOW) { - resume_slideshow(); + // Add to end of the linked list + else { + Gamepad *i; + for (i = gamepads; i->next != NULL; i = i->next); + i->next = gamepad; + gamepad->previous = i; + } + } + if (open) + open_controller(gamepad, raise_error); } - if (config.on_launch == MODE_ON_LAUNCH_HIDE) { - SDL_ShowWindow(window); + else if (open) { + for (Gamepad *i = gamepads; i != NULL; i = i->next) + open_controller(i, raise_error); } - - // Prevent any duplicate keypresses that were used to exit the program (Wayland workaround) - #ifdef __unix__ - SDL_Delay(50); - SDL_PumpEvents(); - SDL_FlushEvent(SDL_KEYDOWN); - #endif - } -free(cmd); } -// A function to connect to a gamepad -static void connect_gamepad(int device_index) +// A function to disconnect gamepad(s) +static void disconnect_gamepad(int id, bool disconnect, bool remove) { - gamepad = SDL_GameControllerOpen(device_index); - if (gamepad == NULL) { - output_log(LOGLEVEL_ERROR, - "Error: Could not open gamepad at device index %i\n", - config.gamepad_device - ); - return; - } - if (config.debug) { - char *mapping = SDL_GameControllerMapping(gamepad); - output_log(LOGLEVEL_DEBUG, - "Gamepad Mapping:\n%s\n", - mapping - ); - SDL_free(mapping); - } + for (Gamepad *i = gamepads; i != NULL;) { + if (id < 0 || i->id == id) { + if (disconnect) + SDL_GameControllerClose(i->controller); + if (remove) { + if (i->next != NULL) + i->next->previous = i->previous; + if (i->previous != NULL) + i->previous->next = i->next; + if (i == gamepads) + gamepads = i->next; + Gamepad *tmp = i->next; + free(i); + i = tmp; + } + else { + i->controller = NULL; + i = i->next; + } + } + else + i = i->next; + } } // A function to poll the connected gamepad for commands static void poll_gamepad() { - int value_multiplier; // Handles positive or negative axis - for (gamepad_control_t *i = gamepad_controls; i != NULL; i = i->next) { - - // Check if axis value exceeds dead zone - if (i->type == TYPE_AXIS_POS || i->type == TYPE_AXIS_NEG) { - if (i->type == TYPE_AXIS_POS) { - value_multiplier = 1; - } - else if (i->type == TYPE_AXIS_NEG) { - value_multiplier = -1; - } - if (value_multiplier*SDL_GameControllerGetAxis(gamepad, i->index) > GAMEPAD_DEADZONE) { - i->repeat++; - } - else { - i->repeat = 0; - } - } + int value_multiplier; // Handles positive or negative axis + bool pressed; + for (GamepadControl *i = gamepad_controls; i != NULL; i = i->next) { + pressed = false; + for (Gamepad *gamepad = gamepads; gamepad != NULL; gamepad = gamepad->next) { + + // Check if axis value exceeds dead zone + if (i->type == TYPE_AXIS_POS || i->type == TYPE_AXIS_NEG) { + if (i->type == TYPE_AXIS_POS) + value_multiplier = 1; + else if (i->type == TYPE_AXIS_NEG) + value_multiplier = -1; + if (value_multiplier*SDL_GameControllerGetAxis(gamepad->controller, i->index) > GAMEPAD_DEADZONE) { + i->repeat++; + pressed = true; + break; + } + } - // Check buttons - else if (i->type == TYPE_BUTTON) { - if (SDL_GameControllerGetButton(gamepad, i->index)) { - i->repeat++; - } - else { - i->repeat = 0; - } - } + // Check buttons + else if (i->type == TYPE_BUTTON) { + if (SDL_GameControllerGetButton(gamepad->controller, i->index)) { + i->repeat++; + pressed = true; + break; + } + } + } + if (!pressed) { + i->repeat = 0; + continue; + } - // Execute command if first press or valid repeat - if (i->repeat == 1) { - output_log(LOGLEVEL_DEBUG, "Gamepad %s detected\n", i->label); - ticks.last_input = ticks.main; - execute_command(i->cmd); - } - else if (i->repeat == delay_period) { - ticks.last_input = ticks.main; - execute_command(i->cmd); - i->repeat -= repeat_period; + // Execute command if first press or valid repeat + if (i->repeat == 1) { + log_debug("Gamepad %s detected", i->label); + ticks.last_input = ticks.main; + execute_command(i->cmd); + } + else if (i->repeat == delay_period) { + ticks.last_input = ticks.main; + execute_command(i->cmd); + i->repeat -= repeat_period; + } } - } } // A function to update the slideshow static void update_slideshow() { - // If image duration time has elapsed, load the next image and start the transition - if (!state.slideshow_transition && (ticks.main - ticks.slideshow_load > config.slideshow_image_duration) && - !state.slideshow_paused) { - - // Render the new background image in a separate thread so we don't block the main thread - if (!state.slideshow_background_rendering && !state.slideshow_background_ready) { - slideshow_thread = SDL_CreateThread(load_next_slideshow_background_async, "Slideshow Thread", (void*) slideshow); - state.slideshow_background_rendering = true; - } + // If image duration time has elapsed, load the next image and start the transition + if (!state.slideshow_transition && (ticks.main - ticks.slideshow_load > config.slideshow_image_duration) && + !state.slideshow_paused) { + + // Render the new background image in a separate thread so we don't block the main thread + if (!state.slideshow_background_rendering && !state.slideshow_background_ready) { + Slideshowhread = SDL_CreateThread(load_next_slideshow_background_async, "Slideshow Thread", (void*) slideshow); + state.slideshow_background_rendering = true; + } - // Convert background to texture after the rendering thread has completed - else if (state.slideshow_background_ready) { - SDL_WaitThread(slideshow_thread, NULL); - slideshow_thread = NULL; - if (config.slideshow_transition_time > 0) { - slideshow->transition_texture = load_texture(slideshow->transition_surface); - SDL_SetTextureAlphaMod(slideshow->transition_texture, 0); - state.slideshow_transition = true; - } - else { - SDL_DestroyTexture(background_texture); - background_texture = load_texture(slideshow->transition_surface); - ticks.slideshow_load = ticks.main; - } - slideshow->transition_surface = NULL; - state.slideshow_background_ready = false; - state.screen_updates = true; - } - } - else if (state.slideshow_transition) { - - // Increase the transparency - slideshow->transition_alpha += slideshow->transition_change_rate; - - // If transition is done, destroy old background and replace it with the new one - if (slideshow->transition_alpha >= 255.0f) { - SDL_SetTextureAlphaMod(slideshow->transition_texture, 0xFF); - slideshow->transition_alpha = 0.0f; - SDL_DestroyTexture(background_texture); - background_texture = slideshow->transition_texture; - slideshow->transition_texture = NULL; - state.slideshow_transition = false; - ticks.slideshow_load = ticks.main; + // Convert background to texture after the rendering thread has completed + else if (state.slideshow_background_ready) { + SDL_WaitThread(Slideshowhread, NULL); + Slideshowhread = NULL; + if (config.slideshow_transition_time > 0) { + slideshow->transition_texture = load_texture(slideshow->transition_surface); + SDL_SetTextureAlphaMod(slideshow->transition_texture, 0); + state.slideshow_transition = true; + } + else { + SDL_DestroyTexture(background_texture); + background_texture = load_texture(slideshow->transition_surface); + ticks.slideshow_load = ticks.main; + } + slideshow->transition_surface = NULL; + state.slideshow_background_ready = false; + } } - else { - SDL_SetTextureAlphaMod(slideshow->transition_texture, (Uint8) slideshow->transition_alpha); + else if (state.slideshow_transition) { + + // Increase the transparency + slideshow->transition_alpha += slideshow->transition_change_rate; + + // If transition is done, destroy old background and replace it with the new one + if (slideshow->transition_alpha >= 255.0f) { + SDL_SetTextureAlphaMod(slideshow->transition_texture, 0xFF); + slideshow->transition_alpha = 0.0f; + SDL_DestroyTexture(background_texture); + background_texture = slideshow->transition_texture; + slideshow->transition_texture = NULL; + state.slideshow_transition = false; + ticks.slideshow_load = ticks.main; + } + else + SDL_SetTextureAlphaMod(slideshow->transition_texture, (Uint8) slideshow->transition_alpha); } - state.screen_updates = true; - } } // A function to update the screensaver static void update_screensaver() { - // Activate the screensaver if the launcher has been idle for the required time - if (!state.screensaver_active && ticks.main - ticks.last_input > config.screensaver_idle_time) { - state.screensaver_active = true; - state.screensaver_transition = true; - if (config.background_mode == MODE_SLIDESHOW && config.screensaver_pause_slideshow) { - state.slideshow_paused = true; - } - } - else { - - // Transition the screen to dark - if (state.screensaver_transition) { - screensaver->alpha += screensaver->transition_change_rate; - if (screensaver->alpha >= screensaver->alpha_end_value) { - SDL_SetTextureAlphaMod(screensaver->texture, (Uint8) screensaver->alpha_end_value); - state.screensaver_transition = false; - } - else { - SDL_SetTextureAlphaMod(screensaver->texture, (Uint8) screensaver->alpha); - } - state.screen_updates = true; + // Activate the screensaver if the launcher has been idle for the required time + if (!state.screensaver_active && ticks.main - ticks.last_input > config.screensaver_idle_time) { + state.screensaver_active = true; + state.screensaver_transition = true; + if (config.background_mode == BACKGROUND_SLIDESHOW && config.screensaver_pause_slideshow) + state.slideshow_paused = true; } + else { - // User has pressed input, deactivate the screensaver - if (state.screensaver_active && ticks.last_input == ticks.main) { - SDL_SetTextureAlphaMod(screensaver->texture, 0); - screensaver->alpha = 0.0f; - state.screensaver_active = false; - state.screensaver_transition = false; - if (config.background_mode == MODE_SLIDESHOW) { - state.slideshow_paused = false; - - // Reset the slideshow time so we don't have a transition immediately - // after coming out of screensaver mode - ticks.slideshow_load = ticks.main; - } - state.screen_updates = true; + // Transition the screen to dark + if (state.screensaver_transition) { + screensaver->alpha += screensaver->transition_change_rate; + if (screensaver->alpha >= screensaver->alpha_end_value) { + SDL_SetTextureAlphaMod(screensaver->texture, (Uint8) screensaver->alpha_end_value); + state.screensaver_transition = false; + } + else + SDL_SetTextureAlphaMod(screensaver->texture, (Uint8) screensaver->alpha); + } + + // User has pressed input, deactivate the screensaver + if (state.screensaver_active && ticks.last_input == ticks.main) { + SDL_SetTextureAlphaMod(screensaver->texture, 0); + screensaver->alpha = 0.0f; + state.screensaver_active = false; + state.screensaver_transition = false; + if (config.background_mode == BACKGROUND_SLIDESHOW) { + state.slideshow_paused = false; + + // Reset the slideshow time so we don't have a transition immediately + // after coming out of screensaver mode + ticks.slideshow_load = ticks.main; + } + } } - } } // A function to update the clock display static void update_clock(bool block) { - if (ticks.main - ticks.clock_update > CLOCK_UPDATE_PERIOD) { - if (!state.clock_rendering) { - - // Check to see if the time has changed - get_time(launcher_clock); - if (launcher_clock->render_time) { - state.clock_rendering = true; - if (block) { - render_clock(launcher_clock); + if (ticks.main - ticks.clock_update > CLOCK_UPDATE_PERIOD) { + if (!state.clock_rendering) { + + // Check to see if the time has changed + get_time(clk); + if (clk->render_time) { + state.clock_rendering = true; + if (block) + render_clock(clk); + else + clock_thread = SDL_CreateThread(render_clock_async, "Clock Thread", (void*) clk); + } + else + ticks.clock_update = ticks.main; } - else { - clock_thread = SDL_CreateThread(render_clock_async, "Clock Thread", (void*) launcher_clock); - } - } - else { - ticks.clock_update = ticks.main; - } - } - // Render texture - if (state.clock_ready) { - SDL_WaitThread(clock_thread, NULL); - clock_thread = NULL; - SDL_DestroyTexture(launcher_clock->time_texture); - launcher_clock->time_texture = load_texture(launcher_clock->time_surface); - launcher_clock->time_surface = NULL; - if (launcher_clock->render_date) { - SDL_DestroyTexture(launcher_clock->date_texture); - launcher_clock->date_texture = load_texture(launcher_clock->date_surface); - launcher_clock->date_surface = NULL; - } - ticks.clock_update = ticks.main; - launcher_clock->render_time = false; - launcher_clock->render_date = false; - state.clock_rendering = false; - state.clock_ready = false; - state.screen_updates = true; + // Render texture + if (state.clock_ready) { + SDL_WaitThread(clock_thread, NULL); + clock_thread = NULL; + SDL_DestroyTexture(clk->time_texture); + clk->time_texture = load_texture(clk->time_surface); + clk->time_surface = NULL; + if (clk->render_date) { + SDL_DestroyTexture(clk->date_texture); + clk->date_texture = load_texture(clk->date_surface); + clk->date_surface = NULL; + } + ticks.clock_update = ticks.main; + clk->render_time = false; + clk->render_date = false; + state.clock_rendering = false; + state.clock_ready = false; + } } - } +} + +static inline void pre_launch() +{ + if (gamepads != NULL) + disconnect_gamepad(-1, true, false); + +// Initialize exit hotkey for Windows +#ifdef _WIN32 + if (has_exit_hotkey()) + SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE); +#endif +} + +static inline void post_launch() +{ + // Rebaseline the timing after the program is done + ticks.main = SDL_GetTicks(); + ticks.last_input = ticks.main; + + // Post-application updates + if (config.gamepad_enabled) + connect_gamepad(-1, true, false); + if (config.clock_enabled) + update_clock(true); + if (config.background_mode == BACKGROUND_SLIDESHOW) + resume_slideshow(); + if (config.on_launch == ON_LAUNCH_BLANK) + set_draw_color(); + +#ifdef _WIN32 + SDL_EventState(SDL_SYSWMEVENT, SDL_DISABLE); + if (config.background_mode == BACKGROUND_TRANSPARENT) + hide_cursor(current_entry); +#endif } // A function to quit the launcher void quit(int status) { - if (status == 0) { - output_log(LOGLEVEL_DEBUG, "Quitting program\n"); - } - cleanup(); - exit(status); + log_debug("Quitting program"); + if (status != EXIT_SUCCESS) + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, + PROJECT_NAME, + "A critical error occurred. Check the log file for details.", + NULL + ); + if (config.quit_cmd != NULL) { + execute_command(config.quit_cmd); + free(config.quit_cmd); + } + cleanup(); + exit(status); +} + +// A function to print the version and other info to command line +void print_version(FILE *stream) +{ + SDL_version sdl_version; + SDL_GetVersion(&sdl_version); + const SDL_version *img_version = IMG_Linked_Version(); + const SDL_version *ttf_version = TTF_Linked_Version(); + fprintf(stream, PROJECT_NAME " version " PROJECT_VERSION ", using:" endline); + fprintf(stream, " SDL %u.%u.%u" endline, sdl_version.major, sdl_version.minor, sdl_version.patch); + fprintf(stream, " SDL_image %u.%u.%u" endline, img_version->major, img_version->minor, img_version->patch); + fprintf(stream, " SDL_ttf %u.%u.%u" endline, ttf_version->major, ttf_version->minor, ttf_version->patch); } int main(int argc, char *argv[]) { - SDL_Event event; - int error; - char *config_file_path = NULL; - config.exe_path = SDL_GetBasePath(); - - // Handle command line arguments, find config file - handle_arguments(argc, argv, &config_file_path); - - // Parse config file for settings and menu entries - parse_config_file(config_file_path); - free(config_file_path); - - // Initialize libraries - if (init_sdl() || init_ttf() || init_svg()) { - quit(1); - } - - // Initialize timing - ticks.main = SDL_GetTicks(); - ticks.last_input = ticks.main; - - // Check settings against requirements - validate_settings(&geo); - - // Load gamepad overrides - if (config.gamepad_enabled) { - if (config.gamepad_mappings_file != NULL) { - error = SDL_GameControllerAddMappingsFromFile(config.gamepad_mappings_file); - if (error) { - output_log(LOGLEVEL_ERROR, - "Error: Could not load gamepad mappings from %s\n", - config.gamepad_mappings_file - ); - } + int error; + char *config_file_path = NULL; + config.exe_path = SDL_GetBasePath(); + + // Handle command line arguments, find config file + handle_arguments(argc, argv, &config_file_path); + + // Parse config file for settings and menu entries + parse_config_file(config_file_path); + free(config_file_path); + + // Get default menu + if (config.default_menu == NULL) + log_fatal("No default menu defined in config file"); + default_menu = get_menu(config.default_menu); + if (default_menu == NULL) + log_fatal("Default menu %s not found in config file", config.default_menu); + + // Initialize SDL, verify all settings are in their allowable range + init_sdl(); + init_sdl_image(); + init_sdl_ttf(); + validate_settings(&geo); + + // Initialize slideshow + if (config.background_mode == BACKGROUND_SLIDESHOW) + init_slideshow(); + + // Initialize Nanosvg, create window and renderer + init_svg(); + create_window(); + + // Initialize timing + ticks.main = SDL_GetTicks(); + ticks.last_input = ticks.main; + ticks.program_start = ticks.main; + + // Load gamepad overrides + if (config.gamepad_enabled && config.gamepad_mappings_file != NULL) { + error = SDL_GameControllerAddMappingsFromFile(config.gamepad_mappings_file); + if (error < 0) { + log_error("Could not load gamepad mappings from %s\n%s", + config.gamepad_mappings_file, + SDL_GetError() + ); + } } - } - // Render background - if (config.background_mode == MODE_IMAGE) { - if (config.background_image == NULL) { - output_log(LOGLEVEL_ERROR, "Error: BackgroundImage not specified in config file\n"); + // Render background + if (config.background_mode == BACKGROUND_IMAGE) { + if (config.background_image == NULL) + log_error("Background 'Image' setting not specified in config file"); + else + background_texture = load_texture_from_file(config.background_image); + + // Switch to color mode if loading background image failed + if (background_texture == NULL) { + config.background_mode = BACKGROUND_COLOR; + log_error("Couldn't load background image, defaulting to color background"); + set_draw_color(); + } } - else { - background_texture = load_texture_from_file(config.background_image); + + // Render first slideshow image + else if (config.background_mode == BACKGROUND_SLIDESHOW) { + SDL_Surface *surface = load_next_slideshow_background(slideshow, false); + background_texture = load_texture(surface); } - // Switch to color mode if loading background image failed - if (background_texture == NULL) { - config.background_mode = MODE_COLOR; - output_log(LOGLEVEL_ERROR, "Error: Couldn't load background image, defaulting to color background\n"); - set_draw_color(); + // Initialize screensaver + if (config.screensaver_enabled) + init_screensaver(); + + // Initialize clock + if (config.clock_enabled) { + clk = malloc(sizeof(Clock)); + init_clock(clk); + ticks.clock_update = ticks.main; } - } - - // Initialize slideshow - else if (config.background_mode == MODE_SLIDESHOW) { - init_slideshow(); - } - - // Initialize screensaver - if (config.screensaver_enabled) { - init_screensaver(); - } - - // Initialize clock - if (config.clock_enabled) { - launcher_clock = malloc(sizeof(launcher_clock_t)); - init_clock(launcher_clock); - ticks.clock_update = ticks.main; - } - - // Render highlight - int button_height = config.icon_size + config.title_padding + geo.font_height; - highlight = malloc(sizeof(highlight_t)); - highlight->rect.w = config.icon_size + 2*config.highlight_hpadding; - highlight->rect.h = button_height + 2*config.highlight_vpadding; - highlight->texture = render_highlight(highlight->rect.w, - highlight->rect.h, - config.highlight_rx, - NULL - ); - - // Render scroll indicators - if (config.scroll_indicators) { - render_scroll_indicators(); - } - - // Print debug info to log - if (config.debug) { - debug_video(renderer, &display_mode); - debug_settings(); - debug_gamepad(gamepad_controls); - debug_hotkeys(hotkeys); - debug_menu_entries(config.first_menu, config.num_menus); - } - - // Load the default menu and display it - if (config.default_menu == NULL) { - output_log(LOGLEVEL_FATAL, "Fatal Error: No default menu defined in config file\n"); - quit(1); - } - default_menu = get_menu(config.default_menu); - if (default_menu == NULL) { - output_log(LOGLEVEL_FATAL, "Fatal Error: Could not load default menu\n"); - quit(1); - } - error = load_menu(default_menu, false, true); - if (error) { - quit(1); - } - - // Draw initial screen - draw_screen(); - - // Main program loop - output_log(LOGLEVEL_DEBUG, "Begin program loop\n"); - while (1) { - ticks.main = SDL_GetTicks(); - while (SDL_PollEvent(&event)) { - switch(event.type) { - case SDL_QUIT: - quit(0); - break; - - case SDL_KEYDOWN: - ticks.last_input = ticks.main; - handle_keypress(&event.key.keysym); - break; - - case SDL_CONTROLLERDEVICEADDED: - output_log(LOGLEVEL_DEBUG, "Gamepad connected with device index %i\n", event.cdevice.which); - if (event.cdevice.which == config.gamepad_device) { - connect_gamepad(event.cdevice.which); - } - break; - - case SDL_CONTROLLERDEVICEREMOVED: - output_log(LOGLEVEL_DEBUG, "Gamepad removed with device index %i\n", event.cdevice.which); - if (event.cdevice.which == config.gamepad_device) { - SDL_GameControllerClose(gamepad); - gamepad = NULL; - } - break; - - case SDL_WINDOWEVENT: - if (event.window.event == SDL_WINDOWEVENT_SHOWN || - event.window.event == SDL_WINDOWEVENT_EXPOSED || - event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) { - if (config.on_launch == MODE_ON_LAUNCH_BLANK) { - set_draw_color(); - state.screen_updates = true; - } - else { - state.screen_updates = true; - } - output_log(LOGLEVEL_DEBUG, "Redrawing window\n"); - } - else if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) { - output_log(LOGLEVEL_DEBUG, "Lost keyboard focus\n"); - } - else if (event.window.event == SDL_WINDOWEVENT_LEAVE) { - output_log(LOGLEVEL_DEBUG, "Lost mouse focus\n"); - } - break; - } + + // Render highlight + if (config.highlight) { + int button_height = config.icon_size + config.title_padding + geo.font_height; + highlight = malloc(sizeof(Highlight)); + highlight->texture = render_highlight(config.icon_size + 2*config.highlight_hpadding, + button_height + 2*config.highlight_vpadding, + &highlight->rect + ); } - // Post-loop updates - if (gamepad != NULL) { - poll_gamepad(); + // Render scroll indicators + if (config.scroll_indicators) { + scroll = malloc(sizeof(Scroll)); + scroll->texture = NULL; + int scroll_indicator_height = (int) ((float) geo.screen_height * SCROLL_INDICATOR_HEIGHT); + render_scroll_indicators(scroll, scroll_indicator_height, &geo); } - if (config.background_mode == MODE_SLIDESHOW) { - update_slideshow(); + + // Render background overlay + if (config.background_overlay) { + SDL_Surface *overlay_surface = NULL; + overlay_surface = SDL_CreateRGBSurfaceWithFormat(0, + geo.screen_width, + geo.screen_height, + 32, + SDL_PIXELFORMAT_ARGB8888 + ); + Uint32 overlay_color = SDL_MapRGBA(overlay_surface->format, + config.background_overlay_color.r, + config.background_overlay_color.g, + config.background_overlay_color.b, + config.background_overlay_color.a + ); + SDL_FillRect(overlay_surface, NULL, overlay_color); + background_overlay = load_texture(overlay_surface); } - if (config.screensaver_enabled) { - update_screensaver(); - } - if (config.clock_enabled) { - update_clock(false); + + // Register exit hotkey with Windows +#ifdef _WIN32 + if (has_exit_hotkey()) + register_exit_hotkey(); +#endif + + // Print debug info to log + if (config.debug) { + debug_video(renderer, &display_mode); + debug_settings(); + debug_gamepad(gamepad_controls); + debug_hotkeys(hotkeys); + debug_menu_entries(config.first_menu, config.num_menus); + } + + // Load the default menu and display it + error = load_menu(default_menu, false, true); + if (error) + log_fatal("Could not load default menu %s", config.default_menu); + + // Execute startup command + if (config.startup_cmd != NULL) + execute_command(config.startup_cmd); + + // Main program loop + log_debug("Begin program loop"); + while (1) { + ticks.main = SDL_GetTicks(); + while (SDL_PollEvent(&event)) { + switch(event.type) { + case SDL_QUIT: + quit(EXIT_SUCCESS); + break; + + case SDL_KEYDOWN: + ticks.last_input = ticks.main; + handle_keypress(&event.key.keysym); + break; + + case SDL_MOUSEBUTTONDOWN: + if (config.mouse_select && event.button.button == SDL_BUTTON_LEFT) { + ticks.last_input = ticks.main; + execute_command(current_entry->cmd); + } + break; + + case SDL_JOYDEVICEADDED: + if (SDL_IsGameController(event.jdevice.which) == SDL_TRUE) { + log_debug("Gamepad connected with device index %i", event.jdevice.which); + if (config.gamepad_device < 0 || config.gamepad_device == event.jdevice.which) + connect_gamepad(event.jdevice.which, !state.application_running, true); + } + break; + + case SDL_JOYDEVICEREMOVED: + if (SDL_IsGameController(event.jdevice.which) == SDL_TRUE || 1) { + log_debug("Gamepad disconnected"); + if (config.gamepad_device < 0 || config.gamepad_device == event.jdevice.which) + disconnect_gamepad(event.jdevice.which, true, true); + } + break; + + case SDL_WINDOWEVENT: + if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) { + log_debug("Lost keyboard focus"); + state.has_focus = false; + if (state.application_launching) { + log_debug("Application detected"); + state.application_launching = false; + state.application_running = true; + pre_launch(); + } +#ifdef _WIN32 + // Sometimes the launcher will lose focus on Windows when autostarting + // So if we lose the window focus within 10 seconds of the launcher starting + // we will grab back th Window focus. This is a bit of a hack + else if (ticks.main - ticks.program_start < 10000) + set_foreground_window(); +#endif + } + else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) { + log_debug("Gained keyboard focus"); + state.has_focus = true; + } + else if (event.window.event == SDL_WINDOWEVENT_LEAVE) + log_debug("Lost mouse focus"); + break; +#ifdef _WIN32 + case SDL_SYSWMEVENT: + check_exit_hotkey(event.syswm.msg); + break; +#endif + } + } + + // Update application state + if (state.application_running && state.has_focus) { + state.application_running = false; + post_launch(); + log_debug("Application finished"); + } + + // Post-event loop updates + if (!(state.application_running || state.application_launching)) { + if (gamepads != NULL) + poll_gamepad(); + if (config.background_mode == BACKGROUND_SLIDESHOW) + update_slideshow(); + if (config.screensaver_enabled) + update_screensaver(); + if (config.clock_enabled) + update_clock(false); + } + if (state.application_launching && + ticks.main - ticks.application_launched > config.application_timeout) { + state.application_launching = false; + if (config.on_launch == ON_LAUNCH_BLANK) + set_draw_color(); + } + if (state.application_running) + SDL_Delay(APPLICATION_WAIT_PERIOD); + else + draw_screen(); } - if (state.screen_updates) { - draw_screen(); - } - SDL_Delay(POLLING_PERIOD); - } - quit(0); -} \ No newline at end of file + quit(EXIT_SUCCESS); +} diff --git a/src/launcher.h b/src/launcher.h index 4545e06..9d18cda 100644 --- a/src/launcher.h +++ b/src/launcher.h @@ -12,53 +12,38 @@ #endif #define COLOR_MASKS RMASK, GMASK, BMASK, AMASK -// Definitions +// Launcher parameters +#define MIN_FPS_LIMIT 10 #define MIN_ICON_SIZE 32 #define MAX_ICON_SIZE 1024 #define MIN_RX_SIZE 0 #define MAX_RX_SIZE 100 #define PERCENT_MAX_CHARS 10 -#define POLLING_PERIOD 15 #define GAMEPAD_DEADZONE 10000 #define GAMEPAD_REPEAT_DELAY 500 #define GAMEPAD_REPEAT_INTERVAL 25 #define CLOCK_UPDATE_PERIOD 1000 -#define SCROLL_INDICATOR_HEIGHT 0.1F -#define SCREEN_MARGIN 0.06F +#define SCROLL_INDICATOR_HEIGHT 0.11F +#define MAX_SCROLL_INDICATOR_OUTLINE 0.01F +#define SCREEN_MARGIN 0.05F #define MAX_CLOCK_MARGIN 0.1F -#define MIN_BUTTON_CENTERLINE 0.25F -#define MAX_BUTTON_CENTERLINE 0.75F -#define MAX_SLIDESHOW_IMAGES 250 +#define MIN_VCENTER 0.25F +#define MAX_VCENTER 0.75F #define MIN_SLIDESHOW_IMAGE_DURATION 5000 -#define MAX_SLIDESHOW_IMAGE_DURATION 600000 -#define MIN_SLIDESHOW_TRANSITION_TIME 0 +#define MAX_SLIDESHOW_IMAGE_DURATION 3600000 #define MAX_SLIDESHOW_TRANSITION_TIME 3000 #define MIN_SCREENSAVER_IDLE_TIME 3 #define MAX_SCREENSAVER_IDLE_TIME 900 #define SCREENSAVER_TRANSITION_TIME 1500 - -// Modes -typedef int mode; -#define MODE_COLOR 0 -#define MODE_IMAGE 1 -#define MODE_SLIDESHOW 2 -#define MODE_TEXT_TRUNCATE 0 -#define MODE_TEXT_SHRINK 1 -#define MODE_TEXT_NONE 2 -#define DIRECTION_LEFT 0 -#define DIRECTION_RIGHT 1 -#define MODE_ON_LAUNCH_HIDE 0 -#define MODE_ON_LAUNCH_NONE 1 -#define MODE_ON_LAUNCH_BLANK 2 - -// Types -#define TYPE_BUTTON 0 -#define TYPE_AXIS_POS 1 -#define TYPE_AXIS_NEG 2 +#define APPLICATION_WAIT_PERIOD 100 +#define MIN_APPLICATION_TIMEOUT 3 +#define MAX_APPLICATION_TIMEOUT 30 // Special commands #define SCMD_SELECT ":select" #define SCMD_SUBMENU ":submenu" +#define SCMD_FORK ":fork" +#define SCMD_EXIT ":exit" #define SCMD_LEFT ":left" #define SCMD_RIGHT ":right" #define SCMD_HOME ":home" @@ -69,210 +54,266 @@ typedef int mode; #define SCMD_SLEEP ":sleep" typedef enum { - ALIGNMENT_LEFT, - ALIGNMENT_RIGHT, -} launcher_alignment_t; + MODE_SETTING_BACKGROUND, + MODE_SETTING_ON_LAUNCH, + MODE_SETTING_OVERSIZE, + MODE_SETTING_ALIGNMENT, + MODE_SETTING_TIME_FORMAT, + MODE_SETTING_DATE_FORMAT +} ModeSettingType; + +typedef enum { + BACKGROUND_COLOR, + BACKGROUND_IMAGE, + BACKGROUND_SLIDESHOW, + BACKGROUND_TRANSPARENT +} ModeBackground; + +typedef enum { + ON_LAUNCH_BLANK, + ON_LAUNCH_NONE, + ON_LAUNCH_QUIT +} ModeOnLaunch; + +typedef enum { + OVERSIZE_TRUNCATE, + OVERSIZE_SHRINK, + OVERSIZE_NONE +} ModeOversize; +typedef enum { + ALIGNMENT_LEFT, + ALIGNMENT_RIGHT, +} Alignment; + +typedef enum { + FORMAT_TIME_24HR, + FORMAT_TIME_12HR, + FORMAT_TIME_AUTO +} TimeFormat; + +typedef enum { + FORMAT_DATE_BIG, + FORMAT_DATE_LITTLE, + FORMAT_DATE_AUTO +} DateFormat; + +typedef enum { + TYPE_BUTTON, + TYPE_AXIS_POS, + TYPE_AXIS_NEG, +} ControlType; + +typedef enum { + DIRECTION_LEFT, + DIRECTION_RIGHT, +} Direction; + +// Program states typedef struct { - bool screen_updates; - bool slideshow_transition; - bool slideshow_background_rendering; - bool slideshow_background_ready; - bool slideshow_paused; - bool screensaver_active; - bool screensaver_transition; - bool clock_rendering; - bool clock_ready; -} state_t; + bool application_launching; + bool application_running; + bool has_focus; + bool slideshow_transition; + bool slideshow_background_rendering; + bool slideshow_background_ready; + bool slideshow_paused; + bool screensaver_active; + bool screensaver_transition; + bool clock_rendering; + bool clock_ready; +} State; +// Timing information typedef struct { - Uint32 main; - Uint32 slideshow_load; - Uint32 last_input; - Uint32 clock_update; -} ticks_t; + Uint32 main; + Uint32 program_start; + Uint32 application_launched; + Uint32 slideshow_load; + Uint32 last_input; + Uint32 clock_update; + Uint32 application_exited; +} Ticks; // Linked list for menu entries typedef struct entry { - char *title; - char *icon_path; - char *cmd; - SDL_Texture *icon; - SDL_Rect icon_rect; - SDL_Texture *title_texture; - SDL_Rect text_rect; - int title_offset; - struct entry *next; - struct entry *previous; -} entry_t; + char *title; + char *icon_path; + char *icon_selected_path; + char *cmd; + SDL_Texture *icon; + SDL_Texture *icon_selected; + SDL_Rect icon_rect; + SDL_Texture *title_texture; + SDL_Rect text_rect; + int title_offset; + struct entry *next; + struct entry *previous; +} Entry; // Linked list for menus typedef struct menu { - char *name; - int num_entries; - bool rendered; - int page; - int highlight_position; - entry_t *first_entry; - entry_t *root_entry; - entry_t *last_selected_entry; - struct menu *next; - struct menu *back; -} menu_t; + char *name; + unsigned int num_entries; + bool rendered; + unsigned int page; + unsigned int highlight_position; + Entry *first_entry; + Entry *root_entry; + Entry *last_selected_entry; + struct menu *next; + struct menu *back; +} Menu; + +typedef struct gamepad { + SDL_GameController *controller; + int device_index; + int id; + struct gamepad *previous; + struct gamepad *next; +} Gamepad; + +// Linked list of gamepad controls +typedef struct gamepad_control { + ControlType type; + int index; + Uint32 repeat; + const char *label; + char *cmd; + struct gamepad_control *next; +} GamepadControl; +// Linked list of hotkeys typedef struct hotkey { - SDL_Keycode keycode; - char *cmd; - struct hotkey *next; -} hotkey_t; + SDL_Keycode keycode; + char *cmd; + struct hotkey *next; +} Hotkey; // Struct for the geometry parameters of the onscreen buttons typedef struct { - int screen_width; - int screen_height; - int screen_margin; - int font_height; - int x_margin; // Distance between left edge of screen and x coordinate of root_entry icon - int y_margin; // Distance between top edge of screen and y coordinate of all entry icons - int x_advance; // Distance between icon x coordinate of adjacent entries - int num_buttons; // Number of buttons shown on the screen -} geometry_t; + int screen_width; + int screen_height; + int screen_margin; + int font_height; + int x_margin; // Distance between left edge of screen and x coordinate of root_entry icon + int y_margin; // Distance between top edge of screen and y coordinate of all entry icons + int x_advance; // Distance between icon x coordinate of adjacent entries + int num_buttons; // Number of buttons shown on the screen +} Geometry; // Struct for highlight typedef struct { - SDL_Texture *texture; - SDL_Rect rect; -} highlight_t; + SDL_Texture *texture; + SDL_Rect rect; +} Highlight; //Struct for scroll indicators typedef struct { - SDL_Texture *texture; - SDL_Rect rect_right; - SDL_Rect rect_left; -} scroll_t; - -// Linked list of gamepad controls -typedef struct gamepad_control { - int type; - int index; - int repeat; - char *label; - char *cmd; - struct gamepad_control *next; -} gamepad_control_t; + SDL_Texture *texture; + SDL_Rect rect_right; + SDL_Rect rect_left; +} Scroll; // Slideshow typedef struct { - char *images[MAX_SLIDESHOW_IMAGES]; - int order[MAX_SLIDESHOW_IMAGES]; - int i; - int num_images; - float transition_alpha; - float transition_change_rate; - SDL_Surface *transition_surface; - SDL_Texture *transition_texture; -} slideshow_t; + char **images; + int *order; + int i; + int num_images; + float transition_alpha; + float transition_change_rate; + SDL_Surface *transition_surface; + SDL_Texture *transition_texture; +} Slideshow; // Screensaver typedef struct { - float alpha; - float alpha_end_value; - float transition_change_rate; - SDL_Texture *texture; -} screensaver_t; - -typedef enum { - FORMAT_TIME_AUTO, - FORMAT_TIME_12HR, - FORMAT_TIME_24HR -} time_format_t; - -typedef enum { - FORMAT_DATE_AUTO, - FORMAT_DATE_LITTLE, - FORMAT_DATE_BIG -} date_format_t; + float alpha; + float alpha_end_value; + float transition_change_rate; + SDL_Texture *texture; +} Screensaver; // Configuration settings typedef struct { - char *default_menu; - unsigned int max_buttons; - mode background_mode; // Defines image or color background mode - SDL_Color background_color; // Background color - char *background_image; // Path to background image - char *slideshow_directory; - Uint16 icon_size; - int icon_spacing; - char icon_spacing_str[PERCENT_MAX_CHARS]; - char *title_font_path; // Path to title TTF font file - unsigned int font_size; - SDL_Color title_color; // Color struct for title text - char title_opacity[PERCENT_MAX_CHARS]; - mode title_oversize_mode; - unsigned int title_padding; - SDL_Color highlight_color; - char highlight_opacity[PERCENT_MAX_CHARS]; - unsigned int highlight_rx; - int highlight_vpadding; - int highlight_hpadding; - char button_centerline[PERCENT_MAX_CHARS]; - bool scroll_indicators; - SDL_Color scroll_indicator_color; - char scroll_indicator_opacity[PERCENT_MAX_CHARS]; - bool reset_on_back; - mode on_launch; - bool screensaver_enabled; - Uint32 screensaver_idle_time; - char screensaver_intensity_str[PERCENT_MAX_CHARS]; - bool screensaver_pause_slideshow; - bool gamepad_enabled; - int gamepad_device; - char *gamepad_mappings_file; - bool debug; - char *exe_path; - menu_t *first_menu; - int num_menus; - bool clock_enabled; - bool clock_show_date; - launcher_alignment_t clock_alignment; - char *clock_font_path; - char clock_margin_str[PERCENT_MAX_CHARS]; - int clock_margin; - SDL_Color clock_color; - char clock_opacity[PERCENT_MAX_CHARS]; - unsigned int clock_font_size; - time_format_t clock_time_format; - date_format_t clock_date_format; - bool clock_include_weekday; - Uint32 slideshow_image_duration; - Uint32 slideshow_transition_time; -} config_t; + char *default_menu; + unsigned int max_buttons; + bool vsync; + int fps_limit; + Uint32 application_timeout; + ModeBackground background_mode; // Defines image or color background mode + SDL_Color background_color; // Background color + SDL_Color chroma_key_color; + char *background_image; // Path to background image + char *slideshow_directory; + bool background_overlay; + SDL_Color background_overlay_color; + char background_overlay_opacity[PERCENT_MAX_CHARS]; + Uint16 icon_size; + int icon_spacing; + char icon_spacing_str[PERCENT_MAX_CHARS]; + bool titles_enabled; + char *title_font_path; // Path to title TTF font file + unsigned int title_font_size; + SDL_Color title_font_color; // Color struct for title text + bool title_shadows; + SDL_Color title_shadow_color; + char title_opacity[PERCENT_MAX_CHARS]; + ModeOversize title_oversize_mode; + int title_padding; + bool highlight; + SDL_Color highlight_fill_color; + SDL_Color highlight_outline_color; + int highlight_outline_size; + char highlight_fill_opacity[PERCENT_MAX_CHARS]; + char highlight_outline_opacity[PERCENT_MAX_CHARS]; + unsigned int highlight_rx; + int highlight_vpadding; + int highlight_hpadding; + char vcenter[PERCENT_MAX_CHARS]; + bool scroll_indicators; + SDL_Color scroll_indicator_fill_color; + int scroll_indicator_outline_size; + SDL_Color scroll_indicator_outline_color; + char scroll_indicator_opacity[PERCENT_MAX_CHARS]; + bool wrap_entries; + bool reset_on_back; + bool mouse_select; + bool inhibit_os_screensaver; + char *startup_cmd; + char *quit_cmd; + ModeOnLaunch on_launch; + bool screensaver_enabled; + Uint32 screensaver_idle_time; + char screensaver_intensity_str[PERCENT_MAX_CHARS]; + bool screensaver_pause_slideshow; + bool gamepad_enabled; + int gamepad_device; + char *gamepad_mappings_file; + bool debug; + char *exe_path; + Menu *first_menu; + size_t num_menus; + bool clock_enabled; + bool clock_show_date; + Alignment clock_alignment; + char *clock_font_path; + char clock_margin_str[PERCENT_MAX_CHARS]; + int clock_margin; + SDL_Color clock_font_color; + char clock_opacity[PERCENT_MAX_CHARS]; + unsigned int clock_font_size; + bool clock_shadows; + SDL_Color clock_shadow_color; + TimeFormat clock_time_format; + DateFormat clock_date_format; + bool clock_include_weekday; + Uint32 slideshow_image_duration; + Uint32 slideshow_transition_time; +} Config; -// Function prototypes -static int init_sdl(void); -static int init_ttf(void); -static int load_menu(menu_t *menu, bool set_back_menu, bool reset_position); -static int load_menu_by_name(const char *menu_name, bool set_back_menu, bool reset_position); -static void update_slideshow(void); -static void resume_slideshow(void); -static void update_screensaver(void); -static void update_clock(bool block); -static void init_slideshow(void); -static void init_screensaver(void); void quit_slideshow(void); void set_draw_color(void); -static void calculate_button_geometry(entry_t *entry, int buttons); -static void render_buttons(menu_t *menu); -static void draw_buttons(entry_t *entry); -static void move_left(void); -static void move_right(void); -static void load_submenu(const char *submenu); -static void load_back_menu(menu_t *menu); -static void draw_screen(void); -static void handle_keypress(SDL_Keysym *key); -static void execute_command(const char *command); -static void poll_gamepad(void); -static void connect_gamepad(int device_index); -static void render_scroll_indicators(void); void quit(int status); -static void cleanup(void); \ No newline at end of file +void print_version(FILE *stream); diff --git a/src/platform/CMakeLists.txt b/src/platform/CMakeLists.txt index 71a530e..764892e 100644 --- a/src/platform/CMakeLists.txt +++ b/src/platform/CMakeLists.txt @@ -1,10 +1,12 @@ +# Build platform-specific library if (UNIX) - add_library(platform "unix.c") - target_include_directories(platform PUBLIC "${SDL2_INCLUDE_DIRS}" "${PROJECT_BINARY_DIR}") + add_library(platform unix.c unix.h platform.h) + target_link_libraries(platform PkgConfig::SDL2) endif () - if (WIN32) - add_library(platform "win32.c") - target_include_directories(platform PUBLIC "${PROJECT_BINARY_DIR}") - target_link_libraries(platform SDL2::SDL2 SDL2::SDL2main SDL2::SDL2-static) + add_library(platform win32.c platform.h) + target_link_libraries(platform + $ + $,SDL2::SDL2,SDL2::SDL2-static> + ) endif () diff --git a/src/platform/keycode_convert.h b/src/platform/keycode_convert.h new file mode 100644 index 0000000..df0afee --- /dev/null +++ b/src/platform/keycode_convert.h @@ -0,0 +1,30 @@ +typedef struct { + SDL_Keycode sdl; + UINT win; +} keycode_conversion; + +static const keycode_conversion table[] = { + {SDLK_F1, VK_F1}, + {SDLK_F2, VK_F2}, + {SDLK_F3, VK_F3}, + {SDLK_F4, VK_F4}, + {SDLK_F5, VK_F5}, + {SDLK_F6, VK_F6}, + {SDLK_F7, VK_F7}, + {SDLK_F8, VK_F8}, + {SDLK_F9, VK_F9}, + {SDLK_F10, VK_F10}, + {SDLK_F11, VK_F11}, + {SDLK_F13, VK_F13}, + {SDLK_F14, VK_F14}, + {SDLK_F15, VK_F15}, + {SDLK_F16, VK_F16}, + {SDLK_F17, VK_F17}, + {SDLK_F18, VK_F18}, + {SDLK_F19, VK_F19}, + {SDLK_F20, VK_F20}, + {SDLK_F21, VK_F21}, + {SDLK_F22, VK_F22}, + {SDLK_F23, VK_F23}, + {SDLK_F24, VK_F24} +}; diff --git a/src/platform/platform.h b/src/platform/platform.h new file mode 100644 index 0000000..a0c6970 --- /dev/null +++ b/src/platform/platform.h @@ -0,0 +1,32 @@ +#ifdef _WIN32 +#define FILE_MODE_WRITE "wt" +#else +#define FILE_MODE_WRITE "w" +#endif + +// Abstracted platform function prototypes +bool file_exists(const char *path); +bool directory_exists(const char *path); +void get_region(char *buffer); +void scan_slideshow_directory(Slideshow *slideshow, const char *directory); +bool start_process(char *cmd, bool application); +void scmd_shutdown(void); +void scmd_restart(void); +void scmd_sleep(void); + +// Linux-specific function prototypes +#ifdef __unix__ +void make_directory(const char *directory); +void print_usage(void); +#endif + +// Windows-specific function prototypes +#ifdef _WIN32 +bool has_exit_hotkey(void); +void set_exit_hotkey(SDL_Keycode keycode); +void register_exit_hotkey(void); +void check_exit_hotkey(SDL_SysWMmsg *msg); +void set_foreground_window(void); +void set_window_transparent(void); +void hide_cursor(Entry* entry); +#endif \ No newline at end of file diff --git a/src/platform/slideshow.h b/src/platform/slideshow.h index 0e6372d..3970a6c 100644 --- a/src/platform/slideshow.h +++ b/src/platform/slideshow.h @@ -1,2 +1,7 @@ -const char *extensions[] = {".jpg", ".jpeg", ".png", ".webp"}; +static const char *extensions[] = { + ".jpg", + ".jpeg", + ".png", + ".webp" +}; #define NUM_IMAGE_EXTENSIONS sizeof(extensions) / sizeof(extensions[0]) \ No newline at end of file diff --git a/src/platform/unix.c b/src/platform/unix.c index d2e39eb..83062cd 100755 --- a/src/platform/unix.c +++ b/src/platform/unix.c @@ -2,217 +2,235 @@ #include #include #include -#include +#include +#include +#include #include -#include "../external/ini.h" +#include #include "../launcher.h" #include #include "unix.h" #include "../util.h" #include "../debug.h" +#include "platform.h" #include "slideshow.h" +static int desktop_handler(void *user, const char *section, const char *name, const char *value); +static void strip_field_codes(char *cmd); +static bool ends_with(const char *string, const char *phrase); +static int image_filter(const struct dirent *file); + // A function to handle .desktop lines static int desktop_handler(void *user, const char *section, const char *name, const char *value) { - desktop_t *pdesktop = (desktop_t*) user; - if (!strcmp(pdesktop->section, section) && !strcmp(name, KEY_EXEC)) { - copy_string(&pdesktop->exec, value); - } + Desktop *pdesktop = (Desktop*) user; + if (!strcmp(pdesktop->section, section) && !strcmp(name, KEY_EXEC)) + pdesktop->exec = strdup(value); + return 0; } // A function to determine if a file exists in the filesystem bool file_exists(const char *path) { - if(access(path, R_OK) == 0) { - return true; - } - else { - return false; - } + return access(path, R_OK) ? false : true; } // A function to determine if a directory exists in the filesystem bool directory_exists(const char *path) { - struct stat directory; - if (stat(path, &directory) == 0 && S_ISDIR(directory.st_mode)) { - return true; - } - else { - return false; - } + struct stat directory; + return stat(path, &directory) == 0 && S_ISDIR(directory.st_mode) ? true : false; } // A function to remove field codes from .desktop file Exec line static void strip_field_codes(char *cmd) { - int start = 0; - for (int i = 0; i < strlen(cmd); i++) { - if (cmd[i] == '%' && i > 0 && cmd[i - 1] == ' ') { - start = i; + size_t start = 0; + for (size_t i = 0; i < strlen(cmd); i++) { + if (cmd[i] == '%' && i > 0 && cmd[i - 1] == ' ') + start = i; + else if (start && i > start + 2 && cmd[i] != ' ') { + strcpy(cmd + start, cmd + i); + start = 0; + } } - else if (start && i > start + 2 && cmd[i] != ' ') { - strcpy(cmd + start, cmd + i); - start = 0; - } - } - if (start) { - cmd[start - 1] ='\0'; - } + if (start) + cmd[start - 1] ='\0'; } // A function to make a directory, including any intermediate // directories if necessary void make_directory(const char *directory) { - char buffer[MAX_PATH_CHARS + 1]; - char *i = NULL; - int length; - snprintf(buffer, sizeof(buffer), "%s", directory); - length = strlen(buffer); - if (buffer[length - 1] == '/') { - buffer[length - 1] = '\0'; - } - for (i = buffer + 1; *i != '\0'; i++) { - if (*i == '/') { - *i = '\0'; - mkdir(buffer, S_IRWXU); - *i = '/'; + char buffer[MAX_PATH_CHARS + 1]; + char *i = NULL; + size_t length; + snprintf(buffer, sizeof(buffer), "%s", directory); + length = strlen(buffer); + if (buffer[length - 1] == '/') + buffer[length - 1] = '\0'; + for (i = buffer + 1; *i != '\0'; i++) { + if (*i == '/') { + *i = '\0'; + mkdir(buffer, S_IRWXU); + *i = '/'; + } } - } - mkdir(buffer, S_IRWXU); + mkdir(buffer, S_IRWXU); } // A function to determine if a string ends with a phrase static bool ends_with(const char *string, const char *phrase) { - int len_string = strlen(string); - int len_phrase = strlen(phrase); - if (len_phrase > len_string) { - return false; - } - char *p = string + len_string - len_phrase; - if (!strcmp(p, phrase)) { - return true; - } - else { - return false; - } + size_t len_string = strlen(string); + size_t len_phrase = strlen(phrase); + if (len_phrase > len_string) + return false; + char *p = (char*) string + len_string - len_phrase; + return strcmp(p, phrase) ? false : true; } // A function to launch an external application -void launch_application(char *cmd) +bool start_process(char *cmd, bool application) { - // Check if the command is an XDG .desktop file - char *tmp = NULL; - char *exec = NULL; - copy_string(&tmp, cmd); - char *file = strtok(tmp, DELIMITER_ACTION); - if (ends_with(file, EXT_DESKTOP)) { - desktop_t desktop; - desktop.exec = NULL; - - // Parse the desktop action from the command (if any) - char *action = strtok(NULL, DELIMITER_ACTION); - if (action == NULL) { - strncpy(desktop.section, DESKTOP_SECTION_HEADER, sizeof(desktop.section)); - } - else { - snprintf(desktop.section, sizeof(desktop.section), DESKTOP_SECTION_HEADER_ACTION, action); - } + // Check if the command is an XDG .desktop file + char *exec = NULL; + char *tmp = strdup(cmd); + char *file = strtok(tmp, DELIMITER_ACTION); + if (ends_with(file, EXT_DESKTOP)) { + Desktop desktop; + desktop.exec = NULL; + + // Parse the desktop action from the command (if any) + const char* const action = strtok(NULL, DELIMITER_ACTION); + if (action == NULL) + copy_string(desktop.section, DESKTOP_SECTION_HEADER, sizeof(desktop.section)); + else + snprintf(desktop.section, sizeof(desktop.section), DESKTOP_SECTION_HEADER_ACTION, action); - // Parse the .desktop file for the Exec line value - int error = ini_parse(file, desktop_handler, &desktop); - if (error < 0) { - output_log(LOGLEVEL_ERROR, "Error: Desktop file \"%s\" not found\n", file); - free(tmp); - return; + // Parse the .desktop file for the Exec line value + int error = ini_parse(file, desktop_handler, &desktop); + if (error < 0) { + log_error("Desktop file '%s' not found", file); + free(tmp); + return false; + } + if (desktop.exec == NULL) { + log_debug("No Exec line found in desktop file '%s'", cmd); + free(tmp); + return false; + } + exec = desktop.exec; + strip_field_codes(exec); + cmd = exec; } - if (desktop.exec == NULL) { - output_log(LOGLEVEL_DEBUG, "No Exec line found in desktop file \"%s\"\n", cmd); - free(tmp); - return; + free(tmp); + + // Fork new system shell process + pid_t child_pid = fork(); + switch(child_pid) { + case -1: + log_error("Could not fork new process for application"); + free(exec); + return false; + + // Child process + case 0: + setpgid(0, 0); + const char *file = "/bin/sh"; + const char *args[] = { + "sh", + "-c", + cmd, + NULL + }; + execvp(file, (char* const*) args); + break; + + // Parent process + default: + if (!application) + return true; + int status; + + // Check to see if the shell successfully launched + SDL_Delay(10); + waitpid(child_pid, &status, WNOHANG); + if (WIFEXITED(status) && WEXITSTATUS(status) > 126) { + log_error("Application failed to launch"); + return false; + } + log_debug("Application launched successfully"); + break; } - exec = desktop.exec; - strip_field_codes(exec); - cmd = exec; - } - free(tmp); - - // Launch application in system shell - system(cmd); - free(exec); + free(exec); + return true; } -int image_filter(struct dirent *file) +// A function to determine if a file is an image file +int image_filter(const struct dirent *file) { - int len_file = strlen(file->d_name); - int len_extension; - for (int i = 0; i < NUM_IMAGE_EXTENSIONS; i++) { - len_extension = strlen(extensions[i]); - if (len_file > len_extension && - !strcmp(file->d_name + len_file - len_extension, extensions[i])) { - return 1; + size_t len_file = strlen(file->d_name); + size_t len_extension; + for (size_t i = 0; i < NUM_IMAGE_EXTENSIONS; i++) { + len_extension = strlen(extensions[i]); + if (len_file > len_extension && + !strcmp(file->d_name + len_file - len_extension, extensions[i])) + return 1; } - } - return 0; + return 0; } -int scan_slideshow_directory(slideshow_t *slideshow, const char *directory) +// A function to scan a directory for images +void scan_slideshow_directory(Slideshow *slideshow, const char *directory) { - struct dirent **files; - int n = scandir(directory, &files, image_filter, NULL); - char file_path[MAX_PATH_CHARS + 1]; - for (int i = 0; i < n; i++) { - if (i < MAX_SLIDESHOW_IMAGES) { - join_paths(file_path, 2, directory, files[i]->d_name); - copy_string(&slideshow->images[i], file_path); + struct dirent **files; + slideshow->num_images = scandir(directory, &files, image_filter, NULL); + slideshow->images = malloc((size_t) slideshow->num_images * sizeof(char*)); + char file_path[MAX_PATH_CHARS + 1]; + for (int i = 0; i < slideshow->num_images; i++) { + join_paths(file_path, sizeof(file_path), 2, directory, files[i]->d_name); + slideshow->images[i] = strdup(file_path); + free(files[i]); } - free(files[i]); - } - free(files); - if (n <= MAX_SLIDESHOW_IMAGES) { - slideshow->num_images = n; - } - else { - slideshow->num_images = MAX_SLIDESHOW_IMAGES; - } - return slideshow->num_images; + free(files); } void get_region(char *buffer) { - char *lang = getenv("LANG"); - char *token = strtok(lang, "_"); - if (token == NULL) { - return; - } - token = strtok(NULL, "."); - if (token != NULL && strlen(token) == 2) { - strcpy(buffer, token); - } + char *lang = getenv("LANG"); + char *token = strtok(lang, "_"); + if (token == NULL) + return; + token = strtok(NULL, "."); + if (token != NULL && strlen(token) == 2) + copy_string(buffer, token, 3); } -// A function to print usage to the command line -void print_usage() +// A function to shutdown the computer +void scmd_shutdown() +{ + start_process(CMD_SHUTDOWN, false); +} + +// A function to restart the computer +void scmd_restart() { - printf("Usage: %s [OPTIONS]\n", EXECUTABLE_TITLE); - printf("-c, --config Path to config file.\n"); - printf("-d, --debug Enable debug messages.\n"); - printf("-h, --help Show this help message.\n"); - printf("-v, --version Print version information.\n"); + start_process(CMD_RESTART, false); } -// A function to print the version and other info to command line -void print_version() +// A function to put the computer to sleep +void scmd_sleep() { - #if (PROJECT_VERSION_PATCH + 0) - printf("%s version %i.%i.%i\n", PROJECT_NAME, - PROJECT_VERSION_MAJOR, - PROJECT_VERSION_MINOR, - PROJECT_VERSION_PATCH); - #else - printf("%s version %i.%i\n", PROJECT_NAME, PROJECT_VERSION_MAJOR, PROJECT_VERSION_MINOR); - #endif -} \ No newline at end of file + start_process(CMD_SLEEP, false); +} + +// A function to print usage to the command line +void print_usage() +{ + printf("Usage: " EXECUTABLE_TITLE " [OPTIONS]\n"); + printf(" -c p, --config=p Load config file from path p.\n"); + printf(" -d, --debug Enable debug messages.\n"); + printf(" -h, --help Show this help message.\n"); + printf(" -v, --version Print version information.\n"); +} diff --git a/src/platform/unix.h b/src/platform/unix.h index 0597d3f..4aa152c 100755 --- a/src/platform/unix.h +++ b/src/platform/unix.h @@ -1,3 +1,8 @@ +#include + +#define CMD_SHUTDOWN "systemctl poweroff" +#define CMD_RESTART "systemctl reboot" +#define CMD_SLEEP "systemctl suspend" #define EXT_DESKTOP ".desktop" #define DELIMITER_ACTION ";" #define DESKTOP_SECTION_HEADER "Desktop Entry" @@ -6,12 +11,6 @@ #define MAX_INI_SECTION 100 typedef struct { - char section[MAX_INI_SECTION + 1]; - char *exec; -} desktop_t; - -static int desktop_handler(void *user, const char *section, const char *name, const char *value); -void make_directory(const char *directory); -static void strip_field_codes(char *cmd); -static bool ends_with(const char *string, const char *phrase); -static int image_filter(struct dirent *file); + char section[MAX_INI_SECTION + 1]; + char *exec; +} Desktop; diff --git a/src/platform/win32.c b/src/platform/win32.c index 093f9ac..05a12ab 100644 --- a/src/platform/win32.c +++ b/src/platform/win32.c @@ -4,388 +4,337 @@ #include #include #include -#include -#include -#include -#include -#include #include -#include -#include #include #include #include "../launcher.h" #include #include "platform.h" -#include "win32.h" #include "../util.h" #include "../debug.h" #include "slideshow.h" -extern config_t config; +static void parse_command(char *cmd, char *file, size_t file_size, char **params); +static char *path_basename(const char *path); +static bool is_browser(const char *exe_basename); +static UINT sdl_to_win32_keycode(SDL_Keycode keycode); +static bool get_shutdown_privilege(void); + +extern Config config; extern SDL_SysWMinfo wm_info; -bool web_browser; -LPWSTR w_process_basename = NULL; +bool has_shutdown_privilege = false; +UINT exit_hotkey = 0; + -// A function to get the basename of a file -static LPWSTR path_basename(LPCWSTR w_path) +// A function to determine if a file exists on the filesystem +bool file_exists(const char *path) { - size_t length = wcslen(w_path); - if (length) { - // Find first path separator, count back from end of string - LPWSTR p = w_path + length - 1; - while (*p != L'\\' && *p != L'/' && p > w_path) { - p -= 1; - } - return p + 1; - } - return NULL; + return _access(path, 4) ? false : true; } -// A function to determine if a process name is a web browser -static bool is_browser(LPCWSTR w_exe_basename) +// A function to determine if a directory exists on the filesystem +bool directory_exists(const char *path) { - LPCWSTR *browsers[] = { - L"chrome.exe", - L"msedge.exe", - L"firefox.exe" - }; - for (int i = 0; i < sizeof(browsers) / sizeof(browsers[0]); i++) { - if (!wcscmp(w_exe_basename, browsers[i])) { - return true; + if (!file_exists(path)) + return false; + else { + DWORD attributes = GetFileAttributesA(path); + if (attributes & FILE_ATTRIBUTE_DIRECTORY) + return true; + else + return false; } - } - return false; } -// A function to convert a UTF-8 string to UTF-16 -static void convert_utf8_string(LPWSTR w_string, const char *string, int buffer_size) +// A function that parses the command string into a file and parameters +static void parse_command(char *cmd, char *file, size_t file_size, char **params) { - MultiByteToWideChar(CP_UTF8, - 0, - string, - -1, - w_string, - buffer_size - ); -} + char *start = NULL; + char *quote_begin = NULL; + char *quote_end = NULL; + char *p = cmd; + file[0] = '\0'; + + // Skip any whitespace at beginning of command + while (*p == ' ') + p++; + start = p; + + // Check for quote, in which case ignore spaces until the end quote is detecetd + if (*p == '"') + quote_begin = p; + + while (*p != '\0') { + // If the quote is complete, copy the file + if (*p == '"' && p != quote_begin) { + quote_end = p; + *p = '\0'; + copy_string(file, quote_begin + 1, file_size); + } -// A function to convert a UTF-16 string to UTF-8 -static void convert_utf16_string(char *string, LPCWSTR w_string, int buffer_size) -{ - WideCharToMultiByte(CP_UTF8, - 0, - w_string, - -1, - string, - buffer_size, - NULL, - NULL - ); -} + else if (*p == ' ') { + // If a space was detected but there hasn't been a quote detected yet, this is the end of the file + if (!quote_begin) { + *p = '\0'; + copy_string(file, start, file_size); + p++; + + // Skip any preceding white space for parameters + while (*p == ' ') + p++; + + // Copy parameters + if (*p != '\0') + *params = strdup(p); + break; + } + + // If a space was detected after the quote + else if (quote_begin && quote_end) { + // Skip any preceding white space for parameters + while (*p == ' ') + p++; + + // Copy rest of command as parameters + if (*p != '\0') + *params = strdup(p); + break; + } + } + p++; + } -// A function to convert a UTF-8 string to UTF-16, and allocate memory for it -static void convert_utf8_alloc(LPWSTR *buffer, const char *string) -{ - int length = strlen(string); - int buffer_size = sizeof(WCHAR)*(length + 1); - *buffer = malloc(buffer_size); - convert_utf8_string(*buffer, string, buffer_size); + // If there were no quotes or spaces, copy whole command into file buffer + if (start && file[0] == '\0') + copy_string(file, start, file_size); } -static void convert_utf16_alloc(const char **buffer, LPCWSTR w_string) +void set_foreground_window() { - size_t length = wcslen(w_string); - int buffer_size = 2*sizeof(WCHAR)*(length + 1); - *buffer = malloc(buffer_size); - convert_utf16_string(*buffer, w_string, buffer_size); + SetForegroundWindow(wm_info.info.win.window); } -// A function to determine if a file with wide char path exists on the filesystem -static bool w_file_exists(LPCWSTR w_path) +void make_window_transparent() { - if (_waccess(w_path, 4) == 0) { - return true; - } - else { - return false; - } + HWND hwnd = wm_info.info.win.window; + SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED); + SetLayeredWindowAttributes(hwnd, + RGB(config.chroma_key_color.r, config.chroma_key_color.g, config.chroma_key_color.b), + 0, + LWA_COLORKEY + ); } -// A function to determine if a file exists on the filesystem -bool file_exists(const char *path) +// When the window is transparent, we need to hide the cursor behind the non-transparent icon +void hide_cursor(Entry *entry) { - WCHAR w_path[MAX_PATH_CHARS + 1]; - convert_utf8_string(w_path, path, sizeof(w_path)); - return w_file_exists(w_path); + SetCursorPos(entry->icon_rect.x + entry->icon_rect.w / 2, + entry->icon_rect.y + entry->icon_rect.h / 2 + ); } -// A function to determine if a directory exists on the filesystem -bool directory_exists(const char *path) +// A function to launch an application +bool start_process(char *cmd, bool application) { - WCHAR w_path[MAX_PATH_CHARS + 1]; - convert_utf8_string(w_path, path, sizeof(w_path)); - if (!w_file_exists(w_path)) { - return false; - } - else { - DWORD attributes = GetFileAttributesW(w_path); - if (attributes & FILE_ATTRIBUTE_DIRECTORY) { - return true; - } + bool ret = false; + char file[MAX_PATH_CHARS + 1]; + char *params = NULL; + int cmd_show = application ? SW_SHOWMAXIMIZED : SW_HIDE; + + // Parse command into file and parameters strings + parse_command(cmd, file, sizeof(file), ¶ms); + + // Set up info struct + SHELLEXECUTEINFOA info = { + .cbSize = sizeof(SHELLEXECUTEINFOA), + .fMask = SEE_MASK_NOCLOSEPROCESS, + .hwnd = NULL, + .lpVerb = "open", + .lpFile = file, + .lpParameters = params, + .lpDirectory = NULL, + .nShow = cmd_show, + .lpIDList = NULL, + .lpClass = NULL, + }; + + BOOL successful = ShellExecuteExA(&info); + if (!application) + ret = true; else { - return false; + // Go down in the window stack so the launched application can take focus + if (successful) { + HWND hwnd = wm_info.info.win.window; + SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOREDRAW | SWP_NOSIZE | SWP_NOMOVE); + ret = true; + } + else { + log_debug("Failed to launch command"); + ret = false; + } } - } + free(params); + return ret; } -// A function that parses the command string into a file and parameters -static void parse_command(char *cmd, LPWSTR w_file, LPWSTR *w_params) +// A function to scan the slideshow directory for image files +void scan_slideshow_directory(Slideshow *slideshow, const char *directory) { - char *start = NULL; - char *quote_begin = NULL; - char *quote_end = NULL; - char *p = cmd; - w_file[0] = L'\0'; - - // Skip any whitespace at beginning of command - while (*p == ' ') { - p++; - } - start = p; - - // Check for quote, in which case ignore spaces until the end quote is detecetd - if (*p == '"') { - quote_begin = p; - } - - while (*p != '\0') { - // If the quote is complete, copy the file - if (*p == '"' && p != quote_begin) { - quote_end = p; - *p = '\0'; - convert_utf8_string(w_file, quote_begin + 1, sizeof(WCHAR)*(MAX_PATH_CHARS + 1)); - } - - else if (*p == ' ') { - // If a space was detected but there hasn't been a quote detected yet, this is the end of the file - if (!quote_begin) { - *p = '\0'; - convert_utf8_string(w_file, start, sizeof(WCHAR)*(MAX_PATH_CHARS + 1)); - p++; - - // Skip any preceding white space for parameters - while (*p == ' ') { - p++; + WIN32_FIND_DATAA data; + HANDLE handle; + char file_search[MAX_PATH_CHARS + 1]; + char file_output[MAX_PATH_CHARS + 1]; + char extension[10]; + + // Generate a wildcard file search string for all supported image file extensions + for (int i = 0; i < NUM_IMAGE_EXTENSIONS; i++) { + copy_string(extension, "*", sizeof(extension)); + strcat(extension, extensions[i]); + join_paths(file_search, sizeof(file_search), 2, directory, extension); + + // Store every result into the slideshow struct + handle = FindFirstFileA(file_search, &data); + if (handle != INVALID_HANDLE_VALUE) { + do { + join_paths(file_output, sizeof(file_output), 2, directory, data.cFileName); + slideshow->images = realloc(slideshow->images, (slideshow->num_images + 1) * sizeof(char*)); + slideshow->images[slideshow->num_images] = strdup(file_output); + slideshow->num_images++; + } while (FindNextFileA(handle, &data) != 0); } - - // Copy parameters - if (*p != '\0') { - convert_utf8_alloc(w_params, p); - break; - } - } - - // If a space was detected after the quote - else if (quote_begin && quote_end) { - // Skip any preceding white space for parameters - while (*p == ' ') { - p++; - } - - // Copy rest of command as parameters - if (*p != '\0') { - convert_utf8_alloc(w_params, p); - } - break; - } } - p++; - } - - // If there were no quotes or spaces, copy whole command into file buffer - if (start && w_file[0] == L'\0') { - convert_utf8_string(w_file, start, sizeof(WCHAR)*(MAX_PATH_CHARS + 1)); - } } -// A function to determine if a process of a given name is running on the system -static bool process_running_name(LPCWSTR w_target_process) +// A function to get the 2 letter region code +void get_region(char *buffer) { - static Uint32 ticks = 0; - Uint32 current_ticks = SDL_GetTicks(); + GEOID geo_id = GetUserGeoID(GEOCLASS_NATION); + GetGeoInfoA(geo_id, GEO_ISO2, buffer, 3, 0); +} - // Only check the process if we've exceeded the process check interval - if (current_ticks - ticks < BROWSER_CHECK_PERIOD) { - return true; - } - else { - ticks = current_ticks; - - // Generate an array of process IDs - DWORD processes[1024], needed, num_processes; - if (!EnumProcesses(processes, sizeof(processes), &needed)) { - return false; - } - num_processes = needed / sizeof(DWORD); - WCHAR w_current_process[MAX_PATH + 1]; - HANDLE process = NULL; - HMODULE module = NULL; - DWORD flags = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; - - // Get the image name of each running process - for (int i = 0; i < num_processes; i++) { - process = OpenProcess(flags, FALSE, processes[i]); - if (process != NULL && - EnumProcessModulesEx(process, &module, sizeof(module), &needed, LIST_MODULES_ALL)) { - GetModuleBaseNameW(process, module, w_current_process, sizeof(w_current_process) / sizeof(WCHAR)); - - // Check if the target process's name is the same as the current process - if (!wcscmp(w_current_process, w_target_process)) { - CloseHandle(process); - return true; - } - CloseHandle(process); - } +// A function to shutdown the computer +void scmd_shutdown() +{ + if (!has_shutdown_privilege) { + bool successful = get_shutdown_privilege(); + if (!successful) + return; } - return false; - } + InitiateShutdownA(NULL, + NULL, + 0, + SHUTDOWN_FORCE_OTHERS | SHUTDOWN_POWEROFF | SHUTDOWN_HYBRID, + SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_MINOR_OTHER + ); } -// A function to determine if the launched process is still running -bool process_running(HANDLE process) +// A function to restart the computer +void scmd_restart() { - if (web_browser) { - return process_running_name(w_process_basename); - } - else { - DWORD status = WaitForSingleObject(process, 0); - if (status == WAIT_OBJECT_0) { - return false; + if (!has_shutdown_privilege) { + bool successful = get_shutdown_privilege(); + if (!successful) + return; } - else { - return true; + InitiateShutdownA(NULL, + NULL, + 0, + SHUTDOWN_FORCE_OTHERS | SHUTDOWN_RESTART | SHUTDOWN_HYBRID, + SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_MINOR_OTHER + ); +} + +// A function to put the computer to sleep +void scmd_sleep() +{ + if (!has_shutdown_privilege) { + bool successful = get_shutdown_privilege(); + if (!successful) + return; } - } + SetSuspendState(FALSE, FALSE, FALSE); } -// A function to launch an application -void launch_application(char *cmd) +// A function to get the shutdown privilege from Windows +static bool get_shutdown_privilege() { - WCHAR w_file[MAX_PATH_CHARS + 1]; - LPWSTR w_params = NULL; - - // Parse command into file and parameters strings - parse_command(cmd, w_file, &w_params); - - // Set up info struct - SHELLEXECUTEINFOW info = { - .cbSize = sizeof(SHELLEXECUTEINFOW), - .fMask = SEE_MASK_NOCLOSEPROCESS, - .hwnd = NULL, - .lpVerb = L"open", - .lpFile = w_file, - .lpParameters = w_params, - .lpDirectory = NULL, - .nShow = SW_SHOWMAXIMIZED, - .lpIDList = NULL, - .lpClass = NULL, - }; - - BOOL successful = ShellExecuteExW(&info); - if (successful) { - HANDLE child_process = info.hProcess; - - // Check if launched application is a web browser - WCHAR w_process_name[MAX_PATH_CHARS + 1]; - DWORD ret = GetProcessImageFileNameW(child_process, w_process_name, sizeof(w_process_name)); - if (ret) { - w_process_basename = path_basename(w_process_name); - web_browser = is_browser(w_process_basename); + HANDLE token = NULL; + BOOL ret = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token); + if (!ret) { + log_error("Could not open process token"); + return false; } - - // Block until application has closed - if (config.on_launch == MODE_ON_LAUNCH_HIDE && !web_browser) { - WaitForSingleObject(child_process, INFINITE); + LUID luid; + ret = LookupPrivilegeValueA(NULL, SE_SHUTDOWN_NAME, &luid); + if (!ret) { + log_error("Failed to lookup privilege"); + CloseHandle(token); + return false; } - - // Non-blocking, run event pump so we don't lose communication with window manager - else { - HWND hwnd = wm_info.info.win.window; - SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOREDRAW | SWP_NOSIZE | SWP_NOMOVE); - do { - SDL_PumpEvents(); - SDL_Delay(100); - } while (process_running(child_process)); + TOKEN_PRIVILEGES tp; + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = luid; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + ret = AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL); + if (!ret) { + log_error("Failed to adjust token privileges"); + CloseHandle(token); + return false; } - } - else { - output_log(LOGLEVEL_DEBUG, "Failed to launch command\n"); - } - free(w_params); + has_shutdown_privilege = true; + CloseHandle(token); + return true; } -// A function to scan the slideshow directory for image files -int scan_slideshow_directory(slideshow_t *slideshow, const char *directory) +// A function to check if there is an exit hotkey +bool has_exit_hotkey() { - WIN32_FIND_DATAW data; - HANDLE handle; - char file_search[MAX_PATH_CHARS + 1]; - WCHAR w_file_search[MAX_PATH_CHARS + 1]; - char file_result[MAX_PATH_UTF8_CONVERT + 1]; - char file_output[sizeof(file_result)]; - char extension[10]; - int num_images = 0; - - // Generate a UTF-16 wildcard file search string for all supported image file extensions - for (int i = 0; i < NUM_IMAGE_EXTENSIONS && num_images < MAX_SLIDESHOW_IMAGES; i++) { - strcpy(extension, "*"); - strcat(extension, extensions[i]); - join_paths(file_search, 2, directory, extension); - convert_utf8_string(w_file_search, file_search, sizeof(w_file_search)); - - // Convert every search result to back to UTF-8, store in slideshow struct - handle = FindFirstFileW(w_file_search, &data); - if (handle != INVALID_HANDLE_VALUE) { - do { - convert_utf16_string(file_result, data.cFileName, sizeof(file_result)); - join_paths(file_output, 2, directory, file_result); - copy_string(&slideshow->images[num_images], file_output); - num_images++; - } while (FindNextFileW(handle, &data) != 0 && num_images < MAX_SLIDESHOW_IMAGES); - } - } - slideshow->num_images = num_images; - return num_images; + if (exit_hotkey) + return true; + return false; } -// A function to get the 2 letter region code -void get_region(char *buffer) +// A function to store an exit hotkey +void set_exit_hotkey(SDL_Keycode keycode) { - WCHAR w_region[3]; - GetUserDefaultGeoName(w_region, sizeof(w_region)); - convert_utf16_string(buffer, w_region, 3); + if (exit_hotkey) + return; + exit_hotkey = sdl_to_win32_keycode(keycode); + if (!exit_hotkey) + log_error("Invalid exit hotkey keycode %X", keycode); } -// A function to convert command line arguments from UTF-16 to UTF-8 -void convert_args(int *argc, char **argv[]) +// A function to register the exit hotkey with Windows +void register_exit_hotkey() { - LPWSTR command_line = GetCommandLineW(); - LPWSTR *w_argv = CommandLineToArgvW(command_line, argc); - int array_length = *argc; - char **arg_list = malloc(sizeof(char*)*array_length); - for (int i = 0; i < array_length; i++) { - convert_utf16_alloc(arg_list + i, w_argv[i]); - } - LocalFree(w_argv); - *argv = arg_list; + BOOL ret = RegisterHotKey(wm_info.info.win.window, 1, 0, exit_hotkey); + if (!ret) { + exit_hotkey = 0; + log_error("Failed to register exit hotkey with Windows"); + } +} + +// A function to check if the exit hotkey was pressed, and close the active window if so +void check_exit_hotkey(SDL_SysWMmsg *msg) +{ + if (msg->msg.win.msg == WM_HOTKEY) { + log_debug("Exit hotkey detected"); + HWND hwnd = GetForegroundWindow(); + if (hwnd == NULL) { + log_error("Could not get top window"); + return; + } + PostMessage(hwnd, WM_CLOSE, 0, 0); + } } -// A function to free the heap-allocated command line arguments -void cleanup_args(int argc, char *argv[]) +// A function to convert an SDL keycode to a WIN32 virtual keycode +static UINT sdl_to_win32_keycode(SDL_Keycode keycode) { - for (int i = 0; i < argc; i++) { - free(argv[i]); - } - free(argv); -} \ No newline at end of file +#include "keycode_convert.h" // Import the conversion table + for (int i = 0; i < sizeof(table) / sizeof(table[0]); i++) { + if (table[i].sdl == keycode) + return table[i].win; + } + return 0; +} diff --git a/src/platform/win32.h b/src/platform/win32.h deleted file mode 100644 index 8996c4b..0000000 --- a/src/platform/win32.h +++ /dev/null @@ -1,14 +0,0 @@ -#define MAX_PATH_UTF8_CONVERT 2*sizeof(WCHAR)*MAX_PATH_CHARS -#define BROWSER_CHECK_PERIOD 1000 - -//Function prototypes -static void convert_utf8_string(LPWSTR w_string, const char *string, int buffer_size); -static void convert_utf16_string(char *string, LPCWSTR w_string, int buffer_size); -static void convert_utf8_alloc(LPWSTR *buffer, const char *string); -static void convert_utf16_alloc(const char **buffer, LPCWSTR w_string); -static void parse_command(char *cmd, LPWSTR w_file, LPWSTR *w_params); -static LPWSTR path_basename(LPCWSTR w_path); -static bool process_running_name(LPCWSTR w_target_process); -static bool w_file_exists(LPCWSTR w_path); -static bool is_browser(LPCWSTR w_exe_basename); -bool process_running(HANDLE process); diff --git a/src/util.c b/src/util.c index c4df277..1df240b 100755 --- a/src/util.c +++ b/src/util.c @@ -3,1120 +3,1102 @@ #include #include #include +#include +#include #include +#include #include "launcher.h" #include #include "util.h" #include "debug.h" #include "platform/platform.h" -#include "external/ini.h" - -extern config_t config; -extern gamepad_control_t *gamepad_controls; -extern hotkey_t *hotkeys; -menu_t *menu = NULL; -entry_t *entry = NULL; +#include + +static void add_gamepad_control(const char *label, const char *cmd); +static bool parse_mode_setting(ModeSettingType type, const char *value, int *setting); +static Menu *create_menu(const char *menu_name, size_t *num_menus); + +extern Config config; +extern GamepadControl *gamepad_controls; +extern Hotkey *hotkeys; +Menu *menu = NULL; +Entry *entry = NULL; + +static const char *mode_settings[][5] = { + {"Color", "Image", "Slideshow", "Transparent", NULL}, // Background Mode + {"Blank", "None", "Quit", NULL, NULL}, // OnLaunch + {"Truncated", "Shrink", "None", NULL, NULL}, // OversizeMode + {"Left", "Right", NULL, NULL, NULL}, // Clock Alignment + {"24hr", "12hr", "Auto", NULL, NULL}, // Clock Format + {"Big", "Little", "Auto", NULL, NULL} // Date Format +}; // A function to handle the arguments from the command line void handle_arguments(int argc, char *argv[], char **config_file_path) { - // Convert Windows UTF-16 arguments to UTF-8 - #ifdef _WIN32 - convert_args(&argc, &argv); - #endif - - // Parse command line arguments - if (argc > 1) { - bool version = false; - bool help = false; - int config_file_index = -1; - for (int i = 1; i < argc; i++) { - - // Current argument is config file path if -c or --config was previous argument - if (i == config_file_index && *config_file_path == NULL) { - if (file_exists(argv[i])) { - copy_string(config_file_path, argv[i]); - } - else { - output_log(LOGLEVEL_FATAL, "Fatal Error: Config file %s not found\n", argv[i]); - quit(1); + // Parse command line arguments + if (argc > 1) { + bool version = false; + bool help = false; + int rc; + const char *short_opts = "hvc:d"; + static const struct option long_opts[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + { "config", required_argument, NULL, 'c' }, + { "debug", no_argument, NULL, 'd' }, + { 0, 0, 0, 0 } + }; + + while ((rc = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { + switch (rc) { + case 'h': + help = true; + break; + + case 'v': + version = true; + break; + + case 'c': + if (file_exists(optarg)) + *config_file_path = strdup(optarg); + else + log_fatal("Config file '%s' not found", optarg); + break; + + case 'd': + config.debug = true; + break; + } } - } - if (!strcmp(argv[i],"-c") || !strcmp(argv[i],"--config")) { - config_file_index = i + 1; - } - else if (!strcmp(argv[i],"-d") || !strcmp(argv[i],"--debug")) { - config.debug = true; - } - #ifdef __unix__ - else if (!strcmp(argv[i],"-h") || !strcmp(argv[i],"--help")) { - help = true; - } - else if (!strcmp(argv[i],"-v") || !strcmp(argv[i],"--version")) { - version = true; - } - #endif - else if (i != config_file_index) { - #ifdef __unix__ - printf("Unrecognized option %s\n",argv[i]); - print_usage(); - #endif - quit(1); - } - } - // Check version, help flags - #ifdef __unix__ - if (version) { - print_version(); - quit(0); - } - if (help) { - print_usage(); - quit(0); + // Check version, help flags +#ifdef __unix__ + if (version) { + print_version(stdout); + fputs("\n", stdout); + print_compiler_info(stdout); + quit(EXIT_SUCCESS); + } + if (help) { + print_usage(); + quit(EXIT_SUCCESS); + } +#endif } - #endif - } - - // Try to find config file if none is specified on the command line - if (*config_file_path == NULL) { - #ifdef __unix__ - char *prefixes[4]; - char home_config_buffer[MAX_PATH_CHARS + 1]; - prefixes[0] = CURRENT_DIRECTORY; - prefixes[1] = config.exe_path; - prefixes[2] = join_paths(home_config_buffer, 3, getenv("HOME"), ".config", EXECUTABLE_TITLE); - prefixes[3] = PATH_CONFIG_SYSTEM; - *config_file_path = find_file(FILENAME_DEFAULT_CONFIG, 4, prefixes); - #else - char *prefixes[2]; - prefixes[0] = CURRENT_DIRECTORY; - prefixes[1] = config.exe_path; - *config_file_path = find_file(FILENAME_DEFAULT_CONFIG, 2, prefixes); - #endif + // Try to find config file if none is specified on the command line if (*config_file_path == NULL) { - output_log(LOGLEVEL_FATAL, "Fatal Error: No config file found\n"); - #ifdef __unix__ - print_usage(); - #endif - quit(1); - } - } - output_log(LOGLEVEL_DEBUG, "Config file found: %s\n", *config_file_path); - - // Clean up heap-allocated arguments for Windows - #ifdef _WIN32 - cleanup_args(argc, argv); - #endif +#ifdef __unix__ + const char *prefixes[4]; + char home_config_buffer[MAX_PATH_CHARS + 1]; + prefixes[0] = CURRENT_DIRECTORY; + prefixes[1] = config.exe_path; + prefixes[2] = join_paths(home_config_buffer, sizeof(home_config_buffer), 3, getenv("HOME"), ".config", EXECUTABLE_TITLE); + prefixes[3] = PATH_CONFIG_SYSTEM; + *config_file_path = find_file(FILENAME_DEFAULT_CONFIG, 4, prefixes); +#else + char *prefixes[2]; + prefixes[0] = CURRENT_DIRECTORY; + prefixes[1] = config.exe_path; + *config_file_path = find_file(FILENAME_DEFAULT_CONFIG, 2, prefixes); +#endif + + if (*config_file_path == NULL) + log_fatal("No config file found"); + } + log_debug("Config file found: %s", *config_file_path); } // A function to parse the config file and store the settings into the config struct void parse_config_file(const char *config_file_path) { - char *buffer = NULL; - read_file(config_file_path, &buffer); - if (buffer == NULL) { - output_log(LOGLEVEL_FATAL, "Fatal Error: Could not read config file\n"); - exit(1); - } - - int error = ini_parse_string(buffer, config_handler, &config); - free(buffer); - - if (error < 0) { - output_log(LOGLEVEL_FATAL, "Fatal Error: Could not parse config file\n"); - quit(1); - } + FILE *file = fopen(config_file_path, "r"); + if (file == NULL) + log_fatal("Could not open config file"); + int error = ini_parse_file(file, config_handler, NULL); + fclose(file); + + if (error < 0) + log_fatal("Could not parse config file"); } // A function to handle config file parsing int config_handler(void *user, const char *section, const char *name, const char *value) { - config_t *pconfig = (config_t*) user; - - // Parse settings - if (!strcmp(section,"Settings")) { - if (!strcmp(name,SETTING_BACKGROUND_IMAGE)) { - copy_string(&pconfig->background_image, value); - clean_path(pconfig->background_image); - } - else if (!strcmp(name,SETTING_TITLE_FONT)) { - copy_string(&pconfig->title_font_path, value); - clean_path(pconfig->title_font_path); - } - else if (!strcmp(name,SETTING_TITLE_FONT_SIZE)) { - pconfig->font_size = (unsigned int) atoi(value); - } - else if (!strcmp(name,SETTING_TITLE_COLOR)) { - hex_to_color(value, &pconfig->title_color); - } - else if (!strcmp(name,SETTING_BACKGROUND_MODE)) { - if (!strcmp(value, "Image")) { - pconfig->background_mode = MODE_IMAGE; - } - else if (!strcmp(value, "Slideshow")) { - pconfig->background_mode = MODE_SLIDESHOW; - } - else { - pconfig->background_mode = MODE_COLOR; - } - } - else if (!strcmp(name,SETTING_BACKGROUND_COLOR)) { - hex_to_color(value, &pconfig->background_color); - } - else if (!strcmp(name,SETTING_SLIDESHOW_DIRECTORY)) { - copy_string(&pconfig->slideshow_directory, value); - clean_path(pconfig->slideshow_directory); - } - else if (!strcmp(name,SETTING_ICON_SIZE)) { - Uint16 icon_size = (Uint16) atoi(value); - if (icon_size >= MIN_ICON_SIZE && icon_size <= MAX_ICON_SIZE) { - pconfig->icon_size = icon_size; - } - } - else if (!strcmp(name,SETTING_DEFAULT_MENU)) { - copy_string(&pconfig->default_menu, value); - } - else if (!strcmp(name,SETTING_HIGHLIGHT_COLOR)) { - hex_to_color(value, &pconfig->highlight_color); - } - else if (!strcmp(name,SETTING_HIGHLIGHT_CORNER_RADIUS)) { - Uint16 rx = (Uint16) atoi(value); - if (rx >= MIN_RX_SIZE && rx <= MAX_RX_SIZE) { - pconfig->highlight_rx = rx; - } - } - else if (!strcmp(name,SETTING_TITLE_PADDING)) { - int title_padding = atoi(value); - if (title_padding >= 0) { - pconfig->title_padding = (unsigned int) title_padding; - } - } - else if (!strcmp(name,SETTING_MAX_BUTTONS)) { - int max_buttons = atoi(value); - if (max_buttons > 0) { - pconfig->max_buttons = (unsigned int) max_buttons; - } - } - else if (!strcmp(name, SETTING_ICON_SPACING)) { - if (is_percent(value)) { - strcpy(pconfig->icon_spacing_str, value); - } - else { - int icon_spacing = atoi(value); - if (icon_spacing > 0 || !strcmp(value,"0")) { - pconfig->icon_spacing = icon_spacing; + UNUSED(user); + + if (MATCH(section, "General")) { + if (MATCH(name, SETTING_DEFAULT_MENU)) + config.default_menu = strdup(value); + else if (MATCH(name, SETTING_VSYNC)) + convert_bool(value, &config.vsync); + else if(MATCH(name, SETTING_FPS_LIMIT)) { + int fps = atoi(value); + if (fps > MIN_FPS_LIMIT) + config.fps_limit = fps; + } + else if (MATCH(name, SETTING_APPLICATION_TIMEOUT)) { + Uint32 application_timeout = (Uint32) atoi(value); + if (application_timeout >= MIN_APPLICATION_TIMEOUT + && application_timeout <= MAX_APPLICATION_TIMEOUT) { + config.application_timeout = 1000 * application_timeout; + } + } + else if (MATCH(name, SETTING_ON_LAUNCH)) + parse_mode_setting(MODE_SETTING_ON_LAUNCH, value, (int*) &config.on_launch); + else if (MATCH(name, SETTING_WRAP_ENTRIES)) + convert_bool(value, &config.wrap_entries); + else if (MATCH(name, SETTING_RESET_ON_BACK)) + convert_bool(value, &config.reset_on_back); + else if (MATCH(name, SETTING_MOUSE_SELECT)) + convert_bool(value, &config.mouse_select); + else if (MATCH(name, SETTING_INHIBIT_OS_SCREENSAVER)) + convert_bool(value, &config.inhibit_os_screensaver); + else if (MATCH(name, SETTING_STARTUP_CMD)) + config.startup_cmd = strdup(value); + else if (MATCH(name, SETTING_QUIT_CMD)) + config.quit_cmd = strdup(value); + } + + else if (MATCH(section, "Layout")) { + if (MATCH(name, SETTING_MAX_BUTTONS)) { + int max_buttons = atoi(value); + if (max_buttons > 0) + config.max_buttons = (unsigned int) max_buttons; + } + else if (MATCH(name, SETTING_ICON_SIZE)) { + Uint16 icon_size = (Uint16) atoi(value); + if (icon_size >= MIN_ICON_SIZE && icon_size <= MAX_ICON_SIZE) + config.icon_size = icon_size; + } + else if (MATCH(name, SETTING_ICON_SPACING)) { + if (is_percent(value)) + copy_string(config.icon_spacing_str, value, sizeof(config.icon_spacing_str)); + else { + int icon_spacing = atoi(value); + if (icon_spacing > 0 || MATCH(value, "0")) + config.icon_spacing = icon_spacing; + } + } + else if (MATCH(name, SETTING_VCENTER)) { + if (is_percent(value)) + copy_string(config.vcenter, value, sizeof(config.vcenter)); } - } - } - else if (!strcmp(name,SETTING_HIGHLIGHT_VPADDING)) { - int highlight_vpadding = atoi(value); - if (highlight_vpadding > 0 || !strcmp(value,"0")) { - pconfig->highlight_vpadding = highlight_vpadding; - } - } - else if (!strcmp(name,SETTING_HIGHLIGHT_HPADDING)) { - int highlight_hpadding = atoi(value); - if (highlight_hpadding > 0 || !strcmp(value,"0")) { - pconfig->highlight_hpadding = highlight_hpadding; - } - } - else if (!strcmp(name, SETTING_SLIDESHOW_IMAGE_DURATION)) { - Uint32 slideshow_image_duration = ((Uint32) atoi(value))*1000; - if (slideshow_image_duration >= MIN_SLIDESHOW_IMAGE_DURATION && - slideshow_image_duration <= MAX_SLIDESHOW_IMAGE_DURATION) { - pconfig->slideshow_image_duration = slideshow_image_duration; - } - } - else if (!strcmp(name, SETTING_SLIDESHOW_TRANSITION_TIME)) { - Uint32 slideshow_transition_time = (Uint32) (atof(value)*1000.0f); - if (slideshow_transition_time >= MIN_SLIDESHOW_TRANSITION_TIME && - slideshow_transition_time <= MAX_SLIDESHOW_TRANSITION_TIME) { - pconfig->slideshow_transition_time = slideshow_transition_time; - } - } - else if (!strcmp(name, SETTING_TITLE_OPACITY)) { - if (is_percent(value)) { - strcpy(pconfig->title_opacity, value); - } - } - else if (!strcmp(name, SETTING_HIGHLIGHT_OPACITY)) { - if (is_percent(value)) { - strcpy(pconfig->highlight_opacity, value); - } - } - else if (!strcmp(name, SETTING_BUTTON_CENTERLINE)) { - if (is_percent(value)) { - strcpy(pconfig->button_centerline, value); - } - } - else if (!strcmp(name,SETTING_SCROLL_INDICATORS)) { - pconfig->scroll_indicators = convert_bool(value, DEFAULT_SCROLL_INDICATORS); - } - else if (!strcmp(name,SETTING_SCROLL_INDICATOR_COLOR)) { - hex_to_color(value, &pconfig->scroll_indicator_color); - } - else if (!strcmp(name,SETTING_SCROLL_INDICATOR_OPACITY)) { - if (is_percent(value)) { - strcpy(pconfig->scroll_indicator_opacity,value); - } - } - else if (!strcmp(name,SETTING_TITLE_OVERSIZE_MODE)) { - if (!strcmp(value,"Shrink")) { - pconfig->title_oversize_mode = MODE_TEXT_SHRINK; - } - else if (!strcmp(value,"None")) { - pconfig->title_oversize_mode = MODE_TEXT_NONE; - } - } - else if (!strcmp(name, SETTING_ON_LAUNCH)) { - if (!strcmp(value, "None")) { - pconfig->on_launch = MODE_ON_LAUNCH_NONE; - } - else if (!strcmp(value, "Blank")) { - pconfig->on_launch = MODE_ON_LAUNCH_BLANK; - } - else { - pconfig->on_launch = MODE_ON_LAUNCH_HIDE; - } - } - else if (!strcmp(name, SETTING_RESET_ON_BACK)) { - pconfig->reset_on_back = convert_bool(value, DEFAULT_RESET_ON_BACK); } - } - // Parse clock settings - else if (!strcmp(section, "Clock")) { - if (!strcmp(name, SETTING_CLOCK_ENABLED)) { - pconfig->clock_enabled = convert_bool(value, DEFAULT_CLOCK_ENABLED); - } - else if (!strcmp(name, SETTING_CLOCK_SHOW_DATE)) { - pconfig->clock_show_date = convert_bool(value, DEFAULT_CLOCK_SHOW_DATE); - } - else if (!strcmp(name, SETTING_CLOCK_ALIGNMENT)) { - if(!strcmp(value, "Left")) { - pconfig->clock_alignment = ALIGNMENT_LEFT; - } - else if (!strcmp(value, "Right")) { - pconfig->clock_alignment = ALIGNMENT_RIGHT; - } - } - else if (!strcmp(name, SETTING_CLOCK_FONT)) { - copy_string(&pconfig->clock_font_path, value); - clean_path(pconfig->clock_font_path); - } - else if (!strcmp(name, SETTING_CLOCK_MARGIN)) { - if (is_percent(value)) { - strcpy(pconfig->clock_margin_str, value); - } - else { - int clock_margin = atoi(value); - if (clock_margin > 0 || !strcmp(value,"0")) { - pconfig->clock_margin = clock_margin; + else if (MATCH(section, "Background")) { + if (MATCH(name, SETTING_BACKGROUND_MODE)) + parse_mode_setting(MODE_SETTING_BACKGROUND, value, (int*) &config.background_mode); + else if (MATCH(name, SETTING_BACKGROUND_COLOR)) + hex_to_color(value, &config.background_color); + else if (MATCH(name, SETTING_BACKGROUND_IMAGE)) { + config.background_image = strdup(value); + clean_path(config.background_image); + } + else if (MATCH(name, SETTING_SLIDESHOW_DIRECTORY)) { + config.slideshow_directory = strdup(value); + clean_path(config.slideshow_directory); + } + else if (MATCH(name, SETTING_SLIDESHOW_IMAGE_DURATION)) { + Uint32 slideshow_image_duration = ((Uint32) atoi(value))*1000; + if (slideshow_image_duration >= MIN_SLIDESHOW_IMAGE_DURATION && + slideshow_image_duration <= MAX_SLIDESHOW_IMAGE_DURATION) + config.slideshow_image_duration = slideshow_image_duration; + } + else if (MATCH(name, SETTING_SLIDESHOW_TRANSITION_TIME)) { + Uint32 slideshow_transition_time = (Uint32) (atof(value)*1000.0f); + if (slideshow_transition_time <= MAX_SLIDESHOW_TRANSITION_TIME) + config.slideshow_transition_time = slideshow_transition_time; + } + else if (MATCH(name, SETTING_CHROMA_KEY_COLOR)) + hex_to_color(value, &config.chroma_key_color); + else if (MATCH(name, SETTING_BACKGROUND_OVERLAY)) + convert_bool(value, &config.background_overlay); + else if (MATCH(name, SETTING_BACKGROUND_OVERLAY_COLOR)) + hex_to_color(value, &config.background_overlay_color); + else if (MATCH(name, SETTING_BACKGROUND_OVERLAY_OPACITY)) { + if (is_percent(value)) + copy_string(config.background_overlay_opacity, value, sizeof(config.background_overlay_opacity)); } - } - } - else if (!strcmp(name, SETTING_CLOCK_COLOR)) { - hex_to_color(value, &pconfig->clock_color); - } - else if (!strcmp(name, SETTING_CLOCK_OPACITY)) { - if (strstr(value, ".") == NULL && - strstr(value, "%") != NULL && - strlen(value) < PERCENT_MAX_CHARS) { - strcpy(pconfig->clock_opacity, value); - } - } - else if (!strcmp(name, SETTING_CLOCK_FONT_SIZE)) { - unsigned int font_size = (unsigned int) atoi(value); - if (font_size) { - pconfig->clock_font_size = font_size; - } - } - else if (!strcmp(name, SETTING_CLOCK_TIME_FORMAT)) { - if (!strcmp(value, "12hr")) { - pconfig->clock_time_format = FORMAT_TIME_12HR; - } - else if (!strcmp(value, "24hr")) { - pconfig->clock_time_format = FORMAT_TIME_24HR; - } - } - else if (!strcmp(name, SETTING_CLOCK_DATE_FORMAT)) { - if (!strcmp(value, "Little")) { - pconfig->clock_date_format = FORMAT_DATE_LITTLE; - } - else if (!strcmp(value, "Big")) { - pconfig->clock_date_format = FORMAT_DATE_BIG; - } - } - else if (!strcmp(name, SETTING_CLOCK_INCLUDE_WEEKDAY)) { - pconfig->clock_include_weekday = convert_bool(value, DEFAULT_CLOCK_INCLUDE_WEEKDAY); } - } - // Parse screensaver setttings - else if (!strcmp(section, "Screensaver")) { - if (!strcmp(name, SETTING_SCREENSAVER_ENABLED)) { - pconfig->screensaver_enabled = convert_bool(value, DEFAULT_SCREENSAVER_ENABLED); - } - else if (!strcmp(name, SETTING_SCREENSAVER_IDLE_TIME)) { - Uint32 screensaver_idle_time = (Uint32) atoi(value); - if (screensaver_idle_time >= MIN_SCREENSAVER_IDLE_TIME && - screensaver_idle_time <= MAX_SCREENSAVER_IDLE_TIME) { - pconfig->screensaver_idle_time = screensaver_idle_time*1000; // Convert to ms - } - } - else if (!strcmp(name, SETTING_SCREENSAVER_INTENSITY)) { - if (is_percent(value)) { - strcpy(pconfig->screensaver_intensity_str, value); - } - } - else if (!strcmp(name, SETTING_SCREENSAVER_PAUSE_SLIDESHOW)) { - pconfig->screensaver_pause_slideshow = convert_bool(value, DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW); - } - } - - // Parse hotkeys - else if (!strcmp(section, "Hotkeys")) { - char *keycode = strtok(value, ";"); - if (keycode != NULL) { - char *cmd = strtok(NULL, ""); - if (cmd != NULL) { - add_hotkey(keycode, cmd); - } + else if (MATCH(section, "Titles")) { + if (MATCH(name, SETTING_TITLES_ENABLED)) + convert_bool(value, &config.titles_enabled); + else if (MATCH(name, SETTING_TITLE_FONT)) { + config.title_font_path = strdup(value); + clean_path(config.title_font_path); + } + else if (MATCH(name, SETTING_TITLE_FONT_SIZE)) + config.title_font_size = (unsigned int) atoi(value); + else if (MATCH(name, SETTING_TITLE_FONT_COLOR)) + hex_to_color(value, &config.title_font_color); + else if (MATCH(name, SETTING_TITLE_OPACITY)) { + if (is_percent(value)) + copy_string(config.title_opacity, value, sizeof(config.title_opacity)); + } + else if (MATCH(name, SETTING_TITLE_SHADOWS)) + convert_bool(value, &config.title_shadows); + else if (MATCH(name, SETTING_TITLE_SHADOW_COLOR)) + hex_to_color(value, &config.title_shadow_color); + else if (MATCH(name, SETTING_TITLE_OVERSIZE_MODE)) + parse_mode_setting(MODE_SETTING_OVERSIZE, value, (int*) &config.title_oversize_mode); + else if (MATCH(name, SETTING_TITLE_PADDING)) { + int title_padding = atoi(value); + if (title_padding >= 0) + config.title_padding = title_padding; + } } - } - // Parse gamepad settings - else if (!strcmp(section, "Gamepad")) { - if (!strcmp(name, SETTING_GAMEPAD_ENABLED)) { - pconfig->gamepad_enabled = convert_bool(value, DEFAULT_GAMEPAD_ENABLED); - } - else if (!strcmp(name, SETTING_GAMEPAD_DEVICE)) { - int device_index = atoi(value); - if (device_index >= 0) { - pconfig->gamepad_device = device_index; - } - } - else if (!strcmp(name, SETTING_GAMEPAD_MAPPINGS_FILE)) { - copy_string(&pconfig->gamepad_mappings_file, value); - clean_path(pconfig->gamepad_mappings_file); - } - else if (!strcmp(name, SETTING_GAMEPAD_LSTICK_XM)) { - add_gamepad_control(TYPE_AXIS_NEG, SDL_CONTROLLER_AXIS_LEFTX, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_LSTICK_XP)) { - add_gamepad_control(TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_LEFTX, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_LSTICK_YM)) { - add_gamepad_control(TYPE_AXIS_NEG, SDL_CONTROLLER_AXIS_LEFTY, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_LSTICK_YP)) { - add_gamepad_control(TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_LEFTY, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_RSTICK_XM)) { - add_gamepad_control(TYPE_AXIS_NEG, SDL_CONTROLLER_AXIS_RIGHTX, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_RSTICK_XP)) { - add_gamepad_control(TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_RIGHTX, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_RSTICK_YM)) { - add_gamepad_control(TYPE_AXIS_NEG, SDL_CONTROLLER_AXIS_RIGHTY, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_RSTICK_YP)) { - add_gamepad_control(TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_RIGHTY, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_LTRIGGER)) { - add_gamepad_control(TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_TRIGGERLEFT, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_RTRIGGER)) { - add_gamepad_control(TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_A)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_A, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_B)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_B, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_X)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_X, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_Y)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_Y, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_BACK)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_BACK, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_GUIDE)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_GUIDE, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_START)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_START, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_LEFT_STICK)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_LEFTSTICK, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_RIGHT_STICK)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_RIGHTSTICK, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_DPAD_UP)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_DPAD_UP, name, value); - } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_DPAD_DOWN)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_DPAD_DOWN, name, value); + else if (MATCH(section, "Highlight")) { + if (MATCH(name, SETTING_HIGHLIGHT_ENABLED)) + convert_bool(value, &config.highlight); + else if (MATCH(name, SETTING_HIGHLIGHT_FILL_COLOR)) + hex_to_color(value, &config.highlight_fill_color); + else if (MATCH(name, SETTING_HIGHLIGHT_OUTLINE_COLOR)) + hex_to_color(value, &config.highlight_outline_color); + else if (MATCH(name, SETTING_HIGHLIGHT_OUTLINE_SIZE)) { + int highlight_outline_size = atoi(value); + if (highlight_outline_size >= 0) + config.highlight_outline_size = highlight_outline_size; + } + else if (MATCH(name, SETTING_HIGHLIGHT_CORNER_RADIUS)) { + int rx = atoi(value); + if (rx >= MIN_RX_SIZE && rx <= MAX_RX_SIZE) + config.highlight_rx = (Uint16) rx; + } + else if (MATCH(name, SETTING_HIGHLIGHT_FILL_OPACITY)) { + if (is_percent(value)) + copy_string(config.highlight_fill_opacity, value, sizeof(config.highlight_fill_opacity)); + } + else if (MATCH(name, SETTING_HIGHLIGHT_OUTLINE_OPACITY)) { + if (is_percent(value)) + copy_string(config.highlight_outline_opacity, value, sizeof(config.highlight_outline_opacity)); + } + else if (MATCH(name, SETTING_HIGHLIGHT_VPADDING)) { + int highlight_vpadding = atoi(value); + if (highlight_vpadding > 0 || MATCH(value,"0")) + config.highlight_vpadding = highlight_vpadding; + } + else if (MATCH(name, SETTING_HIGHLIGHT_HPADDING)) { + int highlight_hpadding = atoi(value); + if (highlight_hpadding > 0 || MATCH(value,"0")) + config.highlight_hpadding = highlight_hpadding; + } } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_DPAD_LEFT)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_DPAD_LEFT, name, value); + + else if (MATCH(section, "Scroll Indicators")) { + if (MATCH(name,SETTING_SCROLL_INDICATORS)) + convert_bool(value, &config.scroll_indicators); + else if (MATCH(name,SETTING_SCROLL_INDICATOR_FILL_COLOR)) + hex_to_color(value, &config.scroll_indicator_fill_color); + else if (MATCH(name, SETTING_SCROLL_INDICATOR_OUTLINE_SIZE)) { + int scroll_indicator_outline_size = atoi(value); + if (scroll_indicator_outline_size >= 0) + config.scroll_indicator_outline_size = scroll_indicator_outline_size; + } + else if (MATCH(name,SETTING_SCROLL_INDICATOR_OUTLINE_COLOR)) + hex_to_color(value, &config.scroll_indicator_outline_color); + else if (MATCH(name,SETTING_SCROLL_INDICATOR_OPACITY)) { + if (is_percent(value)) + copy_string(config.scroll_indicator_opacity, value, sizeof(config.scroll_indicator_opacity)); + } } - else if (!strcmp(name, SETTING_GAMEPAD_BUTTON_DPAD_RIGHT)) { - add_gamepad_control(TYPE_BUTTON, SDL_CONTROLLER_BUTTON_DPAD_RIGHT, name, value); + + else if (MATCH(section, "Clock")) { + if (MATCH(name, SETTING_CLOCK_ENABLED)) + convert_bool(value, &config.clock_enabled); + else if (MATCH(name, SETTING_CLOCK_SHOW_DATE)) + convert_bool(value, &config.clock_show_date); + else if (MATCH(name, SETTING_CLOCK_ALIGNMENT)) + parse_mode_setting(MODE_SETTING_ALIGNMENT, value, (int*) &config.clock_alignment); + else if (MATCH(name, SETTING_CLOCK_FONT)) { + config.clock_font_path = strdup(value); + clean_path(config.clock_font_path); + } + else if (MATCH(name, SETTING_CLOCK_MARGIN)) { + if (is_percent(value)) + copy_string(config.clock_margin_str, value, sizeof(config.clock_margin_str)); + else { + int clock_margin = atoi(value); + if (clock_margin > 0 || MATCH(value,"0")) + config.clock_margin = clock_margin; + } + } + else if (MATCH(name, SETTING_CLOCK_FONT_COLOR)) + hex_to_color(value, &config.clock_font_color); + else if (MATCH(name, SETTING_CLOCK_SHADOW_COLOR)) + hex_to_color(value, &config.clock_shadow_color); + else if (MATCH(name, SETTING_CLOCK_SHADOWS)) + convert_bool(value, &config.clock_shadows); + else if (MATCH(name, SETTING_CLOCK_OPACITY)) { + if (is_percent(value)) + copy_string(config.clock_opacity, value, sizeof(config.clock_opacity)); + } + else if (MATCH(name, SETTING_CLOCK_FONT_SIZE)) { + unsigned int font_size = (unsigned int) atoi(value); + if (font_size) + config.clock_font_size = font_size; + } + else if (MATCH(name, SETTING_CLOCK_TIME_FORMAT)) + parse_mode_setting(MODE_SETTING_TIME_FORMAT, value, (int*) &config.clock_time_format); + else if (MATCH(name, SETTING_CLOCK_DATE_FORMAT)) + parse_mode_setting(MODE_SETTING_DATE_FORMAT, value, (int*) &config.clock_date_format); + else if (MATCH(name, SETTING_CLOCK_INCLUDE_WEEKDAY)) + convert_bool(value, &config.clock_include_weekday); + } + + else if (MATCH(section, "Screensaver")) { + if (MATCH(name, SETTING_SCREENSAVER_ENABLED)) + convert_bool(value, &config.screensaver_enabled); + else if (MATCH(name, SETTING_SCREENSAVER_IDLE_TIME)) { + Uint32 screensaver_idle_time = (Uint32) atoi(value); + if (screensaver_idle_time >= MIN_SCREENSAVER_IDLE_TIME && + screensaver_idle_time <= MAX_SCREENSAVER_IDLE_TIME) + config.screensaver_idle_time = screensaver_idle_time*1000; // Convert to ms + } + else if (MATCH(name, SETTING_SCREENSAVER_INTENSITY)) { + if (is_percent(value)) + copy_string(config.screensaver_intensity_str, value, sizeof(config.screensaver_intensity_str)); + } + else if (MATCH(name, SETTING_SCREENSAVER_PAUSE_SLIDESHOW)) + convert_bool(value, &config.screensaver_pause_slideshow); + } + + else if (MATCH(section, "Hotkeys")) { + char *keycode = strtok((char*) value, ";"); + if (keycode != NULL) { + char *cmd = strtok(NULL, ""); + if (cmd != NULL) + add_hotkey(keycode, cmd); + } } - } - // Parse menus/entries - else { - entry_t *previous_entry; + else if (MATCH(section, "Gamepad")) { + if (MATCH(name, SETTING_GAMEPAD_ENABLED)) + convert_bool(value, &config.gamepad_enabled); + else if (MATCH(name, SETTING_GAMEPAD_DEVICE)) + config.gamepad_device = atoi(value); + else if (MATCH(name, SETTING_GAMEPAD_MAPPINGS_FILE)) { + config.gamepad_mappings_file = strdup(value); + clean_path(config.gamepad_mappings_file); + } - // Check if menu struct exists for current section - if (pconfig->first_menu == NULL) { - pconfig->first_menu = create_menu(section, &pconfig->num_menus); - menu = pconfig->first_menu; + // Parse gamepad controls + else + add_gamepad_control(name, value); } + + // Parse menus/entries else { - bool menu_exists = false; - for (menu_t *tmp = pconfig->first_menu; tmp != NULL; - tmp = tmp->next) { - if (!strcmp(tmp->name,section)) { - menu_exists = true; - break; - } - } + Entry *previous_entry = NULL; - // Create menu if it doesn't already exist - if (menu_exists == false) { - menu->next = create_menu(section, &pconfig->num_menus); - menu = menu->next; - } - } + // Check if menu struct exists for current section + if (config.first_menu == NULL) { + config.first_menu = create_menu(section, &config.num_menus); + menu = config.first_menu; + } + else { + bool menu_exists = false; + for (Menu *tmp = config.first_menu; tmp != NULL; + tmp = tmp->next) { + if (MATCH(tmp->name,section)) { + menu_exists = true; + break; + } + } + + // Create menu if it doesn't already exist + if (menu_exists == false) { + menu->next = create_menu(section, &config.num_menus); + menu = menu->next; + } + } - // Parse entry line for title, icon path, command - char *string = (char*) value; - char *token; - char *delimiter = ";"; - token = strtok(string, delimiter); - if (token != NULL) { - - // Create first entry in the menu if none exists - if (menu->first_entry == NULL) { - menu->first_entry = malloc(sizeof(entry_t)); - entry = menu->first_entry; - entry->next = NULL; - } - - // Add entry to the end of the linked list - else { - previous_entry = entry; - entry = entry->next; - entry = malloc(sizeof(entry_t)); - previous_entry->next = entry; - entry->next = NULL; - } - entry->title_offset = 0; - } + // Parse entry line for title, icon path, command + char *string = (char*) value; + char *token; + char *delimiter = ";"; + token = strtok(string, delimiter); + if (token != NULL) { + + // Create first entry in the menu if none exists + if (menu->first_entry == NULL) { + menu->first_entry = malloc(sizeof(Entry)); + entry = menu->first_entry; + entry->next = NULL; + } + + // Add entry to the end of the linked list + else { + previous_entry = entry; + entry = entry->next; + entry = malloc(sizeof(Entry)); + previous_entry->next = entry; + entry->next = NULL; + } + entry->title_offset = 0; + } - // Store data in entry struct - int i; - for (i = 0;i < 3 && token != NULL; i++) { - if (i == 0) { - copy_string(&entry->title, token); + // Store data in entry struct + int i; + for (i = 0;i < 3 && token != NULL; i++) { + if (i == 0) + entry->title = strdup(token); + else if (i == 1) { + entry->icon_path = strdup(token); + clean_path(entry->icon_path); + delimiter = ""; + } + else if (i == 2) + entry->cmd = strdup(token); + + token = strtok(NULL, delimiter); } - else if (i == 1) { - copy_string(&entry->icon_path, token); - clean_path(entry->icon_path); - delimiter = ""; + + // Delete entry if parse failed to find 3 valid tokens + if (i != 3 || MATCH(":select", entry->cmd)) { + if (menu->num_entries == 0) { + free(menu->first_entry); + menu->first_entry = NULL; + } + else { + free(entry); + entry = previous_entry; + entry->next = NULL; + } } - else if (i == 2){ - copy_string(&entry->cmd, token); + else { + if (menu->num_entries == 0) + entry->previous = NULL; + else + entry->previous = previous_entry; + menu->num_entries++; + entry->icon_selected_path = selected_path(entry->icon_path); } - token = strtok(NULL, delimiter); } + return 0; +} - // Delete entry if parse failed to find 3 valid tokens - if (i != 3 || !strcmp(":select", entry->cmd)) { - if (menu->num_entries == 0) { - free(menu->first_entry); - menu->first_entry = NULL; - } - else { - free(entry); - entry = previous_entry; - entry->next = NULL; - } - } - else { - if (menu->num_entries == 0) { - entry->previous = NULL; - } - else { - entry->previous = previous_entry; - } - menu->num_entries++; +static bool parse_mode_setting(ModeSettingType type, const char *value, int *setting) +{ + const char **arr = mode_settings[type]; + for (int i = 0; arr[i] != NULL; i++) { + if (MATCH(arr[i], value)) { + *setting = i; + return true; + } } - } - return 0; + return false; +} + +const char *get_mode_setting(int type, int value) +{ + return mode_settings[type][value]; } // A function to determine if a string is a percent value bool is_percent(const char *string) { - int length = strlen(string); - if (length > 0 && length < PERCENT_MAX_CHARS && strchr(string, '%') == string + length - 1) { - return true; - } - else { - return false; - } + size_t length = strlen(string); + if (length > 0 && length < PERCENT_MAX_CHARS && strchr(string, '%') == string + length - 1) + return true; + else + return false; } // A function to remove quotation marks that enclose a path // because SDL cannot handle them void clean_path(char *path) { - int length = strlen(path); - if (length >= 3 && path[0] == '"' && path[length - 1] == '"') { - path[length - 1] = '\0'; - for (int i = 1; i <= length; i++) { - *(path + i - 1) = *(path + i); - } - } + size_t length = strlen(path); + if (length >= 3 && path[0] == '"' && path[length - 1] == '"') { + path[length - 1] = '\0'; + for (size_t i = 1; i <= length; i++) + *(path + i - 1) = *(path + i); + } +} + +// A function to get the selected path +char *selected_path(const char *path) +{ + char buffer[MAX_PATH_CHARS + 1]; + size_t length = strlen(path); + char *out = NULL; + + // Find file extension + if (length + LEN(SELECTED_SUFFIX) + 1 > sizeof(buffer)) + return out; + char *p = (char*) path + length - 1; + while (*p != '.' && p > path) + p--; + if (p == path) + return out; + + // Assemble path with suffix + strcpy(buffer, path); + buffer[p - path] = '\0'; + strcat(buffer, SELECTED_SUFFIX); + strcat(buffer, p); + + if (file_exists(buffer)) + out = strdup(buffer); + return out; } // A function to convert a hex-formatted string into a color struct bool hex_to_color(const char *string, SDL_Color *color) { + if (*string != '#') + return false; + char *p = (char*) string + 1; - // If strtoul returned 0, and the hex string wasn't 000..., then there was an error - int length = strlen(string); - Uint32 hex = (Uint32) strtoul(string, NULL, 16); - if ((!hex && strcmp(string,"00000000")) || - (!hex && strcmp(string,"000000")) || - (length != 6 && length != 8)) { - return false; - } + // If strtoul returned 0, and the hex string wasn't 000..., then there was an error + size_t length = strlen(p); + Uint32 hex = (Uint32) strtoul(p, NULL, 16); + if ((!hex && strcmp(p,"000000")) || (length != 6)) + return false; - // Convert int to SDL_Color struct via bitwise logic - if (length == 8) { - color->r = (Uint8) (hex >> 24); - color->g = (Uint8) ((hex & 0x00ff0000) >> 16); - color->b = (Uint8) ((hex & 0x0000ff00) >> 8); - return true; - } - else if (length == 6) { + // Convert int to SDL_Color struct via bitwise logic color->r = (Uint8) (hex >> 16); color->g = (Uint8) ((hex & 0x0000ff00) >> 8); color->b = (Uint8) (hex & 0x000000ff); return true; - } - else { - return false; - } } // A function to convert a string into a bool -bool convert_bool(const char *string, bool default_setting) +bool convert_bool(const char *string, bool *setting) { - if (!strcmp(string, "true")) { - return true; - } - else if (!strcmp(string, "false")) { + if (MATCH(string, "true") || MATCH(string, "True")) { + *setting = true; + return true; + } + else if (MATCH(string, "false") || MATCH(string, "False")) { + *setting = false; + return true; + } return false; - } - else { - return default_setting; - } } -// Allocates memory and copies a variable length string -void copy_string(char **dest, const char *string) +// A function to copy a string into an existing buffer +void copy_string(char *dest, const char *string, size_t size) { - int length = strlen(string); - if (length) { - *dest = malloc(sizeof(char)*(length + 1)); - strcpy(*dest, string); - } - else { - *dest = NULL; - } + strncpy(dest, string, size); + dest[size - 1] = '\0'; } // A function to join paths together -char *join_paths(char *buffer, int num_paths, ...) +char *join_paths(char *buffer, size_t bytes, int num_paths, ...) { - va_list list; - char *arg; - int length; - memset(buffer, 0, MAX_PATH_CHARS + 1); - int bytes = MAX_PATH_CHARS; - va_start(list, num_paths); - - // Add each subdirectory to path - for (int i = 0; i < num_paths && bytes > 0; i++) { - arg = va_arg(list, char*); - length = strlen(arg); - if (i == 0) { - strncpy(buffer, arg, bytes); - bytes -= length; - } - else { + va_list list; + char *arg; + size_t length; + va_start(list, num_paths); + + // Add each subdirectory to path + for (int i = 0; i < num_paths && bytes > 1; i++) { + arg = va_arg(list, char*); + length = strlen(arg); + if (length > bytes - 1) + length = bytes - 1; + if (i == 0) { + copy_string(buffer, arg, bytes); + bytes -= length; + if (bytes == 1) + break; + } + else { - // Don't copy preceding slash if present - if (*arg == '/' || *arg == '\\') { - strncat(buffer, arg + 1, bytes); - bytes -= (length - 1); - } - else { - strncat(buffer, arg, bytes); - bytes -= length; - } - } + // Don't copy preceding slash if present + if (*arg == '/' || *arg == '\\') { + strncat(buffer, arg + 1, bytes - 1); + bytes -= (length - 1); + } + else { + strncat(buffer, arg, bytes - 1); + bytes -= length; + } + } - // Add trailing slash if not present, except last argument - if ((i != num_paths - 1) && bytes > 0 && *(buffer + strlen(buffer) - 1) != '/' && - *(buffer + strlen(buffer) - 1) != '\\') { - strncat(buffer, PATH_SEPARATOR, bytes); - bytes -= 1; + // Add trailing slash if not present, except last argument + if ((i != num_paths - 1) && bytes > 1 && *(buffer + strlen(buffer) - 1) != '/' && + *(buffer + strlen(buffer) - 1) != '\\') { + strncat(buffer, PATH_SEPARATOR, bytes - 1); + bytes -= 1; + } } - } - va_end(list); - return buffer; + va_end(list); + return buffer; } // A function to find a file from a filename and list of path prefixes char *find_file(const char *file, int num_prefixes, const char **prefixes) { - char buffer[MAX_PATH_CHARS + 1]; - for (int i = 0; i < num_prefixes; i++) { - if (prefixes[i] != NULL) { - join_paths(buffer, 2, prefixes[i], file); - if (file_exists(buffer)) { - char *output; - copy_string(&output, buffer); - return output; - } + char buffer[MAX_PATH_CHARS + 1]; + for (int i = 0; i < num_prefixes; i++) { + if (prefixes[i] != NULL) { + join_paths(buffer, sizeof(buffer), 2, prefixes[i], file); + if (file_exists(buffer)) { + char *output; + output = strdup(buffer); + return output; + } + } } - } - return NULL; + return NULL; } // Calculates the length of a utf-8 encoded string int utf8_length(const char *string) { - int length = 0; - char *ptr = string; - while (*ptr != '\0') { - // If byte is 0xxxxxxx, then it's a 1 byte (ASCII) char - if ((*ptr & 0x80) == 0) { - ptr++; - } - // If byte is 110xxxxx, then it's a 2 byte char - else if ((*ptr & 0xE0) == 0xC0) { - ptr +=2; - } - // If byte is 1110xxxx, then it's a 3 byte char - else if ((*ptr & 0xF0) == 0xE0) { - ptr +=3; - } - // If byte is 11110xxx, then it's a 4 byte char - else if ((*ptr & 0xF8) == 0xF0) { - ptr+=4; + int length = 0; + char *ptr = (char*) string; + while (*ptr != '\0') { + // If byte is 0xxxxxxx, then it's a 1 byte (ASCII) char + if ((*ptr & 0x80) == 0) + ptr++; + + // If byte is 110xxxxx, then it's a 2 byte char + else if ((*ptr & 0xE0) == 0xC0) + ptr +=2; + + // If byte is 1110xxxx, then it's a 3 byte char + else if ((*ptr & 0xF0) == 0xE0) + ptr +=3; + + // If byte is 11110xxx, then it's a 4 byte char + else if ((*ptr & 0xF8) == 0xF0) + ptr+=4; + + length++; } - length++; - } - return length; + return length; } // A function to truncate a utf-8 encoded string to max number of pixels void utf8_truncate(char *string, int width, int max_width) { - int string_length = utf8_length(string); - int avg_width = width / string_length; - int num_chars = max_width / avg_width; - int spaces = (string_length - num_chars) + 3; // Number of spaces to go back - char *ptr = string + strlen(string); // Change to null character of string - int chars = 0; - - // Go back required number of spaces - do { - ptr--; - if (!(*ptr & 0x80)) { // ASCII characters have 0 as most significant bit - chars++; - } - else { // Non-ASCII character detected - do { + int string_length = utf8_length(string); + int avg_width = width / string_length; + int num_chars = max_width / avg_width; + int spaces = (string_length - num_chars) + 3; // Number of spaces to go back + char *ptr = string + strlen(string); // Change to null character of string + int chars = 0; + + // Go back required number of spaces + do { ptr--; - } while (ptr > string && (*ptr & 0xC0) == 0x80); // Non-ASCII most significant byte begins with 0b11 - chars++; + if (!(*ptr & 0x80)) // ASCII characters have 0 as most significant bit + chars++; + else { // Non-ASCII character detected + do { + ptr--; + } while (ptr > string && (*ptr & 0xC0) == 0x80); // Non-ASCII most significant byte begins with 0b11 + chars++; + } + } while (chars < spaces); + + // Add "..." to end of string to inform user of truncation + if (strlen(ptr) > 2) { + *ptr = '.'; + *(ptr + 1) = '.'; + *(ptr + 2) = '.'; + *(ptr + 3) = '\0'; } - } while (chars < spaces); - - // Add "..." to end of string to inform user of truncation - if (strlen(ptr) > 2) { - *ptr = '.'; - *(ptr + 1) = '.'; - *(ptr + 2) = '.'; - *(ptr + 3) = '\0'; - } } // A function to extract the Unicode code point from the first character in a UTF-8 string Uint16 get_unicode_code_point(const char *p, int *bytes) { - Uint16 result; - - // 1 byte ASCII char - if ((*p & 0x80) == 0) { - result = (Uint16) *p; - *bytes = 1; - } - - // If byte is 110xxxxx, then it's a 2 byte char - else if ((*p & 0xE0) == 0xC0) { - Uint8 byte1 = *p & 0x1F; - Uint8 byte2 = *(p + 1) & 0x3F; - result = (Uint16) ((byte1 << 6) + byte2); - *bytes = 2; - } - - // If byte is 1110xxxx, then it's a 3 byte char - else if ((*p & 0xF0) == 0xE0) { - Uint8 byte1 = *p & 0x0F; - Uint8 byte2 = *(p + 1) & 0x3F; - Uint8 byte3 = *(p + 2) & 0x3F; - result = (Uint16) ((byte1 << 12) + (byte2 << 6) + byte3); - *bytes = 3; - } - else { - result = 0; - *bytes = 1; - } - return result; + Uint16 result; + + // 1 byte ASCII char + if ((*p & 0x80) == 0) { + result = (Uint16) *p; + *bytes = 1; + } + + // If byte is 110xxxxx, then it's a 2 byte char + else if ((*p & 0xE0) == 0xC0) { + Uint8 byte1 = *p & 0x1F; + Uint8 byte2 = *(p + 1) & 0x3F; + result = (Uint16) ((byte1 << 6) + byte2); + *bytes = 2; + } + + // If byte is 1110xxxx, then it's a 3 byte char + else if ((*p & 0xF0) == 0xE0) { + Uint8 byte1 = *p & 0x0F; + Uint8 byte2 = *(p + 1) & 0x3F; + Uint8 byte3 = *(p + 2) & 0x3F; + result = (Uint16) ((byte1 << 12) + (byte2 << 6) + byte3); + *bytes = 3; + } + else { + result = 0; + *bytes = 1; + } + return result; } // A function to generate an array of random indices void random_array(int *array, int array_size) { - // Fill array with initial indices - for (int i = 0; i < array_size; i++) { - array[i] = i; - } - - // Shuffle array indices randomly, see https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle - srand(time(NULL)); - int j; - int tmp; - for (int i = 0; i < array_size - 1; i++) { - int j = (rand() % (array_size - i)) + i; - tmp = array[i]; - array[i] = array[j]; - array[j] = tmp; - } + // Fill array with initial indices + for (int i = 0; i < array_size; i++) + array[i] = i; + + // Shuffle array indices randomly, see https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + srand((unsigned int) time(NULL)); + int tmp; + for (int i = 0; i < array_size - 1; i++) { + int j = (rand() % (array_size - i)) + i; + tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } } // A function to calculate the total width of all screen objects unsigned int calculate_width(int buttons, int icon_spacing, int icon_size, int highlight_hpadding) { - return (buttons - 1)*icon_spacing + buttons*icon_size + 2*highlight_hpadding; + return (unsigned int) ((buttons - 1)*icon_spacing + buttons*icon_size + 2*highlight_hpadding); } // A function to add a hotkey to the linked list void add_hotkey(const char *keycode, const char *cmd) { - // Convert hex string to binary - static hotkey_t *current_hotkey = NULL; - SDL_Keycode code = (SDL_Keycode) strtol(keycode, NULL, 16); - - // Create first node if not initialized, else add to end of linked list - if (current_hotkey == NULL) { - hotkeys = malloc(sizeof(hotkey_t)); - current_hotkey = hotkeys; - } - else { - current_hotkey->next = malloc(sizeof(hotkey_t)); - current_hotkey = current_hotkey->next; - } - current_hotkey->keycode = code; - copy_string(¤t_hotkey->cmd, cmd); - current_hotkey->next = NULL; + if (keycode[0] != '#') + return; + char *p = (char*) keycode + 1; + + // Convert hex string to binary + static Hotkey *current_hotkey = NULL; + SDL_Keycode code = (SDL_Keycode) strtol(p, NULL, 16); + + // Check if exit hotkey for Windows +#ifdef _WIN32 + if (MATCH(cmd, SCMD_EXIT)) { + set_exit_hotkey(code); + return; + } +#endif + + // Create first node if not initialized, else add to end of linked list + if (current_hotkey == NULL) { + hotkeys = malloc(sizeof(Hotkey)); + current_hotkey = hotkeys; + } + else { + current_hotkey->next = malloc(sizeof(Hotkey)); + current_hotkey = current_hotkey->next; + } + current_hotkey->keycode = code; + current_hotkey->cmd = strdup(cmd); + current_hotkey->next = NULL; } // A function to add a gamepad control to the linked list -void add_gamepad_control(int type, int index, const char *label, const char *cmd) +static void add_gamepad_control(const char *label, const char *cmd) { - static gamepad_control_t *current_gamepad_control = NULL; - if (cmd[0] == '\0') { - return; - } - - // Begin the linked list if none exists - if (current_gamepad_control == NULL) { - gamepad_controls = malloc(sizeof(gamepad_control_t)); - current_gamepad_control = gamepad_controls; - } - - // Add another node to the linked list - else { - current_gamepad_control->next = malloc(sizeof(gamepad_control_t)); - current_gamepad_control = current_gamepad_control->next; - } - - // Copy the parameters in the struct - current_gamepad_control->type = type; - current_gamepad_control->index = index; - current_gamepad_control->repeat = 0; - copy_string(¤t_gamepad_control->label, label); - copy_string(¤t_gamepad_control->cmd, cmd); - current_gamepad_control->next = NULL; + if (cmd[0] == '\0') + return; + + // Table of gamepad info + static const struct gamepad_info info[] = { + {SETTING_GAMEPAD_LSTICK_XM, TYPE_AXIS_NEG, SDL_CONTROLLER_AXIS_LEFTX}, + {SETTING_GAMEPAD_LSTICK_XP, TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_LEFTX}, + {SETTING_GAMEPAD_LSTICK_YM, TYPE_AXIS_NEG, SDL_CONTROLLER_AXIS_LEFTY}, + {SETTING_GAMEPAD_LSTICK_YP, TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_LEFTY}, + {SETTING_GAMEPAD_RSTICK_XM, TYPE_AXIS_NEG, SDL_CONTROLLER_AXIS_RIGHTX}, + {SETTING_GAMEPAD_RSTICK_XP, TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_RIGHTX}, + {SETTING_GAMEPAD_RSTICK_YM, TYPE_AXIS_NEG, SDL_CONTROLLER_AXIS_RIGHTY}, + {SETTING_GAMEPAD_RSTICK_YP, TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_RIGHTY}, + {SETTING_GAMEPAD_LTRIGGER, TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_TRIGGERLEFT}, + {SETTING_GAMEPAD_RTRIGGER, TYPE_AXIS_POS, SDL_CONTROLLER_AXIS_TRIGGERRIGHT}, + {SETTING_GAMEPAD_BUTTON_A, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_A}, + {SETTING_GAMEPAD_BUTTON_B, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_B}, + {SETTING_GAMEPAD_BUTTON_X, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_X}, + {SETTING_GAMEPAD_BUTTON_Y, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_Y}, + {SETTING_GAMEPAD_BUTTON_BACK, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_BACK}, + {SETTING_GAMEPAD_BUTTON_GUIDE, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_GUIDE}, + {SETTING_GAMEPAD_BUTTON_START, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_START}, + {SETTING_GAMEPAD_BUTTON_LEFT_STICK, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_LEFTSTICK}, + {SETTING_GAMEPAD_BUTTON_RIGHT_STICK, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_RIGHTSTICK}, + {SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_LEFTSHOULDER}, + {SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER}, + {SETTING_GAMEPAD_BUTTON_DPAD_UP, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_DPAD_UP}, + {SETTING_GAMEPAD_BUTTON_DPAD_DOWN, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_DPAD_DOWN}, + {SETTING_GAMEPAD_BUTTON_DPAD_LEFT, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_DPAD_LEFT}, + {SETTING_GAMEPAD_BUTTON_DPAD_RIGHT, TYPE_BUTTON, SDL_CONTROLLER_BUTTON_DPAD_RIGHT} + }; + + // Find correct gamepad info for label, return if none found + size_t i; + for (i = 0; i < sizeof(info) / sizeof(info[0]); i++) { + if (MATCH(info[i].label, label)) + break; + } + if (i == sizeof(info) / sizeof(info[0])) + return; + + // Begin the linked list if none exists + static GamepadControl *current_gamepad_control = NULL; + if (current_gamepad_control == NULL) { + gamepad_controls = malloc(sizeof(GamepadControl)); + current_gamepad_control = gamepad_controls; + } + + // Add another node to the linked list + else { + current_gamepad_control->next = malloc(sizeof(GamepadControl)); + current_gamepad_control = current_gamepad_control->next; + } + + // Copy the parameters in the struct + *current_gamepad_control = (GamepadControl) { + .type = info[i].type, + .index = info[i].index, + .label = info[i].label, + .repeat = 0, + .next = NULL + }; + current_gamepad_control->cmd = strdup(cmd); } // A function to convert a string percent setting to an int value void convert_percent_to_int(char *string, int *result, int max_value) { - int length = strlen(string); - char tmp[PERCENT_MAX_CHARS]; - strcpy(tmp, string); - tmp[length - 1] = '\0'; - float percent = atof(tmp); - if (percent >= 0.0F && percent <= 100.0F) { - *result = (int) ((percent / 100.0F) * (float) max_value); - } + size_t length = strlen(string); + char tmp[PERCENT_MAX_CHARS]; + copy_string(tmp, string, sizeof(tmp)); + tmp[length - 1] = '\0'; + float percent = (float) atof(tmp); + if (percent >= 0.0F && percent <= 100.0F) + *result = (int) ((percent / 100.0F) * (float) max_value); } // A function to make sure all settings are in their correct range -void validate_settings(geometry_t *geo) +void validate_settings(Geometry *geo) { - // Reduce number of buttons if they can't all fit on screen - if (config.icon_size * config.max_buttons > geo->screen_width) { - int i; - for (i = config.max_buttons; i * config.icon_size > geo->screen_width && i > 0; i--); - output_log(LOGLEVEL_ERROR, "Error: Not enough screen space for %i buttons, reducing to %i\n", - config.max_buttons, - i); - config.max_buttons = i; - } - - // Convert % opacity settings to 0-255 - if (config.title_opacity[0] != '\0') { - int title_opacity = INVALID_PERCENT_VALUE; - convert_percent_to_int(config.title_opacity, &title_opacity, 255); - if (title_opacity != INVALID_PERCENT_VALUE) { - config.title_color.a = (Uint8) title_opacity; - } - } - if (config.highlight_opacity[0] != '\0') { - int highlight_opacity = INVALID_PERCENT_VALUE; - convert_percent_to_int(config.highlight_opacity, &highlight_opacity, 255); - if (highlight_opacity != INVALID_PERCENT_VALUE) { - config.highlight_color.a = (Uint8) highlight_opacity; - } - } - if (config.scroll_indicator_opacity[0] != '\0') { - int scroll_indicator_opacity = INVALID_PERCENT_VALUE; - convert_percent_to_int(config.scroll_indicator_opacity, &scroll_indicator_opacity, 255); - if (scroll_indicator_opacity != INVALID_PERCENT_VALUE) { - config.scroll_indicator_color.a = (Uint8) scroll_indicator_opacity; - } - } - if (config.clock_opacity[0] != '\0') { - int clock_opacity = INVALID_PERCENT_VALUE; - convert_percent_to_int(config.clock_opacity, &clock_opacity, 255); - if (clock_opacity != INVALID_PERCENT_VALUE) { - config.clock_color.a = (Uint8) clock_opacity; - } - } - - // Set default IconSpacing if none is in the config file - if (config.icon_spacing < 0) { - int icon_spacing = INVALID_PERCENT_VALUE; - if (config.icon_spacing_str[0] != '\0') { - convert_percent_to_int(config.icon_spacing_str, &icon_spacing, geo->screen_width); - } - if (icon_spacing == INVALID_PERCENT_VALUE) { - convert_percent_to_int(DEFAULT_ICON_SPACING, &icon_spacing, geo->screen_width); - } - config.icon_spacing = icon_spacing; - } - - // Convert clock margin setting and check limits - if (config.clock_margin < 0) { - int clock_margin = INVALID_PERCENT_VALUE; - if (config.clock_margin_str[0] != '\0') { - convert_percent_to_int(config.clock_margin_str, &clock_margin, geo->screen_height); - } - if (clock_margin == INVALID_PERCENT_VALUE) { - convert_percent_to_int(DEFAULT_CLOCK_MARGIN, &clock_margin, geo->screen_height); - } - config.clock_margin = clock_margin; - } - int clock_margin_limit = (int) ((float) geo->screen_height*MAX_CLOCK_MARGIN); - if (config.clock_margin > clock_margin_limit) { - config.clock_margin = clock_margin_limit; - } - - // Reduce highlight hpadding to prevent overlaps - if (config.highlight_hpadding > (config.icon_spacing / 2)) { - config.highlight_hpadding = config.icon_spacing / 2; - } - - // Reduce icon spacing and highlight padding if too large to fit onscreen - unsigned int required_length = calculate_width(config.max_buttons, - config.icon_spacing, - config.icon_size, - config.highlight_hpadding); - int highlight_hpadding = config.highlight_hpadding; - int icon_spacing = config.icon_spacing; - for (int i = 0; i < 100 && required_length > geo->screen_width; i++) { - if (highlight_hpadding > 0) { - highlight_hpadding = (highlight_hpadding * 9) / 10; - } - if (icon_spacing > 0) { - icon_spacing = (icon_spacing * 9) / 10; + // Reduce number of buttons if they can't all fit on screen + if (config.icon_size * config.max_buttons > (unsigned int) geo->screen_width) { + unsigned int i; + for (i = config.max_buttons; i * config.icon_size > (unsigned int) geo->screen_width && i > 0; i--); + log_error( + "Not enough screen space for %i buttons, reducing to %i", + config.max_buttons, + i + ); + config.max_buttons = i; + } + + if (!config.titles_enabled) + config.title_padding = 0; + + // Convert % opacity settings to 0-255 + if (config.title_opacity[0] != '\0') { + int title_opacity = INVALID_PERCENT_VALUE; + convert_percent_to_int(config.title_opacity, &title_opacity, 255); + if (title_opacity != INVALID_PERCENT_VALUE) + config.title_font_color.a = (Uint8) title_opacity; + } + if (config.background_overlay_opacity[0] != '\0') { + int background_overlay_opacity = INVALID_PERCENT_VALUE; + convert_percent_to_int(config.background_overlay_opacity, &background_overlay_opacity, 255); + if (background_overlay_opacity != INVALID_PERCENT_VALUE) + config.background_overlay_color.a = (Uint8) background_overlay_opacity; + } + + if (config.highlight_fill_opacity[0] != '\0') { + int highlight_fill_opacity = INVALID_PERCENT_VALUE; + convert_percent_to_int(config.highlight_fill_opacity, &highlight_fill_opacity, 255); + if (highlight_fill_opacity != INVALID_PERCENT_VALUE) + config.highlight_fill_color.a = (Uint8) highlight_fill_opacity; + } + if (config.highlight_outline_opacity[0] != '\0') { + int highlight_outline_opacity = INVALID_PERCENT_VALUE; + convert_percent_to_int(config.highlight_outline_opacity, &highlight_outline_opacity, 255); + if (highlight_outline_opacity != INVALID_PERCENT_VALUE) + config.highlight_outline_color.a = (Uint8) highlight_outline_opacity; + } + if (config.scroll_indicator_opacity[0] != '\0') { + int scroll_indicator_opacity = INVALID_PERCENT_VALUE; + convert_percent_to_int(config.scroll_indicator_opacity, &scroll_indicator_opacity, 255); + if (scroll_indicator_opacity != INVALID_PERCENT_VALUE) { + config.scroll_indicator_fill_color.a = (Uint8) scroll_indicator_opacity; + config.scroll_indicator_outline_color.a = config.scroll_indicator_fill_color.a; + } } - required_length = calculate_width(config.max_buttons,icon_spacing,config.icon_size,highlight_hpadding); - } - if (config.highlight_hpadding != highlight_hpadding) { - output_log(LOGLEVEL_ERROR, - "Error: Highlight padding value %i too large to fit screen, shrinking to %i\n", - config.highlight_hpadding, - highlight_hpadding); - config.highlight_hpadding = highlight_hpadding; - } - if (config.icon_spacing != icon_spacing) { - output_log(LOGLEVEL_ERROR, - "Error: Icon spacing value %i too large to fit screen, shrinking to %i\n", - config.icon_spacing, - icon_spacing); - config.icon_spacing = icon_spacing; - } - - // Make sure title padding is in valid range - if (config.title_padding < 0 || config.title_padding > config.icon_size / 2) { - int title_padding = config.icon_size / 10; - output_log(LOGLEVEL_ERROR, - "Error: Text padding value %i invalid, changing to %i\n", - config.title_padding, - title_padding); - config.title_padding = title_padding; - } - - // Calculate y margin for buttons from centerline setting string, check limits - int button_centerline = INVALID_PERCENT_VALUE; - int button_height = config.icon_size + config.title_padding + geo->font_height; - float f_screen_height = (float) geo->screen_height; - int lower_limit = (int) (MIN_BUTTON_CENTERLINE*f_screen_height); - int upper_limit = (int) (MAX_BUTTON_CENTERLINE*f_screen_height); - - // Convert percent to int - if (config.button_centerline[0] != '\0') { - convert_percent_to_int(config.button_centerline, &button_centerline, geo->screen_height); - } - if (button_centerline == INVALID_PERCENT_VALUE) { - convert_percent_to_int(DEFAULT_BUTTON_CENTERLINE, &button_centerline, geo->screen_height); - } - - // Check limits, calculate margin - if (button_centerline < lower_limit) { - button_centerline = lower_limit; - } - else if (button_centerline > upper_limit) { - button_centerline = upper_limit; - } - geo->y_margin = button_centerline - button_height / 2; + if (config.clock_opacity[0] != '\0') { + int clock_opacity = INVALID_PERCENT_VALUE; + convert_percent_to_int(config.clock_opacity, &clock_opacity, 255); + if (clock_opacity != INVALID_PERCENT_VALUE) + config.clock_font_color.a = (Uint8) clock_opacity; + } + + // Set default IconSpacing if none is in the config file + if (config.icon_spacing < 0) { + int icon_spacing = INVALID_PERCENT_VALUE; + if (config.icon_spacing_str[0] != '\0') + convert_percent_to_int(config.icon_spacing_str, &icon_spacing, geo->screen_width); + if (icon_spacing == INVALID_PERCENT_VALUE) + convert_percent_to_int(DEFAULT_ICON_SPACING, &icon_spacing, geo->screen_width); + config.icon_spacing = icon_spacing; + } + + // Convert clock margin setting and check limits + if (config.clock_margin < 0) { + int clock_margin = INVALID_PERCENT_VALUE; + if (config.clock_margin_str[0] != '\0') + convert_percent_to_int(config.clock_margin_str, &clock_margin, geo->screen_height); + if (clock_margin == INVALID_PERCENT_VALUE) + convert_percent_to_int(DEFAULT_CLOCK_MARGIN, &clock_margin, geo->screen_height); + config.clock_margin = clock_margin; + } + int clock_margin_limit = (int) ((float) geo->screen_height*MAX_CLOCK_MARGIN); + if (config.clock_margin > clock_margin_limit) + config.clock_margin = clock_margin_limit; + + // Reduce highlight hpadding to prevent overlaps + if (config.highlight_hpadding > (config.icon_spacing / 2)) + config.highlight_hpadding = config.icon_spacing / 2; + + // Reduce icon spacing and highlight padding if too large to fit onscreen + unsigned int required_length = calculate_width((int) config.max_buttons, + config.icon_spacing, + config.icon_size, + config.highlight_hpadding + ); + int highlight_hpadding = config.highlight_hpadding; + int icon_spacing = config.icon_spacing; + for (int i = 0; i < 100 && required_length > (unsigned int) geo->screen_width; i++) { + if (highlight_hpadding > 0) + highlight_hpadding = (highlight_hpadding * 9) / 10; + if (icon_spacing > 0) + icon_spacing = (icon_spacing * 9) / 10; + required_length = calculate_width((int) config.max_buttons,icon_spacing,config.icon_size,highlight_hpadding); + } + if (config.highlight_hpadding != highlight_hpadding) { + log_error("Highlight padding value %i too large to fit screen, shrinking to %i", + config.highlight_hpadding, + highlight_hpadding + ); + config.highlight_hpadding = highlight_hpadding; + } + if (config.icon_spacing != icon_spacing) { + log_error("Icon spacing value %i too large to fit screen, shrinking to %i", + config.icon_spacing, + icon_spacing + ); + config.icon_spacing = icon_spacing; + } + + // Make sure title padding is in valid range + if (config.title_padding < 0 || config.title_padding > config.icon_size / 2) { + int title_padding = config.icon_size / 10; + log_error("Text padding value %i invalid, changing to %i", + config.title_padding, + title_padding + ); + config.title_padding = title_padding; + } + + // Calculate y margin for buttons from centerline setting string, check limits + int vcenter = INVALID_PERCENT_VALUE; + int button_height = config.icon_size + config.title_padding + geo->font_height; + float f_screen_height = (float) geo->screen_height; + int lower_limit = (int) (MIN_VCENTER*f_screen_height); + int upper_limit = (int) (MAX_VCENTER*f_screen_height); + + // Convert percent to int + if (config.vcenter[0] != '\0') + convert_percent_to_int(config.vcenter, &vcenter, geo->screen_height); + if (vcenter == INVALID_PERCENT_VALUE) + convert_percent_to_int(DEFAULT_VCENTER, &vcenter, geo->screen_height); + + // Check limits, calculate margin + if (vcenter < lower_limit) + vcenter = lower_limit; + else if (vcenter > upper_limit) + vcenter = upper_limit; + geo->y_margin = vcenter - button_height / 2; + + // Max highlight outline + int max_highlight_outline_size = (config.highlight_hpadding < config.highlight_vpadding) + ? config.highlight_hpadding : config.highlight_vpadding; + if (config.highlight_outline_size > max_highlight_outline_size) + config.highlight_outline_size = max_highlight_outline_size; + + // Max scroll indicator outline + int max_scroll_indicator_outline_size = (int) ((float) geo->screen_height * MAX_SCROLL_INDICATOR_OUTLINE); + if (config.scroll_indicator_outline_size > max_scroll_indicator_outline_size) + config.scroll_indicator_outline_size = max_scroll_indicator_outline_size; + + // Don't allow rounded rectangle with outline due to Nanosvg bug + if (config.highlight_rx && config.highlight_outline_size) + config.highlight_rx = 0; } // A function to retreive menu struct from the linked list via the menu name -menu_t *get_menu(char *menu_name) +Menu *get_menu(const char *menu_name) { - for (menu_t *menu = config.first_menu; menu != NULL; menu = menu->next) { - if (!strcmp(menu_name, menu->name)) { - return menu; + for (Menu *menu = config.first_menu; menu != NULL; menu = menu->next) { + if (MATCH(menu_name, menu->name)) + return menu; } - } - output_log(LOGLEVEL_ERROR, - "Error: Menu \"%s\" not found in config file\n", - menu_name - ); - return NULL; + log_error("Menu '%s' not found in config file", menu_name); + return NULL; } // A function to allocate memory to and initialize a menu struct -menu_t *create_menu(char *menu_name, int *num_menus) +Menu *create_menu(const char *menu_name, size_t *num_menus) { - menu_t *menu = malloc(sizeof(menu_t)); - copy_string(&menu->name, menu_name); - menu->first_entry = NULL; - menu->next = NULL; - menu->back = NULL; - menu->root_entry = NULL; - menu->num_entries = 0; - menu->page = 0; - menu->highlight_position = 0; - menu->rendered = false; - (*num_menus)++; - return menu; + Menu *menu = malloc(sizeof(Menu)); + *menu = (Menu) { + .first_entry = NULL, + .next = NULL, + .back = NULL, + .root_entry = NULL, + .num_entries = 0, + .page = 0, + .highlight_position = 0, + .rendered = false + }; + menu->name = strdup(menu_name); + (*num_menus)++; + + return menu; } // A function to advance X spaces in the entry linked list (left or right) -entry_t *advance_entries(entry_t *entry, int spaces, mode direction) +Entry *advance_entries(Entry *entry, int spaces, Direction direction) { - if (direction == DIRECTION_LEFT) { - for (int i = 0; i < spaces; i++) { - entry = entry->previous; + if (direction == DIRECTION_LEFT) { + for (int i = 0; i < spaces; i++) + entry = entry->previous; } - } - else if (direction == DIRECTION_RIGHT) { - for (int i = 0; i < spaces; i++) { - entry = entry->next; + else if (direction == DIRECTION_RIGHT) { + for (int i = 0; i < spaces; i++) + entry = entry->next; } - } - return entry; + return entry; } -// A function to read a file into a buffer -void read_file(const char *path, char **buffer) +// A function to dynamically allocate a buffer for and copy a formatted string +void sprintf_alloc(char **buffer, const char *format, ...) { - SDL_RWops *file = SDL_RWFromFile(path, "rb"); - if (file == NULL) { - output_log(LOGLEVEL_ERROR, "Error: Could not open file\n%s\n", SDL_GetError()); - return; - } - - // Allocate buffer - Sint64 file_size = SDL_RWsize(file); - *buffer = malloc(file_size + 1); - if (*buffer == NULL) { - return; - } - - // Read contents of file into buffer - Sint64 total_bytes_read = 0; - Sint64 current_bytes_read; - char *p = *buffer; - do { - current_bytes_read = SDL_RWread(file, p, 1, file_size - total_bytes_read); - total_bytes_read += current_bytes_read; - p += current_bytes_read; - } while (total_bytes_read < file_size && total_bytes_read > 0); - SDL_RWclose(file); - - if (total_bytes_read != file_size) { - free(*buffer); - *buffer = NULL; - } - *(*buffer + total_bytes_read) = '\0'; -} + va_list args1, args2; + va_start(args1, format); + va_copy(args2, args1); + + size_t length = (size_t) vsnprintf(NULL, 0, format, args1); + if (length) { + *buffer = malloc(length + 1); + vsnprintf(*buffer, length + 1, format, args2); + } + va_end(args1); + va_end(args2); +} \ No newline at end of file diff --git a/src/util.h b/src/util.h index bf3792f..90dc0cf 100755 --- a/src/util.h +++ b/src/util.h @@ -8,28 +8,42 @@ #define PATH_SEPARATOR "/" #endif +#define UNUSED(x) (void)(x) +#define SELECTED_SUFFIX "_selected" +#define LEN(x) ((sizeof(x)/sizeof(x[0])) - sizeof(x[0])) +#define MATCH(x, y) !strcmp(x, y) + +#define DIV_ROUND_UP(a, b) ((a + (b - 1)) / b) +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) + +struct gamepad_info { + const char *label; + int type; + int index; +}; + int config_handler(void *user, const char *section, const char *name, const char *value); int convert_percent(const char *string, int max_value); +const char *get_mode_setting(int type, int value); int utf8_length(const char *string); unsigned int calculate_width(int buttons, int icon_spacing, int icon_size, int highlight_padding); bool hex_to_color(const char *string, SDL_Color *color); -bool convert_bool(const char *string, bool default_setting); +bool convert_bool(const char *string, bool *setting); bool is_percent(const char *string); -static bool ends_with(const char *string, const char *phrase); -char *join_paths(char *buffer, int num_paths, ...); +char *selected_path(const char *path); +char *join_paths(char *buffer, size_t bytes, int num_paths, ...); char *find_file(const char *file, int num_prefixes, const char **prefixes); void handle_arguments(int argc, char *argv[], char **config_file_path); -void copy_string(char **dest, const char *string); +void copy_string(char* dest, const char* string, size_t size); void utf8_truncate(char *string, int width, int max_width); void convert_percent_to_int(char *string, int *result, int max_value); void add_hotkey(const char *keycode, const char *cmd); -void add_gamepad_control(int type, int index, const char *label, const char *cmd); void random_array(int *array, int array_size); void clean_path(char *path); -void validate_settings(geometry_t *geo); +void validate_settings(Geometry *geo); void parse_config_file(const char *config_file_path); void read_file(const char *path, char **buffer); +void sprintf_alloc(char **buffer, const char *format, ...); Uint16 get_unicode_code_point(const char *p, int *bytes); -menu_t *get_menu(char *menu_name); -menu_t *create_menu(char *menu_name, int *num_menus); -entry_t *advance_entries(entry_t *entry, int spaces, mode direction); \ No newline at end of file +Menu *get_menu(const char *menu_name); +Entry *advance_entries(Entry *entry, int spaces, Direction direction); \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..c39abf0 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,12 @@ +{ + "dependencies": [ + "sdl2", + { + "name": "sdl2-image", + "features": ["libjpeg-turbo", "libwebp"] + }, + "sdl2-ttf", + "inih", + "getopt" + ] +}