From d725fdb4f3b15167b4154e6b88a27ecb112b0ead Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Sat, 23 Sep 2023 05:17:31 +0200 Subject: [PATCH] Add screen reader support --- .github/workflows/Windows_MinGW_x64.yml | 2 +- .github/workflows/Windows_MinGW_x86.yml | 2 +- 3rdParty/tolk/CMakeLists.txt | 25 ++++++++++ CMake/Definitions.cmake | 1 + CMake/Dependencies.cmake | 8 ++++ CMake/finders/FindSpeechd.cmake | 17 +++++++ CMakeLists.txt | 8 ++++ Packaging/nix/debian-cross-aarch64-prep.sh | 1 + Packaging/nix/debian-cross-i386-prep.sh | 2 +- Packaging/nix/debian-host-prep.sh | 2 +- Source/CMakeLists.txt | 22 +++++++++ Source/DiabloUI/diabloui.cpp | 3 ++ Source/control.cpp | 3 ++ Source/diablo.cpp | 4 ++ Source/utils/screen_reader.cpp | 54 ++++++++++++++++++++++ Source/utils/screen_reader.hpp | 25 ++++++++++ 16 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 3rdParty/tolk/CMakeLists.txt create mode 100644 CMake/finders/FindSpeechd.cmake create mode 100644 Source/utils/screen_reader.cpp create mode 100644 Source/utils/screen_reader.hpp diff --git a/.github/workflows/Windows_MinGW_x64.yml b/.github/workflows/Windows_MinGW_x64.yml index 941ecc02f..60ca9448b 100644 --- a/.github/workflows/Windows_MinGW_x64.yml +++ b/.github/workflows/Windows_MinGW_x64.yml @@ -36,7 +36,7 @@ jobs: - name: Configure CMake shell: bash working-directory: ${{github.workspace}} - run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=OFF -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc64.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON -DDISCORD_INTEGRATION=ON + run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=OFF -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc64.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON -DDISCORD_INTEGRATION=ON -DSCREEN_READER_INTEGRATION=ON - name: Build working-directory: ${{github.workspace}} diff --git a/.github/workflows/Windows_MinGW_x86.yml b/.github/workflows/Windows_MinGW_x86.yml index a15058e86..cff307e41 100644 --- a/.github/workflows/Windows_MinGW_x86.yml +++ b/.github/workflows/Windows_MinGW_x86.yml @@ -36,7 +36,7 @@ jobs: - name: Configure CMake shell: bash working-directory: ${{github.workspace}} - run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=OFF -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON -DDISCORD_INTEGRATION=ON + run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=OFF -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON -DDISCORD_INTEGRATION=ON -DSCREEN_READER_INTEGRATION=ON - name: Build working-directory: ${{github.workspace}} diff --git a/3rdParty/tolk/CMakeLists.txt b/3rdParty/tolk/CMakeLists.txt new file mode 100644 index 000000000..9b4cee23c --- /dev/null +++ b/3rdParty/tolk/CMakeLists.txt @@ -0,0 +1,25 @@ +include(functions/FetchContent_MakeAvailableExcludeFromAll) + +include(FetchContent) +FetchContent_Declare(Tolk + URL https://github.com/sig-a11y/tolk/archive/89de98779e3b6365dc1688538d5de4ecba3fdbab.tar.gz + URL_HASH MD5=724f6022186573dd9c5c2c92ed9e21e6 +) +FetchContent_MakeAvailableExcludeFromAll(Tolk) + +target_include_directories(Tolk PUBLIC ${libTolk_SOURCE_DIR}/src) + +if(CMAKE_SIZEOF_VOID_P EQUAL 4) + set(TOLK_LIB_DIR "${Tolk_SOURCE_DIR}/libs/x86") +else() + set(TOLK_LIB_DIR "${Tolk_SOURCE_DIR}/libs/x64") +endif() +file(GLOB TOLK_DLLS + LIST_DIRECTORIES false + "${TOLK_LIB_DIR}/*.dll" + "${TOLK_LIB_DIR}/*.ini") +foreach(_TOLK_DLL_PATH ${TOLK_DLLS}) + install(FILES "${_TOLK_DLL_PATH}" + DESTINATION "." + ) +endforeach() diff --git a/CMake/Definitions.cmake b/CMake/Definitions.cmake index 5d6b2081f..a444decde 100644 --- a/CMake/Definitions.cmake +++ b/CMake/Definitions.cmake @@ -18,6 +18,7 @@ foreach( DEVILUTIONX_RESAMPLER_SPEEX DEVILUTIONX_RESAMPLER_SDL DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT + SCREEN_READER_INTEGRATION UNPACKED_MPQS UNPACKED_SAVES DEVILUTIONX_WINDOWS_NO_WCHAR diff --git a/CMake/Dependencies.cmake b/CMake/Dependencies.cmake index 9bcbe09ae..da00a27b4 100644 --- a/CMake/Dependencies.cmake +++ b/CMake/Dependencies.cmake @@ -24,6 +24,14 @@ if(SUPPORTS_MPQ) endif() endif() +if(SCREEN_READER_INTEGRATION) + if(WIN32) + add_subdirectory(3rdParty/tolk) + else() + find_package(Speechd REQUIRED) + endif() +endif() + if(EMSCRIPTEN) # We use `USE_PTHREADS=1` here to get a version of SDL2 that supports threads. emscripten_system_library("SDL2" SDL2::SDL2 USE_SDL=2 USE_PTHREADS=1) diff --git a/CMake/finders/FindSpeechd.cmake b/CMake/finders/FindSpeechd.cmake new file mode 100644 index 000000000..e02bf82ad --- /dev/null +++ b/CMake/finders/FindSpeechd.cmake @@ -0,0 +1,17 @@ +# find speech-dispatcher library and header if available +# Copyright (c) 2009, Jeremy Whiting +# Copyright (c) 2011, Raphael Kubo da Costa +# This module defines +# SPEECHD_INCLUDE_DIR, where to find libspeechd.h +# SPEECHD_LIBRARIES, the libraries needed to link against speechd +# SPEECHD_FOUND, If false, speechd was not found +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +find_path(SPEECHD_INCLUDE_DIR libspeechd.h PATH_SUFFIXES speech-dispatcher) + +find_library(SPEECHD_LIBRARIES NAMES speechd) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Speechd REQUIRED_VARS SPEECHD_INCLUDE_DIR SPEECHD_LIBRARIES) diff --git a/CMakeLists.txt b/CMakeLists.txt index e5ded7b00..8906a4181 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,6 +153,8 @@ mark_as_advanced(DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT) # Additional features option(DISABLE_DEMOMODE "Disable demo mode support" OFF) option(DISCORD_INTEGRATION "Build with Discord SDK for rich presence support" OFF) +option(SCREEN_READER_INTEGRATION "Build with screen reader support" OFF) +mark_as_advanced(SCREEN_READER_INTEGRATION) # If both UNPACKED_MPQS and UNPACKED_SAVES are enabled, we completely remove MPQ support. if(UNPACKED_MPQS AND UNPACKED_SAVES) @@ -550,6 +552,12 @@ if(CPACK AND (APPLE OR BUILD_ASSETS_MPQ OR SRC_DIST)) ) endif() + if(SCREEN_READER_INTEGRATION) + install(FILES "${Tolk_BINARY_DIR}/libTolk.dll" + DESTINATION "." + ) + endif() + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") string(TOLOWER ${PROJECT_NAME} project_name) set(CPACK_PACKAGE_NAME ${project_name}) diff --git a/Packaging/nix/debian-cross-aarch64-prep.sh b/Packaging/nix/debian-cross-aarch64-prep.sh index b2d79d5f1..22704264c 100755 --- a/Packaging/nix/debian-cross-aarch64-prep.sh +++ b/Packaging/nix/debian-cross-aarch64-prep.sh @@ -23,6 +23,7 @@ PACKAGES=( cmake git smpq gettext dpkg-cross libc-dev-arm64-cross libsdl2-dev:arm64 libsdl2-image-dev:arm64 libsodium-dev:arm64 libsimpleini-dev:arm64 libpng-dev:arm64 libbz2-dev:arm64 libfmt-dev:arm64 + libspeechd-dev:arm64 ) if (( $# < 1 )) || [[ "$1" != --no-gcc ]]; then diff --git a/Packaging/nix/debian-cross-i386-prep.sh b/Packaging/nix/debian-cross-i386-prep.sh index 1adfee994..1b0f79218 100755 --- a/Packaging/nix/debian-cross-i386-prep.sh +++ b/Packaging/nix/debian-cross-i386-prep.sh @@ -5,7 +5,7 @@ set -x PACKAGES=( cmake git smpq gettext libsdl2-dev:i386 libsdl2-image-dev:i386 libsodium-dev:i386 - libpng-dev:i386 libbz2-dev:i386 libfmt-dev:i386 + libpng-dev:i386 libbz2-dev:i386 libfmt-dev:i386 libspeechd-dev:i386 ) if (( $# < 1 )) || [[ "$1" != --no-gcc ]]; then diff --git a/Packaging/nix/debian-host-prep.sh b/Packaging/nix/debian-host-prep.sh index bf0827390..b46223fb5 100755 --- a/Packaging/nix/debian-host-prep.sh +++ b/Packaging/nix/debian-host-prep.sh @@ -4,7 +4,7 @@ set -x PACKAGES=( rpm pkg-config cmake git smpq gettext libsdl2-dev libsdl2-image-dev libsodium-dev - libpng-dev libbz2-dev libfmt-dev + libpng-dev libbz2-dev libfmt-dev libspeechd-dev ) if (( $# < 1 )) || [[ "$1" != --no-gcc ]]; then diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index f7adf835e..a027ccdd3 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -251,9 +251,19 @@ if(DISCORD_INTEGRATION) ) endif() +if(SCREEN_READER_INTEGRATION) + list(APPEND libdevilutionx_SRCS + utils/screen_reader.cpp + ) +endif() + add_devilutionx_library(libdevilutionx OBJECT ${libdevilutionx_SRCS}) target_include_directories(libdevilutionx PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) +if(SCREEN_READER_INTEGRATION AND NOT WIN32) + target_include_directories(libdevilutionx PUBLIC ${Speechd_INCLUDE_DIRS}) +endif() + # Use file GENERATE instead of configure_file because configure_file # does not support generator expressions. get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) @@ -277,6 +287,10 @@ if(DISCORD_INTEGRATION) target_link_libraries(libdevilutionx PRIVATE discord discord_game_sdk) endif() +if(SCREEN_READER_INTEGRATION AND WIN32) + target_compile_definitions(libdevilutionx PRIVATE Tolk) +endif() + target_link_libraries(libdevilutionx PUBLIC Threads::Threads DevilutionX::SDL @@ -288,6 +302,14 @@ target_link_libraries(libdevilutionx PUBLIC ${libdevilutionx_DEPS} ) +if(SCREEN_READER_INTEGRATION) + if(WIN32) + target_link_libraries(libdevilutionx PUBLIC Tolk) + else() + target_link_libraries(libdevilutionx PUBLIC speechd) + endif() +endif() + if(NOT USE_SDL1) target_link_libraries(libdevilutionx PUBLIC SDL2::SDL2_image) endif() diff --git a/Source/DiabloUI/diabloui.cpp b/Source/DiabloUI/diabloui.cpp index 9265d7819..4cd106ea2 100644 --- a/Source/DiabloUI/diabloui.cpp +++ b/Source/DiabloUI/diabloui.cpp @@ -24,6 +24,7 @@ #include "utils/language.h" #include "utils/log.hpp" #include "utils/pcx_to_clx.hpp" +#include "utils/screen_reader.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/sdl_wrap.h" @@ -151,6 +152,7 @@ void UiInitList(void (*fnFocus)(int value), void (*fnSelect)(int value), void (* gUiList = uiList; if (selectedItem <= SelectedItemMax && HasAnyOf(uiList->GetItem(selectedItem)->uiFlags, UiFlags::NeedsNextElement)) AdjustListOffset(selectedItem + 1); + SpeakText(uiList->GetItem(selectedItem)->m_text); } else if (item->IsType(UiType::Scrollbar)) { uiScrollbar = static_cast(item.get()); } @@ -225,6 +227,7 @@ void UiFocus(std::size_t itemIndex, bool checkUp, bool ignoreItemsWraps = false) } pItem = gUiList->GetItem(itemIndex); } + SpeakText(pItem->m_text); if (HasAnyOf(pItem->uiFlags, UiFlags::NeedsNextElement)) AdjustListOffset(itemIndex + 1); diff --git a/Source/control.cpp b/Source/control.cpp index 1761f03ca..058ce3f99 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -50,6 +50,7 @@ #include "utils/language.h" #include "utils/log.hpp" #include "utils/parse_int.hpp" +#include "utils/screen_reader.hpp" #include "utils/sdl_geometry.h" #include "utils/str_case.hpp" #include "utils/str_cat.hpp" @@ -269,6 +270,8 @@ void PrintInfo(const Surface &out) // which throws off the vertical centering infoArea.position.y += spacing / 2; + SpeakText(InfoString); + DrawString(out, InfoString, infoArea, InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::KerningFitSpacing, 2, lineHeight); } diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 1ac2b5bd8..fc16323bc 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -83,6 +83,7 @@ #include "utils/language.h" #include "utils/parse_int.hpp" #include "utils/paths.h" +#include "utils/screen_reader.hpp" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" @@ -1141,6 +1142,7 @@ void ApplicationInit() init_create_window(); was_window_init = true; + InitializeScreenReader(); LanguageInitialize(); SetApplicationVersions(); @@ -1228,6 +1230,8 @@ void DiabloDeinit() { FreeItemGFX(); + ShutDownScreenReader(); + if (gbSndInited) effects_cleanup_sfx(); snd_deinit(); diff --git a/Source/utils/screen_reader.cpp b/Source/utils/screen_reader.cpp new file mode 100644 index 000000000..43e3ecdf9 --- /dev/null +++ b/Source/utils/screen_reader.cpp @@ -0,0 +1,54 @@ +#include "utils/screen_reader.hpp" + +#include +#include + +#ifdef _WIN32 +#include "utils/file_util.h" +#include +#else +#include +#endif + +namespace devilution { + +#ifndef _WIN32 +SPDConnection *Speechd; +#endif + +void InitializeScreenReader() +{ +#ifdef _WIN32 + Tolk_Load(); +#else + Speechd = spd_open("DevilutionX", "DevilutionX", NULL, SPD_MODE_SINGLE); +#endif +} + +void ShutDownScreenReader() +{ +#ifdef _WIN32 + Tolk_Unload(); +#else + spd_close(Speechd); +#endif +} + +void SpeakText(std::string_view text) +{ + static std::string SpokenText; + + if (SpokenText == text) + return; + + SpokenText = text; + +#ifdef _WIN32 + const auto textUtf16 = ToWideChar(SpokenText); + Tolk_Output(&textUtf16[0], true); +#else + spd_say(Speechd, SPD_TEXT, SpokenText.c_str()); +#endif +} + +} // namespace devilution diff --git a/Source/utils/screen_reader.hpp b/Source/utils/screen_reader.hpp new file mode 100644 index 000000000..64a029c1c --- /dev/null +++ b/Source/utils/screen_reader.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace devilution { + +#ifdef SCREEN_READER_INTEGRATION +void InitializeScreenReader(); +void ShutDownScreenReader(); +void SpeakText(std::string_view text); +#else +constexpr void InitializeScreenReader() +{ +} + +constexpr void ShutDownScreenReader() +{ +} + +constexpr void SpeakText(std::string_view text) +{ +} +#endif + +} // namespace devilution