From 7a078b2d34a35684ae6c22ae4b35c591a05d8e74 Mon Sep 17 00:00:00 2001 From: Ritchie Frodomar Date: Thu, 3 Apr 2025 19:51:40 +0000 Subject: [PATCH] Add "Read Text Aloud" context menu action for messages This merge request adds a simple "Read Text Aloud" menu action to the context menu for chat messages. When clicked, the message text will be read aloud using text-to-speech. The intention behind this change is to make it easier for myself and other low-vision users to use Matrix, without needing to use a system-wide screen reader, in cases where the user still has enough sight left to navigate a computer faster without a screen reader. I'd eventually like to have it read " said ," so that the message sender gets read aloud as well, but for now it just reads the plaintext message contents. Another problem, at least on my computer specifically, is that the voice's accent doesn't seem correct. For whatever reason, on my system, messages are read in a Scottish accent which is harder for me to understand. Other apps don't do that, so I'm not sure what's going on there. I do not want to hardcode a specific voice/locale, since I want this feature to work well for everyone and not just me. @teams/qa Please do break my code! :) - I've only tested with basic text messages. @teams/usability Not sure if I put the context menu action in an ideal place, it's grouped in the same area as clipboard actions like "Copy Text." @teams/localization How could I go about getting author names to be read aloud in a way that's properly translated for other languages? I'm not experienced with i18n. --- src/CMakeLists.txt | 5 +++++ src/qml/Main.qml | 1 + src/qml/MessageDelegateContextMenu.qml | 12 ++++++++++++ src/qml/TextToSpeechWrapper.qml | 24 ++++++++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 src/qml/TextToSpeechWrapper.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2cdb3e066..7495016c2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -200,6 +200,10 @@ add_library(neochat STATIC models/pollanswermodel.h ) +set_source_files_properties(qml/TextToSpeechWrapper.qml PROPERTIES + QT_QML_SINGLETON_TYPE TRUE +) + set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE ) @@ -260,6 +264,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE qml/AvatarTabButton.qml qml/SpaceDrawer.qml qml/OsmLocationPlugin.qml + qml/TextToSpeechWrapper.qml qml/FullScreenMap.qml qml/LocationsPage.qml qml/LocationMapItem.qml diff --git a/src/qml/Main.qml b/src/qml/Main.qml index be493a58f..2635e82b5 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -189,6 +189,7 @@ Kirigami.ApplicationWindow { NeoChatSettingsView.window = root; NeoChatSettingsView.connection = root.connection; WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout); + TextToSpeechWrapper.warmUp(); if (ShareHandler.text && root.connection) { root.handleShare() } diff --git a/src/qml/MessageDelegateContextMenu.qml b/src/qml/MessageDelegateContextMenu.qml index 54be17dac..a2b13cff6 100644 --- a/src/qml/MessageDelegateContextMenu.qml +++ b/src/qml/MessageDelegateContextMenu.qml @@ -4,8 +4,13 @@ import QtQuick import QtQuick.Controls as QQC2 +import QtTextToSpeech import QtQuick.Layouts import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.components as KirigamiComponents +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.neochat + import org.kde.kirigamiaddons.components as KirigamiComponents import org.kde.kirigamiaddons.formcard as FormCard @@ -83,6 +88,13 @@ DelegateContextMenu { Clipboard.saveText("https://matrix.to/#/" + currentRoom.id + "/" + root.eventId); } } + QQC2.Action { + text: i18nc("@action:inmenu", "Read Text Aloud") + icon.name: "audio-speakers-symbolic" + onTriggered: { + TextToSpeechWrapper.say(i18nc("@info text-to-speech %1 is author %2 is message text", "%1 said %2", root.author.displayName, root.plainText)) + } + } Kirigami.Action { separator: true } diff --git a/src/qml/TextToSpeechWrapper.qml b/src/qml/TextToSpeechWrapper.qml new file mode 100644 index 000000000..cd4506e4d --- /dev/null +++ b/src/qml/TextToSpeechWrapper.qml @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2025 Ritchie Frodomar +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma Singleton + +import QtQuick +import QtTextToSpeech + +QtObject { + id: root + + readonly property TextToSpeech tts: TextToSpeech { + id: tts + } + + function warmUp() { + // TODO: This method is called on startup to avoid a UI freeze the first time you read a message aloud, but there's nothing for it to do. + // This would be a good place to check if TTS can actually be used. + } + + function say(text: String) { + tts.say(text) + } +} \ No newline at end of file