From d31cc486bb442da1f6817197c4c713f95a03c6b0 Mon Sep 17 00:00:00 2001 From: Ritchie Frodomar Date: Fri, 16 May 2025 14:27:49 -0400 Subject: [PATCH] Work around startup UI freeze caused by QtTextToSpeech This merge request works around an annoying startup hang that I introduced when adding text-to-speech to NeoChat. The previous implementation was a QML singleton that used the `TextToSpeech` QML component. Unfortunately that component blocks the UI thread when first loading it, while it connects to speech-dispatcher. This MR just rewrites that singleton in C++, and moves initialization of QtTextToSpeech to the first time you read a message aloud. It doesn't fix the performance problem, but it at least stops it from affecting startup. In the future, I'd like to move speech operations to a background thread to completely mitigate the initialization freeze. --- CMakeLists.txt | 2 +- src/app/CMakeLists.txt | 8 +++---- src/app/qml/Main.qml | 1 - src/app/qml/TextToSpeechWrapper.qml | 24 --------------------- src/app/texttospeechhelper.cpp | 15 +++++++++++++ src/app/texttospeechhelper.h | 21 ++++++++++++++++++ src/timeline/MessageDelegateContextMenu.qml | 2 +- 7 files changed, 41 insertions(+), 32 deletions(-) delete mode 100644 src/app/qml/TextToSpeechWrapper.qml create mode 100644 src/app/texttospeechhelper.cpp create mode 100644 src/app/texttospeechhelper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c23fa0b3b..d3efaf289 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,7 +56,7 @@ ecm_setup_version(${PROJECT_VERSION} VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h ) -find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView) +find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg TextToSpeech WebView) set_package_properties(Qt6 PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 207d9bd5f..3b8e0458f 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -37,10 +37,8 @@ add_library(neochat STATIC identityserverhelper.h models/commonroomsmodel.cpp models/commonroomsmodel.h -) - -set_source_files_properties(qml/TextToSpeechWrapper.qml PROPERTIES - QT_QML_SINGLETON_TYPE TRUE + texttospeechhelper.h + texttospeechhelper.cpp ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES @@ -87,7 +85,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE qml/InvitationView.qml qml/AvatarTabButton.qml qml/OsmLocationPlugin.qml - qml/TextToSpeechWrapper.qml qml/FullScreenMap.qml qml/LocationsPage.qml qml/LocationMapItem.qml @@ -204,6 +201,7 @@ target_link_libraries(neochat PUBLIC Qt::Multimedia Qt::Network Qt::QuickControls2 + Qt::TextToSpeech KF6::I18n KF6::Kirigami KF6::Notifications diff --git a/src/app/qml/Main.qml b/src/app/qml/Main.qml index 219327253..e2e1b4bec 100644 --- a/src/app/qml/Main.qml +++ b/src/app/qml/Main.qml @@ -196,7 +196,6 @@ 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/app/qml/TextToSpeechWrapper.qml b/src/app/qml/TextToSpeechWrapper.qml deleted file mode 100644 index cd4506e4d..000000000 --- a/src/app/qml/TextToSpeechWrapper.qml +++ /dev/null @@ -1,24 +0,0 @@ -// 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 diff --git a/src/app/texttospeechhelper.cpp b/src/app/texttospeechhelper.cpp new file mode 100644 index 000000000..be72d3f8a --- /dev/null +++ b/src/app/texttospeechhelper.cpp @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2025 Ritchie Frodomar +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "texttospeechhelper.h" + +void TextToSpeechHelper::speak(const QString &textToSpeak) +{ + if (!m_speech) { + m_speech = new QTextToSpeech(); + } + + m_speech->say(textToSpeak); +} + +#include "moc_texttospeechhelper.cpp" \ No newline at end of file diff --git a/src/app/texttospeechhelper.h b/src/app/texttospeechhelper.h new file mode 100644 index 000000000..58e4cc3b1 --- /dev/null +++ b/src/app/texttospeechhelper.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2025 Ritchie Frodomar +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include +#include + +class TextToSpeechHelper : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + Q_INVOKABLE void speak(const QString &textToSpeak); + +private: + QTextToSpeech *m_speech = nullptr; +}; \ No newline at end of file diff --git a/src/timeline/MessageDelegateContextMenu.qml b/src/timeline/MessageDelegateContextMenu.qml index 476297227..d7adf2a70 100644 --- a/src/timeline/MessageDelegateContextMenu.qml +++ b/src/timeline/MessageDelegateContextMenu.qml @@ -94,7 +94,7 @@ DelegateContextMenu { 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)) + TextToSpeechHelper.speak(i18nc("@info text-to-speech %1 is author %2 is message text", "%1 said %2", root.author.displayName, root.plainText)) } } Kirigami.Action {