From 7a759dab7ae90596af4c1005a24dc24b361e86fb Mon Sep 17 00:00:00 2001 From: cberthet <capucine.berthet@savoirfairelinux.com> Date: Tue, 3 Oct 2023 15:06:50 -0400 Subject: [PATCH] Push-To-Talk : add global and local ptt It works with a Pimpl which the right pttlistener.cpp depending on the platform you are on (macOs, windows or X11). It is a global PTT which listen to key events during calls. If the global PTT is not supported, a local PTT is set. https://git.jami.net/savoirfairelinux/jami-project/-/issues/1402 Change-Id: I8399800966c737bb8e8a656ecbb6af7ac7cdde8c --- CMakeLists.txt | 32 +- resources/Info.plist | 2 + src/app/appsettingsmanager.h | 5 +- src/app/calladapter.cpp | 115 ++- src/app/calladapter.h | 15 +- src/app/calloverlaymodel.cpp | 22 +- src/app/calloverlaymodel.h | 13 +- .../commoncomponents/ChangePttKeyPopup.qml | 123 +++ src/app/constant/JamiStrings.qml | 9 + src/app/mainapplication.cpp | 6 +- src/app/mainapplication.h | 10 +- src/app/mainview/components/CallStackView.qml | 11 +- src/app/platform/local/pttlistener.cpp | 42 + src/app/platform/macos/pttlistener.cpp | 390 ++++++++ src/app/platform/windows/pttlistener.cpp | 309 +++++++ src/app/platform/x11/pttlistener.cpp | 192 ++++ src/app/platform/x11/xcbkeyboard.h | 856 ++++++++++++++++++ src/app/pttlistener.h | 52 ++ .../components/CallSettingsPage.qml | 69 ++ src/app/systemtray.h | 5 + src/libclient/callmodel.cpp | 5 +- 21 files changed, 2256 insertions(+), 27 deletions(-) create mode 100644 src/app/commoncomponents/ChangePttKeyPopup.qml create mode 100644 src/app/platform/local/pttlistener.cpp create mode 100644 src/app/platform/macos/pttlistener.cpp create mode 100644 src/app/platform/windows/pttlistener.cpp create mode 100644 src/app/platform/x11/pttlistener.cpp create mode 100644 src/app/platform/x11/xcbkeyboard.h create mode 100644 src/app/pttlistener.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c7c667544..1e70d9226 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -245,7 +245,8 @@ set(COMMON_SOURCES ${APP_SRC_DIR}/previewengine.cpp ${APP_SRC_DIR}/imagedownloader.cpp ${APP_SRC_DIR}/pluginversionmanager.cpp - ${APP_SRC_DIR}/connectioninfolistmodel.cpp) + ${APP_SRC_DIR}/connectioninfolistmodel.cpp + ${APP_SRC_DIR}/pluginversionmanager.cpp) set(COMMON_HEADERS ${APP_SRC_DIR}/avatarimageprovider.h @@ -312,7 +313,8 @@ set(COMMON_HEADERS ${APP_SRC_DIR}/htmlparser.h ${APP_SRC_DIR}/imagedownloader.h ${APP_SRC_DIR}/pluginversionmanager.h - ${APP_SRC_DIR}/connectioninfolistmodel.h) + ${APP_SRC_DIR}/connectioninfolistmodel.h + ${APP_SRC_DIR}/pttlistener.h) # For libavutil/avframe. set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib") @@ -494,6 +496,29 @@ else() OPTIONAL_COMPONENTS LinguistTools) endif() +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + if (DEFINED ENV{XDG_SESSION_TYPE}) + if ($ENV{XDG_SESSION_TYPE} STREQUAL "x11") + set(PTT_PLATFORM "x11") + list(APPEND COMMON_HEADER ${APP_SRC_DIR}/platform/X11/xcbkeyboard.H) + # TODO: add Wayland support + endif () + endif () +elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(PTT_PLATFORM "windows") +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(PTT_PLATFORM "macos") +endif () + +if (NOT ${PTT_PLATFORM} STREQUAL "") + message(STATUS "Platform: ${PTT_PLATFORM}") + add_definitions(-DHAVE_GLOBAL_PTT) + list(APPEND COMMON_SOURCES ${APP_SRC_DIR}/platform/${PTT_PLATFORM}/pttlistener.cpp) +else () + message(WARNING "Global push-to-talk not supported.") + list(APPEND COMMON_SOURCES ${APP_SRC_DIR}/platform/local/pttlistener.cpp) +endif () + # common includes include_directories( ${PROJECT_SOURCE_DIR} @@ -594,7 +619,6 @@ elseif (NOT APPLE) ${GLIB_LIBRARIES} ${GIO_LIBRARIES}) - # Installation rules install( TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) @@ -725,7 +749,7 @@ else() list(APPEND CLIENT_LIBS "-framework AVFoundation" "-framework CoreAudio -framework CoreMedia -framework CoreVideo" - "-framework VideoToolbox -framework AudioUnit" + "-framework VideoToolbox -framework AudioUnit -framework Carbon" "-framework Security" compression resolv diff --git a/resources/Info.plist b/resources/Info.plist index 198d8bb1b..65c8c8ad5 100644 --- a/resources/Info.plist +++ b/resources/Info.plist @@ -36,5 +36,7 @@ <string>Jami requires to access your microphone to make calls and record audio</string> <key>ITSAppUsesNonExemptEncryption</key> <true/> + <key>NSAppleEventsUsageDescription</key> + <string>Jami requires to monitor global key events for push-to-talk functionality.</string> </dict> </plist> diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h index 0473a3dd6..099db20ce 100644 --- a/src/app/appsettingsmanager.h +++ b/src/app/appsettingsmanager.h @@ -66,8 +66,9 @@ extern const QString defaultDownloadPath; X(ShowSendOption, false) \ X(DonationVisibleDate, "2023-11-01 05:00") \ X(IsDonationVisible, true) \ - X(DonationEndDate, "2024-01-01 00:00") - + X(DonationEndDate, "2024-01-01 00:00") \ + X(EnablePtt, false) \ + X(pttKey, 36) /* * A class to expose settings keys in both c++ and QML. * Note: this is using a non-constructable class instead of a diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp index 8ed634504..0a6e91520 100644 --- a/src/app/calladapter.cpp +++ b/src/app/calladapter.cpp @@ -7,6 +7,7 @@ * Author: Isa Nanic <isa.nanic@savoirfairelinux.com> * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * Author: Capucine Berthet <capucine.berthet@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -47,7 +48,7 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &CallAdapter::updateAdvancedInformation); - overlayModel_.reset(new CallOverlayModel(lrcInstance_, this)); + overlayModel_.reset(new CallOverlayModel(lrcInstance_, listener_, this)); QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, overlayModel_.get(), "CallOverlayModel"); accountId_ = lrcInstance_->get_currentAccountId(); @@ -97,6 +98,65 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject* &LRCInstance::selectedConvUidChanged, this, &CallAdapter::saveConferenceSubcalls); + +#ifdef HAVE_GLOBAL_PTT + connectPtt(); +#endif +} + +CallAdapter::~CallAdapter() +{ +#ifdef HAVE_GLOBAL_PTT + disconnectPtt(); +#endif +} + +void +CallAdapter::connectPtt() +{ +#ifdef HAVE_GLOBAL_PTT + if (listener_->getPttState()) { + QObject::connect( + listener_, + &PTTListener::pttKeyPressed, + this, + [this]() { + const auto callId + = lrcInstance_->getCallIdForConversationUid(lrcInstance_->get_selectedConvUid(), + accountId_); + try { + isMicrophoneMuted_ = isMuted(callId); + if (isMicrophoneMuted_) + muteAudioToggle(); + } catch (const std::exception& e) { + qWarning() << e.what(); + } + }, + Qt::QueuedConnection); + + QObject::connect( + listener_, + &PTTListener::pttKeyReleased, + this, + [this]() { + if (isMicrophoneMuted_) { + muteAudioToggle(); + } + }, + Qt::QueuedConnection); + } +#endif +} + +void +CallAdapter::disconnectPtt() +{ +#ifdef HAVE_GLOBAL_PTT + if (listener_->getPttState()) { + QObject::disconnect(listener_, &PTTListener::pttKeyPressed, this, nullptr); + QObject::disconnect(listener_, &PTTListener::pttKeyReleased, this, nullptr); + } +#endif } void @@ -172,6 +232,12 @@ CallAdapter::onCallStarted(const QString& callId) // update call Information list by adding the new information related to the callId callInformationListModel_->addElement( qMakePair(callId, callModel->advancedInformationForCallId(callId))); + if (listener_->getPttState()){ +#ifdef HAVE_GLOBAL_PTT + listener_->startListening(); + toMute += callId; +#endif + } } void @@ -181,6 +247,10 @@ CallAdapter::onCallEnded(const QString& callId) return; // update call Information list by removing information related to the callId callInformationListModel_->removeElement(callId); +#ifdef HAVE_GLOBAL_PTT + if (listener_->getPttState() && !hasCall_) + listener_->stopListening(); +#endif } void @@ -271,6 +341,15 @@ CallAdapter::onCallStatusChanged(const QString& callId, int code) } } +void +CallAdapter::onCallInfosChanged(const QString& accountId, const QString& callId) +{ + Q_UNUSED(accountId) + auto mute = toMute.remove(callId); + if (mute && listener_->getPttState()) + muteAudioToggle(); +} + void CallAdapter::onCallAddedToConference(const QString& callId, const QString& confId) { @@ -494,6 +573,12 @@ CallAdapter::connectCallModel(const QString& accountId) QOverload<const QString&, int>::of(&CallAdapter::onCallStatusChanged), Qt::UniqueConnection); + connect(accInfo.callModel.get(), + &CallModel::callInfosChanged, + this, + &CallAdapter::onCallInfosChanged, + Qt::UniqueConnection); + connect(accInfo.callModel.get(), &CallModel::callAddedToConference, this, @@ -816,6 +901,23 @@ CallAdapter::holdThisCallToggle() } } +bool +CallAdapter::isMuted(const QString& callId) +{ + if (!(callId.isEmpty() || !lrcInstance_->getCurrentCallModel()->hasCall(callId))) { + auto* callModel = lrcInstance_->getCurrentCallModel(); + if (callModel->hasCall(callId)) { + const auto callInfo = lrcInstance_->getCurrentCallModel()->getCall(callId); + auto mute = false; + for (const auto& m : callInfo.mediaList) + if (m[libjami::Media::MediaAttributeKey::LABEL] == "audio_0") + mute = m[libjami::Media::MediaAttributeKey::MUTED] == TRUE_STR; + return mute; + } + } + throw std::runtime_error("CallAdapter::isMuted: callId is empty or call does not exist"); +} + void CallAdapter::muteAudioToggle() { @@ -825,13 +927,10 @@ CallAdapter::muteAudioToggle() return; } auto* callModel = lrcInstance_->getCurrentCallModel(); - if (callModel->hasCall(callId)) { - const auto callInfo = lrcInstance_->getCurrentCallModel()->getCall(callId); - auto mute = false; - for (const auto& m : callInfo.mediaList) - if (m[libjami::Media::MediaAttributeKey::LABEL] == "audio_0") - mute = m[libjami::Media::MediaAttributeKey::MUTED] == FALSE_STR; - callModel->muteMedia(callId, "audio_0", mute); + try { + callModel->muteMedia(callId, "audio_0", !isMuted(callId)); + } catch (const std::exception& e) { + qWarning() << e.what(); } } diff --git a/src/app/calladapter.h b/src/app/calladapter.h index 1b16186d2..5086e7e84 100644 --- a/src/app/calladapter.h +++ b/src/app/calladapter.h @@ -25,6 +25,10 @@ #include "screensaver.h" #include "calloverlaymodel.h" +#ifdef HAVE_GLOBAL_PTT +#include "pttlistener.h" +#endif + #include <QObject> #include <QString> #include <QVariant> @@ -46,7 +50,7 @@ public: Q_ENUM(MuteStates) explicit CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject* parent = nullptr); - ~CallAdapter() = default; + ~CallAdapter(); public: Q_INVOKABLE void startTimerInformation(); @@ -76,6 +80,9 @@ public: Q_INVOKABLE void holdThisCallToggle(); Q_INVOKABLE void recordThisCallToggle(); Q_INVOKABLE void muteAudioToggle(); + Q_INVOKABLE bool isMuted(const QString& callId); + Q_INVOKABLE void connectPtt(); + Q_INVOKABLE void disconnectPtt(); Q_INVOKABLE void muteCameraToggle(); Q_INVOKABLE bool isRecordingThisCall(); Q_INVOKABLE void muteParticipant(const QString& accountUri, @@ -109,6 +116,7 @@ public Q_SLOTS: void onCallAddedToConference(const QString& callId, const QString& confId); void onCallStarted(const QString& callId); void onCallEnded(const QString& callId); + void onCallInfosChanged(const QString& accountId, const QString& callId); private: void showNotification(const QString& accountId, const QString& convUid); @@ -121,6 +129,9 @@ private: SystemTray* systemTray_; QScopedPointer<CallOverlayModel> overlayModel_; VectorString currentConfSubcalls_; - std::unique_ptr<CallInformationListModel> callInformationListModel_; + + PTTListener* listener_ = new PTTListener(systemTray_->getSettingsManager()); + bool isMicrophoneMuted_ = true; + QSet<QString> toMute; }; diff --git a/src/app/calloverlaymodel.cpp b/src/app/calloverlaymodel.cpp index a15496eca..8bfd66a39 100644 --- a/src/app/calloverlaymodel.cpp +++ b/src/app/calloverlaymodel.cpp @@ -22,6 +22,7 @@ #include <QEvent> #include <QMouseEvent> #include <QQuickWindow> +#include <QKeyEvent> IndexRangeFilterProxyModel::IndexRangeFilterProxyModel(QAbstractListModel* parent) : QSortFilterProxyModel(parent) @@ -74,10 +75,10 @@ PendingConferenceesListModel::data(const QModelIndex& index, int role) const using namespace PendingConferences; // WARNING: not swarm ready + lrc::api::call::Status callStatus; QString pendingConferenceeCallId; QString pendingConferenceeContactUri; ContactModel* contactModel {nullptr}; - lrc::api::call::Status callStatus; try { auto callModel = lrcInstance_->getCurrentCallModel(); auto currentPendingConferenceeInfo = callModel->getPendingConferencees().at(index.row()); @@ -268,7 +269,7 @@ CallControlListModel::clearData() data_.clear(); } -CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent) +CallOverlayModel::CallOverlayModel(LRCInstance* instance, PTTListener* listener, QObject* parent) : QObject(parent) , lrcInstance_(instance) , primaryModel_(new CallControlListModel(this)) @@ -283,6 +284,10 @@ CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent) this, &CallOverlayModel::setControlRanges); overflowVisibleModel_->setFilterRole(CallControl::Role::UrgentCount); + +#ifndef HAVE_GLOBAL_PTT + listener_ = listener; +#endif } void @@ -386,6 +391,19 @@ CallOverlayModel::eventFilter(QObject* object, QEvent* event) } } } +#ifndef HAVE_GLOBAL_PTT + else if (event->type() == QEvent::KeyPress && listener_->getPttState()) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == listener_->getCurrentKey() && !keyEvent->isAutoRepeat()) { + Q_EMIT pttKeyPressed(); + } + } else if (event->type() == QEvent::KeyRelease && listener_->getPttState()) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == listener_->getCurrentKey() && !keyEvent->isAutoRepeat()) { + Q_EMIT pttKeyReleased(); + } + } +#endif return QObject::eventFilter(object, event); } diff --git a/src/app/calloverlaymodel.h b/src/app/calloverlaymodel.h index 2cbb7d074..2c2e81951 100644 --- a/src/app/calloverlaymodel.h +++ b/src/app/calloverlaymodel.h @@ -21,6 +21,9 @@ #include "lrcinstance.h" #include "qtutils.h" +#include "mainapplication.h" + +#include "pttlistener.h" #include <QAbstractListModel> #include <QObject> @@ -36,7 +39,7 @@ namespace CallControl { Q_NAMESPACE -enum Role { ItemAction = Qt::UserRole + 1, UrgentCount, Enabled}; +enum Role { ItemAction = Qt::UserRole + 1, UrgentCount, Enabled }; Q_ENUM_NS(Role) struct Item @@ -121,7 +124,7 @@ class CallOverlayModel : public QObject QML_PROPERTY(int, overflowIndex) public: - CallOverlayModel(LRCInstance* instance, QObject* parent = nullptr); + CallOverlayModel(LRCInstance* instance, PTTListener* listener, QObject* parent = nullptr); Q_INVOKABLE void addPrimaryControl(const QVariant& action, bool enabled); Q_INVOKABLE void addSecondaryControl(const QVariant& action, bool enabled); @@ -142,6 +145,8 @@ public: Q_SIGNALS: void mouseMoved(QQuickItem* item); + void pttKeyPressed(); + void pttKeyReleased(); private Q_SLOTS: void setControlRanges(); @@ -157,4 +162,8 @@ private: PendingConferenceesListModel* pendingConferenceesModel_; QList<QQuickItem*> watchedItems_; + +#ifndef HAVE_GLOBAL_PTT + PTTListener* listener_ {nullptr}; +#endif }; diff --git a/src/app/commoncomponents/ChangePttKeyPopup.qml b/src/app/commoncomponents/ChangePttKeyPopup.qml new file mode 100644 index 000000000..90078407a --- /dev/null +++ b/src/app/commoncomponents/ChangePttKeyPopup.qml @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +BaseModalDialog { + id: pttPage + + property string bestName: "" + property string accountId: "" + property int pressedKey: Qt.Key_unknown + + signal accepted + signal choiceMade(int chosenKey) + + title: JamiStrings.changeShortcut + + popupContent: ColumnLayout { + id: deleteAccountContentColumnLayout + anchors.centerIn: parent + spacing: JamiTheme.preferredMarginSize + + Component.onCompleted: keyItem.forceActiveFocus() + Label { + id: instructionLabel + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: JamiTheme.preferredDialogWidth - 4*JamiTheme.preferredMarginSize + color: JamiTheme.textColor + text: JamiStrings.assignmentIndication + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + font.pointSize: JamiTheme.textFontSize + font.kerning: true + + wrapMode: Text.Wrap + } + + Label { + id: keyLabel + Layout.alignment: Qt.AlignCenter + + color: JamiTheme.blackColor + wrapMode: Text.WordWrap + text: "" + font.pointSize: JamiTheme.settingsFontSize + font.kerning: true + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + background: Rectangle { + id: backgroundRect + + anchors.centerIn: parent + + width: keyLabel.width + 2 * JamiTheme.preferredMarginSize + height: keyLabel.height + JamiTheme.preferredMarginSize + color: JamiTheme.lightGrey_ + border.color: JamiTheme.darkGreyColor + radius: 4 + } + + } + + MaterialButton { + id: btnAssign + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: JamiTheme.preferredMarginSize + + preferredWidth: JamiTheme.preferredFieldWidth / 2 - 8 + buttontextHeightMargin: JamiTheme.buttontextHeightMargin + + color: JamiTheme.buttonTintedBlack + hoveredColor: JamiTheme.buttonTintedBlackHovered + pressedColor: JamiTheme.buttonTintedBlackPressed + secondary: true + + text: JamiStrings.assign + autoAccelerator: true + + onClicked: { + if (!(pressedKey === Qt.Key_unknown)){ + pttListener.setPttKey(pressedKey); + choiceMade(pressedKey); + } + close(); + } + } + + Item { + id: keyItem + + Keys.onPressed: (event)=>{ + keyLabel.text = pttListener.keyToString(event.key); + pressedKey = event.key; + } + } + } + + +} diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 40d301585..dd4879a0c 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -83,6 +83,15 @@ Item { property string selectNewRingtone: qsTr("Select a new ringtone") property string certificateFile: qsTr("Certificate File (*.crt)") property string audioFile: qsTr("Audio File (*.wav *.ogg *.opus *.mp3 *.aiff *.wma)") + property string pushToTalk: qsTr("Push-to-talk") + property string enablePTT: qsTr("Enable push-to-talk") + property string keyboardShortcut: qsTr("Keyboard shortcut") + property string changeKeyboardShortcut: qsTr("Change keyboard shortcut") + + // ChangePttKeyPopup + property string changeShortcut: qsTr("Change shortcut") + property string assignmentIndication: qsTr("Press the key to be assigned to push-to-talk shortcut") + property string assign: qsTr("Assign") // AdvancedChatSettings property string enableReadReceipts: qsTr("Enable read receipts") diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp index 29c98343e..5f5c485be 100644 --- a/src/app/mainapplication.cpp +++ b/src/app/mainapplication.cpp @@ -20,6 +20,7 @@ */ #include "mainapplication.h" +#include "pttlistener.h" #include "qmlregister.h" #include "appsettingsmanager.h" @@ -132,6 +133,7 @@ MainApplication::init() connectivityMonitor_.reset(new ConnectivityMonitor(this)); settingsManager_.reset(new AppSettingsManager(this)); systemTray_.reset(new SystemTray(settingsManager_.get(), this)); + listener_ = new PTTListener(settingsManager_.get(), this); QObject::connect(settingsManager_.get(), &AppSettingsManager::retranslate, @@ -350,6 +352,7 @@ MainApplication::initQmlLayer() auto videoProvider = new VideoProvider(lrcInstance_->avModel(), this); engine_->rootContext()->setContextProperty("videoProvider", videoProvider); + engine_->rootContext()->setContextProperty("pttListener", listener_); engine_->load(QUrl(QStringLiteral("qrc:/MainApplicationWindow.qml"))); qWarning().noquote() << "Main window loaded using" << getRenderInterfaceString(); @@ -409,10 +412,9 @@ MainApplication::cleanup() } } -#ifdef Q_OS_MACOS void MainApplication::setEventFilter() { installEventFilter(this); } -#endif + diff --git a/src/app/mainapplication.h b/src/app/mainapplication.h index 3fabd8e5b..41159fc52 100644 --- a/src/app/mainapplication.h +++ b/src/app/mainapplication.h @@ -23,6 +23,7 @@ #include "imagedownloader.h" #include "lrcinstance.h" #include "qtutils.h" +#include "pttlistener.h" #include <QFile> #include <QApplication> @@ -82,17 +83,20 @@ public: return runOptions_[opt]; }; -#ifdef Q_OS_MACOS Q_INVOKABLE void setEventFilter(); bool eventFilter(QObject* object, QEvent* event) { +#ifdef Q_OS_MACOS + if (event->type() == QEvent::ApplicationActivate) { restoreApp(); } + +#endif // Q_OS_MACOS + return QApplication::eventFilter(object, event); } -#endif // Q_OS_MACOS Q_SIGNALS: void closeRequested(); @@ -120,6 +124,8 @@ private: QScopedPointer<SystemTray> systemTray_; QScopedPointer<ImageDownloader> imageDownloader_; + PTTListener* listener_; + ScreenInfo screenInfo_; bool isCleanupped; diff --git a/src/app/mainview/components/CallStackView.qml b/src/app/mainview/components/CallStackView.qml index fd38f5fe1..9fa6a36d2 100644 --- a/src/app/mainview/components/CallStackView.qml +++ b/src/app/mainview/components/CallStackView.qml @@ -25,7 +25,6 @@ import "../../commoncomponents" Item { id: root - property alias chatViewContainer: ongoingCallPage.chatViewContainer property alias contentView: callStackMainView @@ -49,6 +48,16 @@ Item { } } + Connections { + target: CallOverlayModel + function onPttKeyPressed() { + CallAdapter.muteAudioToggle(); + } + function onPttKeyReleased() { + CallAdapter.muteAudioToggle(); + } + } + // TODO: this should all be done by listening to // parent visibility change or parent `Component.onDestruction` function needToCloseInCallConversationAndPotentialWindow() { diff --git a/src/app/platform/local/pttlistener.cpp b/src/app/platform/local/pttlistener.cpp new file mode 100644 index 000000000..6a8db7c6f --- /dev/null +++ b/src/app/platform/local/pttlistener.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "pttlistener.h" + +#include <QCoreApplication> +#include <QVariant> + +class PTTListener::Impl : public QObject +{ + Q_OBJECT +public: + Impl(PTTListener* parent) + : QObject(parent) + {} + + ~Impl() = default; +}; + +PTTListener::PTTListener(AppSettingsManager* settingsManager, QObject* parent) + : settingsManager_(settingsManager) + , QObject(parent) + , pimpl_(std::make_unique<Impl>(this)) +{} + +PTTListener::~PTTListener() = default; + +#include "pttlistener.moc" diff --git a/src/app/platform/macos/pttlistener.cpp b/src/app/platform/macos/pttlistener.cpp new file mode 100644 index 000000000..2fd251001 --- /dev/null +++ b/src/app/platform/macos/pttlistener.cpp @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <ApplicationServices/ApplicationServices.h> +#include <Carbon/Carbon.h> + +#include "pttlistener.h" + +#include <QCoreApplication> +#include <QVariant> + +class PTTListener::Impl : public QObject +{ + Q_OBJECT +public: + Impl(PTTListener* parent) + : QObject(parent) + { + qApp->setProperty("PTTListener", QVariant::fromValue(parent)); + } + + ~Impl() + { + stopListening(); + }; + + void startListening() + { + CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp); + CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionDefault, + eventMask, + CGEventCallback, + this); + + if (eventTap) { + CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, + eventTap, + 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopDefaultMode); + CFRelease(runLoopSource); + + CGEventTapEnable(eventTap, true); + } else { + qDebug() << "Impossible to create the keyboard tap."; + } + } + + void stopListening() + { + if (eventTap) { + CGEventTapEnable(eventTap, false); + CFRelease(eventTap); + } + } + + static CGEventRef CGEventCallback(CGEventTapProxy proxy, + CGEventType type, + CGEventRef event, + void* refcon) + { + auto* pThis = qApp->property("PTTListener").value<PTTListener*>(); + CGKeyCode keycode = (CGKeyCode) CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); + if (pThis == nullptr) { + qWarning() << "PTTListener not found"; + return {}; + } + CGKeyCode pttKey = (CGKeyCode) pThis->pimpl_->qtKeyTokVKey(pThis->getCurrentKey()); + static bool isKeyDown = false; + if (keycode == pttKey) { + if (type == kCGEventKeyDown && !isKeyDown) { + Q_EMIT pThis->pttKeyPressed(); + isKeyDown = true; + } else if (type == kCGEventKeyUp && isKeyDown) { + Q_EMIT pThis->pttKeyReleased(); + isKeyDown = false; + } + } + return event; + } + + quint32 qtKeyTokVKey(Qt::Key key); + +private: + CFMachPortRef eventTap; +}; + +PTTListener::PTTListener(AppSettingsManager* settingsManager, QObject* parent) + : settingsManager_(settingsManager) + , QObject(parent) + , pimpl_(std::make_unique<Impl>(this)) +{} + +PTTListener::~PTTListener() = default; + +void +PTTListener::startListening() +{ + pimpl_->startListening(); +} + +void +PTTListener::stopListening() +{ + pimpl_->stopListening(); +} + +quint32 +PTTListener::Impl::qtKeyTokVKey(Qt::Key key) +{ + UTF16Char ch; + // Constants found in NSEvent.h from AppKit.framework + switch (key) { + case Qt::Key_Return: + return kVK_Return; + case Qt::Key_Enter: + return kVK_ANSI_KeypadEnter; + case Qt::Key_Tab: + return kVK_Tab; + case Qt::Key_Space: + return kVK_Space; + case Qt::Key_Backspace: + return kVK_Delete; + case Qt::Key_Control: + return kVK_Command; + case Qt::Key_Shift: + return kVK_Shift; + case Qt::Key_CapsLock: + return kVK_CapsLock; + case Qt::Key_Option: + return kVK_Option; + case Qt::Key_Meta: + return kVK_Control; + case Qt::Key_F17: + return kVK_F17; + case Qt::Key_VolumeUp: + return kVK_VolumeUp; + case Qt::Key_VolumeDown: + return kVK_VolumeDown; + case Qt::Key_F18: + return kVK_F18; + case Qt::Key_F19: + return kVK_F19; + case Qt::Key_F20: + return kVK_F20; + case Qt::Key_F5: + return kVK_F5; + case Qt::Key_F6: + return kVK_F6; + case Qt::Key_F7: + return kVK_F7; + case Qt::Key_F3: + return kVK_F3; + case Qt::Key_F8: + return kVK_F8; + case Qt::Key_F9: + return kVK_F9; + case Qt::Key_F11: + return kVK_F11; + case Qt::Key_F13: + return kVK_F13; + case Qt::Key_F16: + return kVK_F16; + case Qt::Key_F14: + return kVK_F14; + case Qt::Key_F10: + return kVK_F10; + case Qt::Key_F12: + return kVK_F12; + case Qt::Key_F15: + return kVK_F15; + case Qt::Key_Help: + return kVK_Help; + case Qt::Key_Home: + return kVK_Home; + case Qt::Key_PageUp: + return kVK_PageUp; + case Qt::Key_Delete: + return kVK_ForwardDelete; + case Qt::Key_F4: + return kVK_F4; + case Qt::Key_End: + return kVK_End; + case Qt::Key_F2: + return kVK_F2; + case Qt::Key_PageDown: + return kVK_PageDown; + case Qt::Key_F1: + return kVK_F1; + case Qt::Key_Left: + return kVK_LeftArrow; + case Qt::Key_Right: + return kVK_RightArrow; + case Qt::Key_Down: + return kVK_DownArrow; + case Qt::Key_Up: + return kVK_UpArrow; + default:; + } + + if (key == Qt::Key_Escape) + ch = 27; + else if (key == Qt::Key_Return) + ch = 13; + else if (key == Qt::Key_Enter) + ch = 3; + else if (key == Qt::Key_Tab) + ch = 9; + else + ch = key; + + CFDataRef currentLayoutData; + TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource(); + + if (currentKeyboard == nullptr) + return 0; + + currentLayoutData = (CFDataRef) TISGetInputSourceProperty(currentKeyboard, + kTISPropertyUnicodeKeyLayoutData); + CFRelease(currentKeyboard); + if (currentLayoutData == nullptr) + return 0; + + UCKeyboardLayout* header = (UCKeyboardLayout*) CFDataGetBytePtr(currentLayoutData); + UCKeyboardTypeHeader* table = header->keyboardTypeList; + + uint8_t* data = (uint8_t*) header; + // God, would a little documentation for this shit kill you... + for (quint32 i = 0; i < header->keyboardTypeCount; i++) { + UCKeyStateRecordsIndex* stateRec = 0; + if (table[i].keyStateRecordsIndexOffset != 0) { + stateRec = reinterpret_cast<UCKeyStateRecordsIndex*>( + data + table[i].keyStateRecordsIndexOffset); + if (stateRec->keyStateRecordsIndexFormat != kUCKeyStateRecordsIndexFormat) + stateRec = 0; + } + + UCKeyToCharTableIndex* charTable = reinterpret_cast<UCKeyToCharTableIndex*>( + data + table[i].keyToCharTableIndexOffset); + if (charTable->keyToCharTableIndexFormat != kUCKeyToCharTableIndexFormat) + continue; + + for (quint32 j = 0; j < charTable->keyToCharTableCount; j++) { + UCKeyOutput* keyToChar = reinterpret_cast<UCKeyOutput*>( + data + charTable->keyToCharTableOffsets[j]); + for (quint32 k = 0; k < charTable->keyToCharTableSize; k++) { + if (keyToChar[k] & kUCKeyOutputTestForIndexMask) { + long idx = keyToChar[k] & kUCKeyOutputGetIndexMask; + if (stateRec && idx < stateRec->keyStateRecordCount) { + UCKeyStateRecord* rec = reinterpret_cast<UCKeyStateRecord*>( + data + stateRec->keyStateRecordOffsets[idx]); + if (rec->stateZeroCharData == ch) + return k; + } + } else if (!(keyToChar[k] & kUCKeyOutputSequenceIndexMask) + && keyToChar[k] < 0xFFFE) { + if (keyToChar[k] == ch) + return k; + } + } // for k + } // for j + } // for i + + // The code above fails to translate keys like semicolon with Qt 5.7.1. + // Last resort is to try mapping the rest of the keys directly. + switch (key) { + case Qt::Key_A: + return kVK_ANSI_A; + case Qt::Key_S: + return kVK_ANSI_S; + case Qt::Key_D: + return kVK_ANSI_D; + case Qt::Key_F: + return kVK_ANSI_F; + case Qt::Key_H: + return kVK_ANSI_H; + case Qt::Key_G: + return kVK_ANSI_G; + case Qt::Key_Z: + return kVK_ANSI_Z; + case Qt::Key_X: + return kVK_ANSI_X; + case Qt::Key_C: + return kVK_ANSI_C; + case Qt::Key_V: + return kVK_ANSI_V; + case Qt::Key_B: + return kVK_ANSI_B; + case Qt::Key_Q: + return kVK_ANSI_Q; + case Qt::Key_W: + return kVK_ANSI_W; + case Qt::Key_E: + return kVK_ANSI_E; + case Qt::Key_R: + return kVK_ANSI_R; + case Qt::Key_Y: + return kVK_ANSI_Y; + case Qt::Key_T: + return kVK_ANSI_T; + case Qt::Key_1: + return kVK_ANSI_1; + case Qt::Key_2: + return kVK_ANSI_2; + case Qt::Key_3: + return kVK_ANSI_3; + case Qt::Key_4: + return kVK_ANSI_4; + case Qt::Key_6: + return kVK_ANSI_6; + case Qt::Key_5: + return kVK_ANSI_5; + case Qt::Key_Equal: + return kVK_ANSI_Equal; + case Qt::Key_9: + return kVK_ANSI_9; + case Qt::Key_7: + return kVK_ANSI_7; + case Qt::Key_Minus: + return kVK_ANSI_Minus; + case Qt::Key_8: + return kVK_ANSI_8; + case Qt::Key_0: + return kVK_ANSI_0; + case Qt::Key_BracketRight: + return kVK_ANSI_RightBracket; + case Qt::Key_O: + return kVK_ANSI_O; + case Qt::Key_U: + return kVK_ANSI_U; + case Qt::Key_BracketLeft: + return kVK_ANSI_LeftBracket; + case Qt::Key_I: + return kVK_ANSI_I; + case Qt::Key_P: + return kVK_ANSI_P; + case Qt::Key_L: + return kVK_ANSI_L; + case Qt::Key_J: + return kVK_ANSI_J; + case Qt::Key_QuoteDbl: + return kVK_ANSI_Quote; + case Qt::Key_K: + return kVK_ANSI_K; + case Qt::Key_Semicolon: + return kVK_ANSI_Semicolon; + case Qt::Key_Backslash: + return kVK_ANSI_Backslash; + case Qt::Key_Comma: + return kVK_ANSI_Comma; + case Qt::Key_Slash: + return kVK_ANSI_Slash; + case Qt::Key_N: + return kVK_ANSI_N; + case Qt::Key_M: + return kVK_ANSI_M; + case Qt::Key_Period: + return kVK_ANSI_Period; + case Qt::Key_Dead_Grave: + return kVK_ANSI_Grave; + case Qt::Key_Asterisk: + return kVK_ANSI_KeypadMultiply; + case Qt::Key_Plus: + return kVK_ANSI_KeypadPlus; + case Qt::Key_Clear: + return kVK_ANSI_KeypadClear; + case Qt::Key_Escape: + return kVK_Escape; + default:; + } + + return 0; +} + +#include "pttlistener.moc" diff --git a/src/app/platform/windows/pttlistener.cpp b/src/app/platform/windows/pttlistener.cpp new file mode 100644 index 000000000..20c5b77cb --- /dev/null +++ b/src/app/platform/windows/pttlistener.cpp @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "pttlistener.h" + +#include <QCoreApplication> +#include <QVariant> + +#include <Windows.h> + +class PTTListener::Impl : public QObject +{ + Q_OBJECT +public: + Impl(PTTListener* parent) + : QObject(nullptr) + { + qApp->setProperty("PTTListener", QVariant::fromValue(parent)); + } + + ~Impl() + { + stopListening(); + }; + + void startListening() + { + keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, GlobalKeyboardProc, NULL, 0); + } + + void stopListening() + { + UnhookWindowsHookEx(keyboardHook); + } + + static LRESULT CALLBACK GlobalKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) + { + auto* pThis = qApp->property("PTTListener").value<PTTListener*>(); + if (pThis == nullptr) { + qWarning() << "PTTListener not found"; + return {}; + } + auto* keyboardHook = pThis->pimpl_->keyboardHook; + + quint32 key = qtKeyToVKey(pThis->getCurrentKey()); + static bool isKeyDown = false; + if (nCode == HC_ACTION) { + if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) { + KBDLLHOOKSTRUCT* keyInfo = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam); + if (keyInfo->vkCode == key && !isKeyDown) { + Q_EMIT pThis->pttKeyPressed(); + isKeyDown = true; + } + } else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) { + KBDLLHOOKSTRUCT* keyInfo = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam); + if (keyInfo->vkCode == key) { + Q_EMIT pThis->pttKeyReleased(); + isKeyDown = false; + } + } + } + + return CallNextHookEx(keyboardHook, nCode, wParam, lParam); + } + + HHOOK keyboardHook; + + static quint32 qtKeyToVKey(Qt::Key key); + +}; + +PTTListener::PTTListener(AppSettingsManager* settingsManager, QObject* parent) + : settingsManager_(settingsManager) + , QObject(parent) + , pimpl_(std::make_unique<Impl>(this)) +{} + +PTTListener::~PTTListener() = default; + +#ifdef HAVE_GLOBAL_PTT +void +PTTListener::startListening() +{ + pimpl_->startListening(); +} + +void +PTTListener::stopListening() +{ + pimpl_->stopListening(); +} +#endif + +quint32 +PTTListener::Impl::qtKeyToVKey(Qt::Key key) +{ + switch (key) { + case Qt::Key_Escape: + return VK_ESCAPE; + case Qt::Key_Tab: + case Qt::Key_Backtab: + return VK_TAB; + case Qt::Key_Backspace: + return VK_BACK; + case Qt::Key_Return: + case Qt::Key_Enter: + return VK_RETURN; + case Qt::Key_Insert: + return VK_INSERT; + case Qt::Key_Delete: + return VK_DELETE; + case Qt::Key_Pause: + return VK_PAUSE; + case Qt::Key_Print: + return VK_PRINT; + case Qt::Key_Clear: + return VK_CLEAR; + case Qt::Key_Home: + return VK_HOME; + case Qt::Key_End: + return VK_END; + case Qt::Key_Left: + return VK_LEFT; + case Qt::Key_Up: + return VK_UP; + case Qt::Key_Right: + return VK_RIGHT; + case Qt::Key_Down: + return VK_DOWN; + case Qt::Key_PageUp: + return VK_PRIOR; + case Qt::Key_PageDown: + return VK_NEXT; + case Qt::Key_F1: + return VK_F1; + case Qt::Key_F2: + return VK_F2; + case Qt::Key_F3: + return VK_F3; + case Qt::Key_F4: + return VK_F4; + case Qt::Key_F5: + return VK_F5; + case Qt::Key_F6: + return VK_F6; + case Qt::Key_F7: + return VK_F7; + case Qt::Key_F8: + return VK_F8; + case Qt::Key_F9: + return VK_F9; + case Qt::Key_F10: + return VK_F10; + case Qt::Key_F11: + return VK_F11; + case Qt::Key_F12: + return VK_F12; + case Qt::Key_F13: + return VK_F13; + case Qt::Key_F14: + return VK_F14; + case Qt::Key_F15: + return VK_F15; + case Qt::Key_F16: + return VK_F16; + case Qt::Key_F17: + return VK_F17; + case Qt::Key_F18: + return VK_F18; + case Qt::Key_F19: + return VK_F19; + case Qt::Key_F20: + return VK_F20; + case Qt::Key_F21: + return VK_F21; + case Qt::Key_F22: + return VK_F22; + case Qt::Key_F23: + return VK_F23; + case Qt::Key_F24: + return VK_F24; + case Qt::Key_Space: + return VK_SPACE; + case Qt::Key_Asterisk: + return VK_MULTIPLY; + case Qt::Key_Plus: + return VK_ADD; + case Qt::Key_Minus: + return VK_SUBTRACT; + case Qt::Key_Slash: + return VK_DIVIDE; + case Qt::Key_MediaNext: + return VK_MEDIA_NEXT_TRACK; + case Qt::Key_MediaPrevious: + return VK_MEDIA_PREV_TRACK; + case Qt::Key_MediaPlay: + return VK_MEDIA_PLAY_PAUSE; + case Qt::Key_MediaStop: + return VK_MEDIA_STOP; + // couldn't find those in VK_* + // case Qt::Key_MediaLast: + // case Qt::Key_MediaRecord: + case Qt::Key_VolumeDown: + return VK_VOLUME_DOWN; + case Qt::Key_VolumeUp: + return VK_VOLUME_UP; + case Qt::Key_VolumeMute: + return VK_VOLUME_MUTE; + case Qt::Key_0: + return VK_NUMPAD0; + case Qt::Key_1: + return VK_NUMPAD1; + case Qt::Key_2: + return VK_NUMPAD2; + case Qt::Key_3: + return VK_NUMPAD3; + case Qt::Key_4: + return VK_NUMPAD4; + case Qt::Key_5: + return VK_NUMPAD5; + case Qt::Key_6: + return VK_NUMPAD6; + case Qt::Key_7: + return VK_NUMPAD7; + case Qt::Key_8: + return VK_NUMPAD8; + case Qt::Key_9: + return VK_NUMPAD9; + case Qt::Key_A: + return 'A'; + case Qt::Key_B: + return 'B'; + case Qt::Key_C: + return 'C'; + case Qt::Key_D: + return 'D'; + case Qt::Key_E: + return 'E'; + case Qt::Key_F: + return 'F'; + case Qt::Key_G: + return 'G'; + case Qt::Key_H: + return 'H'; + case Qt::Key_I: + return 'I'; + case Qt::Key_J: + return 'J'; + case Qt::Key_K: + return 'K'; + case Qt::Key_L: + return 'L'; + case Qt::Key_M: + return 'M'; + case Qt::Key_N: + return 'N'; + case Qt::Key_O: + return 'O'; + case Qt::Key_P: + return 'P'; + case Qt::Key_Q: + return 'Q'; + case Qt::Key_R: + return 'R'; + case Qt::Key_S: + return 'S'; + case Qt::Key_T: + return 'T'; + case Qt::Key_U: + return 'U'; + case Qt::Key_V: + return 'V'; + case Qt::Key_W: + return 'W'; + case Qt::Key_X: + return 'X'; + case Qt::Key_Y: + return 'Y'; + case Qt::Key_Z: + return 'Z'; + + default: + //Try to get virtual key from current keyboard layout or US. + const HKL layout = GetKeyboardLayout(0); + int vk = VkKeyScanEx(key, layout); + if (vk == -1) { + const HKL layoutUs = GetKeyboardLayout(0x409); + vk = VkKeyScanEx(key, layoutUs); + } + return vk == -1 ? 0 : vk; + } +} + + +#include "pttlistener.moc" \ No newline at end of file diff --git a/src/app/platform/x11/pttlistener.cpp b/src/app/platform/x11/pttlistener.cpp new file mode 100644 index 000000000..568634824 --- /dev/null +++ b/src/app/platform/x11/pttlistener.cpp @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "pttlistener.h" +#include "appsettingsmanager.h" +#include "xcbkeyboard.h" + +#include <QCoreApplication> +#include <QVariant> + +#include <X11/X.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> + +#include <thread> + +class PTTListener::Impl : public QObject +{ + Q_OBJECT +public: + Impl(PTTListener* parent) + : QObject(nullptr) + , parent_(*parent) + , display_(XOpenDisplay(NULL)) + , root_(DefaultRootWindow(display_)) + { + thread_.reset(new QThread()); + moveToThread(thread_.get()); + } + + ~Impl() + { + stopListening(); + XCloseDisplay(display_); + }; + + void startListening() + { + stop_.store(false); + connect(thread_.get(), &QThread::started, this, &Impl::processEvents); + thread_->start(); + } + + void stopListening() + { + stop_.store(true); + thread_->quit(); + thread_->wait(); + } + + static const unsigned int* keyTbl_; + + KeySym getKeySymFromQtKey(Qt::Key qtKey); + + QString keySymToQString(KeySym ks); + + KeySym qtKeyToXKeySym(Qt::Key key); + +private Q_SLOTS: + void processEvents() + { + Window curFocus; + char buf[17]; + KeySym ks; + XComposeStatus comp; + int len; + int revert; + static auto flags = KeyPressMask | KeyReleaseMask | FocusChangeMask; + + XGetInputFocus(display_, &curFocus, &revert); + XSelectInput(display_, curFocus, flags); + bool pressed = false; + KeySym key = qtKeyToXKeySym(parent_.getCurrentKey()); + + while (!stop_.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + while (XPending(display_)) { + XEvent ev; + + XNextEvent(display_, &ev); + XLookupString(&ev.xkey, buf, 16, &ks, &comp); + switch (ev.type) { + case FocusOut: + if (curFocus != root_) + XSelectInput(display_, curFocus, 0); + XGetInputFocus(display_, &curFocus, &revert); + if (curFocus == PointerRoot) + curFocus = root_; + XSelectInput(display_, curFocus, flags); + break; + + case KeyPress: { + if (!(pressed) && ks == key) { + Q_EMIT parent_.pttKeyPressed(); + pressed = true; + } + break; + } + + case KeyRelease: + bool is_retriggered = false; + if (XEventsQueued(display_, QueuedAfterReading)) { + XEvent nev; + XPeekEvent(display_, &nev); + if (nev.type == KeyPress && nev.xkey.time == ev.xkey.time + && nev.xkey.keycode == ev.xkey.keycode) { + is_retriggered = true; + } + } + if (!is_retriggered && ks == key) { + Q_EMIT parent_.pttKeyReleased(); + pressed = false; + } + break; + } + } + } + } + +private: + PTTListener& parent_; + Display* display_; + Window root_; + QScopedPointer<QThread> thread_; + std::atomic_bool stop_ {false}; +}; + +QString +PTTListener::Impl::keySymToQString(KeySym ks) +{ + return QString::fromUtf8(XKeysymToString(ks)); +} + +KeySym +PTTListener::Impl::qtKeyToXKeySym(Qt::Key key) +{ + const auto keySym = getKeySymFromQtKey(key); + if (keySym != NoSymbol) { + return keySym; + } + for (int i = 0; keyTbl_[i] != 0; i += 2) { + if (keyTbl_[i + 1] == key) + return keyTbl_[i]; + } + + return static_cast<ushort>(key); +} + +PTTListener::PTTListener(AppSettingsManager* settingsManager, QObject* parent) + : settingsManager_(settingsManager) + , QObject(parent) + , pimpl_(std::make_unique<Impl>(this)) +{} + +PTTListener::~PTTListener() = default; + +void +PTTListener::startListening() +{ + pimpl_->startListening(); +} + +void +PTTListener::stopListening() +{ + pimpl_->stopListening(); +} +KeySym +PTTListener::Impl::getKeySymFromQtKey(Qt::Key qtKey) +{ + QString keyString = QKeySequence(qtKey).toString().toLower(); + KeySym keySym = XStringToKeysym(keyString.toUtf8().data()); + return keySym; +} + +const unsigned int* PTTListener::Impl::keyTbl_ = keyTable; + +#include "pttlistener.moc" diff --git a/src/app/platform/x11/xcbkeyboard.h b/src/app/platform/x11/xcbkeyboard.h new file mode 100644 index 000000000..db5a533f6 --- /dev/null +++ b/src/app/platform/x11/xcbkeyboard.h @@ -0,0 +1,856 @@ +#include "pttlistener.h" +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// Following definitions and table are taken from +// "qt5/qtbase/src/plugins/platforms/xcb/qxcbkeyboard.cpp". + +#include <Qt> +#include <X11/keysym.h> + +#ifndef XK_ISO_Left_Tab +#define XK_ISO_Left_Tab 0xFE20 +#endif + +#ifndef XK_dead_hook +#define XK_dead_hook 0xFE61 +#endif + +#ifndef XK_dead_horn +#define XK_dead_horn 0xFE62 +#endif + +#ifndef XK_Codeinput +#define XK_Codeinput 0xFF37 +#endif + +#ifndef XK_Kanji_Bangou +#define XK_Kanji_Bangou 0xFF37 /* same as codeinput */ +#endif + +// Fix old X libraries +#ifndef XK_KP_Home +#define XK_KP_Home 0xFF95 +#endif +#ifndef XK_KP_Left +#define XK_KP_Left 0xFF96 +#endif +#ifndef XK_KP_Up +#define XK_KP_Up 0xFF97 +#endif +#ifndef XK_KP_Right +#define XK_KP_Right 0xFF98 +#endif +#ifndef XK_KP_Down +#define XK_KP_Down 0xFF99 +#endif +#ifndef XK_KP_Prior +#define XK_KP_Prior 0xFF9A +#endif +#ifndef XK_KP_Next +#define XK_KP_Next 0xFF9B +#endif +#ifndef XK_KP_End +#define XK_KP_End 0xFF9C +#endif +#ifndef XK_KP_Insert +#define XK_KP_Insert 0xFF9E +#endif +#ifndef XK_KP_Delete +#define XK_KP_Delete 0xFF9F +#endif + +// the next lines are taken on 10/2009 from X.org (X11/XF86keysym.h), defining some special +// multimedia keys. They are included here as not every system has them. +#define XF86XK_MonBrightnessUp 0x1008FF02 +#define XF86XK_MonBrightnessDown 0x1008FF03 +#define XF86XK_KbdLightOnOff 0x1008FF04 +#define XF86XK_KbdBrightnessUp 0x1008FF05 +#define XF86XK_KbdBrightnessDown 0x1008FF06 +#define XF86XK_Standby 0x1008FF10 +#define XF86XK_AudioLowerVolume 0x1008FF11 +#define XF86XK_AudioMute 0x1008FF12 +#define XF86XK_AudioRaiseVolume 0x1008FF13 +#define XF86XK_AudioPlay 0x1008FF14 +#define XF86XK_AudioStop 0x1008FF15 +#define XF86XK_AudioPrev 0x1008FF16 +#define XF86XK_AudioNext 0x1008FF17 +#define XF86XK_HomePage 0x1008FF18 +#define XF86XK_Mail 0x1008FF19 +#define XF86XK_Start 0x1008FF1A +#define XF86XK_Search 0x1008FF1B +#define XF86XK_AudioRecord 0x1008FF1C +#define XF86XK_Calculator 0x1008FF1D +#define XF86XK_Memo 0x1008FF1E +#define XF86XK_ToDoList 0x1008FF1F +#define XF86XK_Calendar 0x1008FF20 +#define XF86XK_PowerDown 0x1008FF21 +#define XF86XK_ContrastAdjust 0x1008FF22 +#define XF86XK_Back 0x1008FF26 +#define XF86XK_Forward 0x1008FF27 +#define XF86XK_Stop 0x1008FF28 +#define XF86XK_Refresh 0x1008FF29 +#define XF86XK_PowerOff 0x1008FF2A +#define XF86XK_WakeUp 0x1008FF2B +#define XF86XK_Eject 0x1008FF2C +#define XF86XK_ScreenSaver 0x1008FF2D +#define XF86XK_WWW 0x1008FF2E +#define XF86XK_Sleep 0x1008FF2F +#define XF86XK_Favorites 0x1008FF30 +#define XF86XK_AudioPause 0x1008FF31 +#define XF86XK_AudioMedia 0x1008FF32 +#define XF86XK_MyComputer 0x1008FF33 +#define XF86XK_LightBulb 0x1008FF35 +#define XF86XK_Shop 0x1008FF36 +#define XF86XK_History 0x1008FF37 +#define XF86XK_OpenURL 0x1008FF38 +#define XF86XK_AddFavorite 0x1008FF39 +#define XF86XK_HotLinks 0x1008FF3A +#define XF86XK_BrightnessAdjust 0x1008FF3B +#define XF86XK_Finance 0x1008FF3C +#define XF86XK_Community 0x1008FF3D +#define XF86XK_AudioRewind 0x1008FF3E +#define XF86XK_BackForward 0x1008FF3F +#define XF86XK_Launch0 0x1008FF40 +#define XF86XK_Launch1 0x1008FF41 +#define XF86XK_Launch2 0x1008FF42 +#define XF86XK_Launch3 0x1008FF43 +#define XF86XK_Launch4 0x1008FF44 +#define XF86XK_Launch5 0x1008FF45 +#define XF86XK_Launch6 0x1008FF46 +#define XF86XK_Launch7 0x1008FF47 +#define XF86XK_Launch8 0x1008FF48 +#define XF86XK_Launch9 0x1008FF49 +#define XF86XK_LaunchA 0x1008FF4A +#define XF86XK_LaunchB 0x1008FF4B +#define XF86XK_LaunchC 0x1008FF4C +#define XF86XK_LaunchD 0x1008FF4D +#define XF86XK_LaunchE 0x1008FF4E +#define XF86XK_LaunchF 0x1008FF4F +#define XF86XK_ApplicationLeft 0x1008FF50 +#define XF86XK_ApplicationRight 0x1008FF51 +#define XF86XK_Book 0x1008FF52 +#define XF86XK_CD 0x1008FF53 +#define XF86XK_Calculater 0x1008FF54 +#define XF86XK_Clear 0x1008FF55 +#define XF86XK_ClearGrab 0x1008FE21 +#define XF86XK_Close 0x1008FF56 +#define XF86XK_Copy 0x1008FF57 +#define XF86XK_Cut 0x1008FF58 +#define XF86XK_Display 0x1008FF59 +#define XF86XK_DOS 0x1008FF5A +#define XF86XK_Documents 0x1008FF5B +#define XF86XK_Excel 0x1008FF5C +#define XF86XK_Explorer 0x1008FF5D +#define XF86XK_Game 0x1008FF5E +#define XF86XK_Go 0x1008FF5F +#define XF86XK_iTouch 0x1008FF60 +#define XF86XK_LogOff 0x1008FF61 +#define XF86XK_Market 0x1008FF62 +#define XF86XK_Meeting 0x1008FF63 +#define XF86XK_MenuKB 0x1008FF65 +#define XF86XK_MenuPB 0x1008FF66 +#define XF86XK_MySites 0x1008FF67 +#define XF86XK_New 0x1008FF68 +#define XF86XK_News 0x1008FF69 +#define XF86XK_OfficeHome 0x1008FF6A +#define XF86XK_Open 0x1008FF6B +#define XF86XK_Option 0x1008FF6C +#define XF86XK_Paste 0x1008FF6D +#define XF86XK_Phone 0x1008FF6E +#define XF86XK_Reply 0x1008FF72 +#define XF86XK_Reload 0x1008FF73 +#define XF86XK_RotateWindows 0x1008FF74 +#define XF86XK_RotationPB 0x1008FF75 +#define XF86XK_RotationKB 0x1008FF76 +#define XF86XK_Save 0x1008FF77 +#define XF86XK_Send 0x1008FF7B +#define XF86XK_Spell 0x1008FF7C +#define XF86XK_SplitScreen 0x1008FF7D +#define XF86XK_Support 0x1008FF7E +#define XF86XK_TaskPane 0x1008FF7F +#define XF86XK_Terminal 0x1008FF80 +#define XF86XK_Tools 0x1008FF81 +#define XF86XK_Travel 0x1008FF82 +#define XF86XK_Video 0x1008FF87 +#define XF86XK_Word 0x1008FF89 +#define XF86XK_Xfer 0x1008FF8A +#define XF86XK_ZoomIn 0x1008FF8B +#define XF86XK_ZoomOut 0x1008FF8C +#define XF86XK_Away 0x1008FF8D +#define XF86XK_Messenger 0x1008FF8E +#define XF86XK_WebCam 0x1008FF8F +#define XF86XK_MailForward 0x1008FF90 +#define XF86XK_Pictures 0x1008FF91 +#define XF86XK_Music 0x1008FF92 +#define XF86XK_Battery 0x1008FF93 +#define XF86XK_Bluetooth 0x1008FF94 +#define XF86XK_WLAN 0x1008FF95 +#define XF86XK_UWB 0x1008FF96 +#define XF86XK_AudioForward 0x1008FF97 +#define XF86XK_AudioRepeat 0x1008FF98 +#define XF86XK_AudioRandomPlay 0x1008FF99 +#define XF86XK_Subtitle 0x1008FF9A +#define XF86XK_AudioCycleTrack 0x1008FF9B +#define XF86XK_Time 0x1008FF9F +#define XF86XK_Select 0x1008FFA0 +#define XF86XK_View 0x1008FFA1 +#define XF86XK_TopMenu 0x1008FFA2 +#define XF86XK_Red 0x1008FFA3 +#define XF86XK_Green 0x1008FFA4 +#define XF86XK_Yellow 0x1008FFA5 +#define XF86XK_Blue 0x1008FFA6 +#define XF86XK_Suspend 0x1008FFA7 +#define XF86XK_Hibernate 0x1008FFA8 +#define XF86XK_TouchpadToggle 0x1008FFA9 +#define XF86XK_TouchpadOn 0x1008FFB0 +#define XF86XK_TouchpadOff 0x1008FFB1 +#define XF86XK_AudioMicMute 0x1008FFB2 + +// end of XF86keysyms.h + +// keyboard mapping table +static const unsigned int keyTable[] = { + + // misc keys + + XK_Escape, + Qt::Key_Escape, + XK_Tab, + Qt::Key_Tab, + XK_ISO_Left_Tab, + Qt::Key_Backtab, + XK_BackSpace, + Qt::Key_Backspace, + XK_Return, + Qt::Key_Return, + XK_Insert, + Qt::Key_Insert, + XK_Delete, + Qt::Key_Delete, + XK_Clear, + Qt::Key_Delete, + XK_Pause, + Qt::Key_Pause, + XK_Print, + Qt::Key_Print, + 0x1005FF60, + Qt::Key_SysReq, // hardcoded Sun SysReq + 0x1007ff00, + Qt::Key_SysReq, // hardcoded X386 SysReq + + // cursor movement + + XK_Home, + Qt::Key_Home, + XK_End, + Qt::Key_End, + XK_Left, + Qt::Key_Left, + XK_Up, + Qt::Key_Up, + XK_Right, + Qt::Key_Right, + XK_Down, + Qt::Key_Down, + XK_Prior, + Qt::Key_PageUp, + XK_Next, + Qt::Key_PageDown, + + // modifiers + + XK_Shift_L, + Qt::Key_Shift, + XK_Shift_R, + Qt::Key_Shift, + XK_Shift_Lock, + Qt::Key_Shift, + XK_Control_L, + Qt::Key_Control, + XK_Control_R, + Qt::Key_Control, + XK_Meta_L, + Qt::Key_Meta, + XK_Meta_R, + Qt::Key_Meta, + XK_Alt_L, + Qt::Key_Alt, + XK_Alt_R, + Qt::Key_Alt, + XK_Caps_Lock, + Qt::Key_CapsLock, + XK_Num_Lock, + Qt::Key_NumLock, + XK_Scroll_Lock, + Qt::Key_ScrollLock, + XK_Super_L, + Qt::Key_Super_L, + XK_Super_R, + Qt::Key_Super_R, + XK_Menu, + Qt::Key_Menu, + XK_Hyper_L, + Qt::Key_Hyper_L, + XK_Hyper_R, + Qt::Key_Hyper_R, + XK_Help, + Qt::Key_Help, + 0x1000FF74, + Qt::Key_Backtab, // hardcoded HP backtab + 0x1005FF10, + Qt::Key_F11, // hardcoded Sun F36 (labeled F11) + 0x1005FF11, + Qt::Key_F12, // hardcoded Sun F37 (labeled F12) + + // numeric and function keypad keys + + XK_KP_Space, + Qt::Key_Space, + XK_KP_Tab, + Qt::Key_Tab, + XK_KP_Enter, + Qt::Key_Enter, + XK_KP_F1, + Qt::Key_F1, + XK_F2, + Qt::Key_F2, + XK_F3, + Qt::Key_F3, + XK_F4, + Qt::Key_F4, + XK_F5, + Qt::Key_F5, + XK_F6, + Qt::Key_F6, + XK_F7, + Qt::Key_F7, + XK_F8, + Qt::Key_F8, + XK_F9, + Qt::Key_F9, + XK_F10, + Qt::Key_F10, + XK_KP_Home, + Qt::Key_Home, + XK_KP_Left, + Qt::Key_Left, + XK_KP_Up, + Qt::Key_Up, + XK_KP_Right, + Qt::Key_Right, + XK_KP_Down, + Qt::Key_Down, + XK_KP_Prior, + Qt::Key_PageUp, + XK_KP_Next, + Qt::Key_PageDown, + XK_KP_End, + Qt::Key_End, + XK_KP_Begin, + Qt::Key_Clear, + XK_KP_Insert, + Qt::Key_Insert, + XK_KP_Delete, + Qt::Key_Delete, + XK_KP_Equal, + Qt::Key_Equal, + XK_KP_Multiply, + Qt::Key_Asterisk, + XK_KP_Add, + Qt::Key_Plus, + XK_KP_Separator, + Qt::Key_Comma, + XK_KP_Subtract, + Qt::Key_Minus, + XK_KP_Decimal, + Qt::Key_Period, + XK_KP_Divide, + Qt::Key_Slash, + + // International input method support keys + + // International & multi-key character composition + XK_ISO_Level3_Shift, + Qt::Key_AltGr, + XK_Multi_key, + Qt::Key_Multi_key, + XK_Codeinput, + Qt::Key_Codeinput, + XK_SingleCandidate, + Qt::Key_SingleCandidate, + XK_MultipleCandidate, + Qt::Key_MultipleCandidate, + XK_PreviousCandidate, + Qt::Key_PreviousCandidate, + + // Misc Functions + XK_Mode_switch, + Qt::Key_Mode_switch, + XK_script_switch, + Qt::Key_Mode_switch, + + // Japanese keyboard support + XK_Kanji, + Qt::Key_Kanji, + XK_Muhenkan, + Qt::Key_Muhenkan, + // XK_Henkan_Mode, Qt::Key_Henkan_Mode, + XK_Henkan_Mode, + Qt::Key_Henkan, + XK_Henkan, + Qt::Key_Henkan, + XK_Romaji, + Qt::Key_Romaji, + XK_Hiragana, + Qt::Key_Hiragana, + XK_Katakana, + Qt::Key_Katakana, + XK_Hiragana_Katakana, + Qt::Key_Hiragana_Katakana, + XK_Zenkaku, + Qt::Key_Zenkaku, + XK_Hankaku, + Qt::Key_Hankaku, + XK_Zenkaku_Hankaku, + Qt::Key_Zenkaku_Hankaku, + XK_Touroku, + Qt::Key_Touroku, + XK_Massyo, + Qt::Key_Massyo, + XK_Kana_Lock, + Qt::Key_Kana_Lock, + XK_Kana_Shift, + Qt::Key_Kana_Shift, + XK_Eisu_Shift, + Qt::Key_Eisu_Shift, + XK_Eisu_toggle, + Qt::Key_Eisu_toggle, + // XK_Kanji_Bangou, Qt::Key_Kanji_Bangou, + // XK_Zen_Koho, Qt::Key_Zen_Koho, + // XK_Mae_Koho, Qt::Key_Mae_Koho, + XK_Kanji_Bangou, + Qt::Key_Codeinput, + XK_Zen_Koho, + Qt::Key_MultipleCandidate, + XK_Mae_Koho, + Qt::Key_PreviousCandidate, + +#ifdef XK_KOREAN + // Korean keyboard support + XK_Hangul, + Qt::Key_Hangul, + XK_Hangul_Start, + Qt::Key_Hangul_Start, + XK_Hangul_End, + Qt::Key_Hangul_End, + XK_Hangul_Hanja, + Qt::Key_Hangul_Hanja, + XK_Hangul_Jamo, + Qt::Key_Hangul_Jamo, + XK_Hangul_Romaja, + Qt::Key_Hangul_Romaja, + // XK_Hangul_Codeinput, Qt::Key_Hangul_Codeinput, + XK_Hangul_Codeinput, + Qt::Key_Codeinput, + XK_Hangul_Jeonja, + Qt::Key_Hangul_Jeonja, + XK_Hangul_Banja, + Qt::Key_Hangul_Banja, + XK_Hangul_PreHanja, + Qt::Key_Hangul_PreHanja, + XK_Hangul_PostHanja, + Qt::Key_Hangul_PostHanja, + // XK_Hangul_SingleCandidate,Qt::Key_Hangul_SingleCandidate, + // XK_Hangul_MultipleCandidate,Qt::Key_Hangul_MultipleCandidate, + // XK_Hangul_PreviousCandidate,Qt::Key_Hangul_PreviousCandidate, + XK_Hangul_SingleCandidate, + Qt::Key_SingleCandidate, + XK_Hangul_MultipleCandidate, + Qt::Key_MultipleCandidate, + XK_Hangul_PreviousCandidate, + Qt::Key_PreviousCandidate, + XK_Hangul_Special, + Qt::Key_Hangul_Special, + // XK_Hangul_switch, Qt::Key_Hangul_switch, + XK_Hangul_switch, + Qt::Key_Mode_switch, +#endif // XK_KOREAN + + // dead keys + XK_dead_grave, + Qt::Key_Dead_Grave, + XK_dead_acute, + Qt::Key_Dead_Acute, + XK_dead_circumflex, + Qt::Key_Dead_Circumflex, + XK_dead_tilde, + Qt::Key_Dead_Tilde, + XK_dead_macron, + Qt::Key_Dead_Macron, + XK_dead_breve, + Qt::Key_Dead_Breve, + XK_dead_abovedot, + Qt::Key_Dead_Abovedot, + XK_dead_diaeresis, + Qt::Key_Dead_Diaeresis, + XK_dead_abovering, + Qt::Key_Dead_Abovering, + XK_dead_doubleacute, + Qt::Key_Dead_Doubleacute, + XK_dead_caron, + Qt::Key_Dead_Caron, + XK_dead_cedilla, + Qt::Key_Dead_Cedilla, + XK_dead_ogonek, + Qt::Key_Dead_Ogonek, + XK_dead_iota, + Qt::Key_Dead_Iota, + XK_dead_voiced_sound, + Qt::Key_Dead_Voiced_Sound, + XK_dead_semivoiced_sound, + Qt::Key_Dead_Semivoiced_Sound, + XK_dead_belowdot, + Qt::Key_Dead_Belowdot, + XK_dead_hook, + Qt::Key_Dead_Hook, + XK_dead_horn, + Qt::Key_Dead_Horn, + + // Special keys from X.org - This include multimedia keys, + // wireless/bluetooth/uwb keys, special launcher keys, etc. + XF86XK_Back, + Qt::Key_Back, + XF86XK_Forward, + Qt::Key_Forward, + XF86XK_Stop, + Qt::Key_Stop, + XF86XK_Refresh, + Qt::Key_Refresh, + XF86XK_Favorites, + Qt::Key_Favorites, + XF86XK_AudioMedia, + Qt::Key_LaunchMedia, + XF86XK_OpenURL, + Qt::Key_OpenUrl, + XF86XK_HomePage, + Qt::Key_HomePage, + XF86XK_Search, + Qt::Key_Search, + XF86XK_AudioLowerVolume, + Qt::Key_VolumeDown, + XF86XK_AudioMute, + Qt::Key_VolumeMute, + XF86XK_AudioRaiseVolume, + Qt::Key_VolumeUp, + XF86XK_AudioPlay, + Qt::Key_MediaPlay, + XF86XK_AudioStop, + Qt::Key_MediaStop, + XF86XK_AudioPrev, + Qt::Key_MediaPrevious, + XF86XK_AudioNext, + Qt::Key_MediaNext, + XF86XK_AudioRecord, + Qt::Key_MediaRecord, + XF86XK_AudioPause, + Qt::Key_MediaPause, + XF86XK_Mail, + Qt::Key_LaunchMail, + XF86XK_MyComputer, + Qt::Key_Launch0, // ### Qt 6: remap properly + XF86XK_Calculator, + Qt::Key_Launch1, + XF86XK_Memo, + Qt::Key_Memo, + XF86XK_ToDoList, + Qt::Key_ToDoList, + XF86XK_Calendar, + Qt::Key_Calendar, + XF86XK_PowerDown, + Qt::Key_PowerDown, + XF86XK_ContrastAdjust, + Qt::Key_ContrastAdjust, + XF86XK_Standby, + Qt::Key_Standby, + XF86XK_MonBrightnessUp, + Qt::Key_MonBrightnessUp, + XF86XK_MonBrightnessDown, + Qt::Key_MonBrightnessDown, + XF86XK_KbdLightOnOff, + Qt::Key_KeyboardLightOnOff, + XF86XK_KbdBrightnessUp, + Qt::Key_KeyboardBrightnessUp, + XF86XK_KbdBrightnessDown, + Qt::Key_KeyboardBrightnessDown, + XF86XK_PowerOff, + Qt::Key_PowerOff, + XF86XK_WakeUp, + Qt::Key_WakeUp, + XF86XK_Eject, + Qt::Key_Eject, + XF86XK_ScreenSaver, + Qt::Key_ScreenSaver, + XF86XK_WWW, + Qt::Key_WWW, + XF86XK_Sleep, + Qt::Key_Sleep, + XF86XK_LightBulb, + Qt::Key_LightBulb, + XF86XK_Shop, + Qt::Key_Shop, + XF86XK_History, + Qt::Key_History, + XF86XK_AddFavorite, + Qt::Key_AddFavorite, + XF86XK_HotLinks, + Qt::Key_HotLinks, + XF86XK_BrightnessAdjust, + Qt::Key_BrightnessAdjust, + XF86XK_Finance, + Qt::Key_Finance, + XF86XK_Community, + Qt::Key_Community, + XF86XK_AudioRewind, + Qt::Key_AudioRewind, + XF86XK_BackForward, + Qt::Key_BackForward, + XF86XK_ApplicationLeft, + Qt::Key_ApplicationLeft, + XF86XK_ApplicationRight, + Qt::Key_ApplicationRight, + XF86XK_Book, + Qt::Key_Book, + XF86XK_CD, + Qt::Key_CD, + XF86XK_Calculater, + Qt::Key_Calculator, + XF86XK_Clear, + Qt::Key_Clear, + XF86XK_ClearGrab, + Qt::Key_ClearGrab, + XF86XK_Close, + Qt::Key_Close, + XF86XK_Copy, + Qt::Key_Copy, + XF86XK_Cut, + Qt::Key_Cut, + XF86XK_Display, + Qt::Key_Display, + XF86XK_DOS, + Qt::Key_DOS, + XF86XK_Documents, + Qt::Key_Documents, + XF86XK_Excel, + Qt::Key_Excel, + XF86XK_Explorer, + Qt::Key_Explorer, + XF86XK_Game, + Qt::Key_Game, + XF86XK_Go, + Qt::Key_Go, + XF86XK_iTouch, + Qt::Key_iTouch, + XF86XK_LogOff, + Qt::Key_LogOff, + XF86XK_Market, + Qt::Key_Market, + XF86XK_Meeting, + Qt::Key_Meeting, + XF86XK_MenuKB, + Qt::Key_MenuKB, + XF86XK_MenuPB, + Qt::Key_MenuPB, + XF86XK_MySites, + Qt::Key_MySites, +#if QT_VERSION >= 0x050400 + XF86XK_New, + Qt::Key_New, +#endif + XF86XK_News, + Qt::Key_News, + XF86XK_OfficeHome, + Qt::Key_OfficeHome, +#if QT_VERSION >= 0x050400 + XF86XK_Open, + Qt::Key_Open, +#endif + XF86XK_Option, + Qt::Key_Option, + XF86XK_Paste, + Qt::Key_Paste, + XF86XK_Phone, + Qt::Key_Phone, + XF86XK_Reply, + Qt::Key_Reply, + XF86XK_Reload, + Qt::Key_Reload, + XF86XK_RotateWindows, + Qt::Key_RotateWindows, + XF86XK_RotationPB, + Qt::Key_RotationPB, + XF86XK_RotationKB, + Qt::Key_RotationKB, + XF86XK_Save, + Qt::Key_Save, + XF86XK_Send, + Qt::Key_Send, + XF86XK_Spell, + Qt::Key_Spell, + XF86XK_SplitScreen, + Qt::Key_SplitScreen, + XF86XK_Support, + Qt::Key_Support, + XF86XK_TaskPane, + Qt::Key_TaskPane, + XF86XK_Terminal, + Qt::Key_Terminal, + XF86XK_Tools, + Qt::Key_Tools, + XF86XK_Travel, + Qt::Key_Travel, + XF86XK_Video, + Qt::Key_Video, + XF86XK_Word, + Qt::Key_Word, + XF86XK_Xfer, + Qt::Key_Xfer, + XF86XK_ZoomIn, + Qt::Key_ZoomIn, + XF86XK_ZoomOut, + Qt::Key_ZoomOut, + XF86XK_Away, + Qt::Key_Away, + XF86XK_Messenger, + Qt::Key_Messenger, + XF86XK_WebCam, + Qt::Key_WebCam, + XF86XK_MailForward, + Qt::Key_MailForward, + XF86XK_Pictures, + Qt::Key_Pictures, + XF86XK_Music, + Qt::Key_Music, + XF86XK_Battery, + Qt::Key_Battery, + XF86XK_Bluetooth, + Qt::Key_Bluetooth, + XF86XK_WLAN, + Qt::Key_WLAN, + XF86XK_UWB, + Qt::Key_UWB, + XF86XK_AudioForward, + Qt::Key_AudioForward, + XF86XK_AudioRepeat, + Qt::Key_AudioRepeat, + XF86XK_AudioRandomPlay, + Qt::Key_AudioRandomPlay, + XF86XK_Subtitle, + Qt::Key_Subtitle, + XF86XK_AudioCycleTrack, + Qt::Key_AudioCycleTrack, + XF86XK_Time, + Qt::Key_Time, + XF86XK_Select, + Qt::Key_Select, + XF86XK_View, + Qt::Key_View, + XF86XK_TopMenu, + Qt::Key_TopMenu, +#if QT_VERSION >= 0x050400 + XF86XK_Red, + Qt::Key_Red, + XF86XK_Green, + Qt::Key_Green, + XF86XK_Yellow, + Qt::Key_Yellow, + XF86XK_Blue, + Qt::Key_Blue, +#endif + XF86XK_Bluetooth, + Qt::Key_Bluetooth, + XF86XK_Suspend, + Qt::Key_Suspend, + XF86XK_Hibernate, + Qt::Key_Hibernate, +#if QT_VERSION >= 0x050400 + XF86XK_TouchpadToggle, + Qt::Key_TouchpadToggle, + XF86XK_TouchpadOn, + Qt::Key_TouchpadOn, + XF86XK_TouchpadOff, + Qt::Key_TouchpadOff, + XF86XK_AudioMicMute, + Qt::Key_MicMute, +#endif + XF86XK_Launch0, + Qt::Key_Launch2, // ### Qt 6: remap properly + XF86XK_Launch1, + Qt::Key_Launch3, + XF86XK_Launch2, + Qt::Key_Launch4, + XF86XK_Launch3, + Qt::Key_Launch5, + XF86XK_Launch4, + Qt::Key_Launch6, + XF86XK_Launch5, + Qt::Key_Launch7, + XF86XK_Launch6, + Qt::Key_Launch8, + XF86XK_Launch7, + Qt::Key_Launch9, + XF86XK_Launch8, + Qt::Key_LaunchA, + XF86XK_Launch9, + Qt::Key_LaunchB, + XF86XK_LaunchA, + Qt::Key_LaunchC, + XF86XK_LaunchB, + Qt::Key_LaunchD, + XF86XK_LaunchC, + Qt::Key_LaunchE, + XF86XK_LaunchD, + Qt::Key_LaunchF, + XF86XK_LaunchE, + Qt::Key_LaunchG, + XF86XK_LaunchF, + Qt::Key_LaunchH, + + 0, + 0}; diff --git a/src/app/pttlistener.h b/src/app/pttlistener.h new file mode 100644 index 000000000..4f527896a --- /dev/null +++ b/src/app/pttlistener.h @@ -0,0 +1,52 @@ +#pragma once + +#include "systemtray.h" +#include "appsettingsmanager.h" + +#include <QObject> +#include <QThread> +#include <QKeyEvent> + +class PTTListener : public QObject +{ + Q_OBJECT + +public: + Q_INVOKABLE Qt::Key getCurrentKey() + { + int keyInt = settingsManager_->getValue(Settings::Key::pttKey).toInt(); + Qt::Key key = static_cast<Qt::Key>(keyInt); + return key; + } + + Q_INVOKABLE QString keyToString(Qt::Key key) + { + return QKeySequence(key).toString(); + } + Q_INVOKABLE void setPttKey(Qt::Key key) + { + settingsManager_->setValue(Settings::Key::pttKey, key); + } + Q_INVOKABLE bool getPttState() + { + return settingsManager_->getValue(Settings::Key::EnablePtt).toBool(); + } + + PTTListener(AppSettingsManager* settingsManager, QObject* parent = nullptr); + ~PTTListener(); + +Q_SIGNALS: + void pttKeyPressed(); + void pttKeyReleased(); + +#ifdef HAVE_GLOBAL_PTT +public Q_SLOTS: + void startListening(); + void stopListening(); +#endif + +private: + class Impl; + std::unique_ptr<Impl> pimpl_; + AppSettingsManager* settingsManager_; +}; diff --git a/src/app/settingsview/components/CallSettingsPage.qml b/src/app/settingsview/components/CallSettingsPage.qml index 9a8bfa51c..ba25fab98 100644 --- a/src/app/settingsview/components/CallSettingsPage.qml +++ b/src/app/settingsview/components/CallSettingsPage.qml @@ -28,11 +28,13 @@ import "../../commoncomponents" import "../../mainview/components" import "../../mainview/js/contactpickercreation.js" as ContactPickerCreation + SettingsPageBase { id: root property bool isSIP: CurrentAccount.type === Profile.Type.SIP property int itemWidth: 132 + property string key: pttListener.keyToString(pttListener.getCurrentKey()) title: JamiStrings.callSettingsTitle function updateAndShowModeratorsSlot() { @@ -374,5 +376,72 @@ SettingsPageBase { } } } + ColumnLayout{ + width: parent.width + spacing: 9 + Text { + text: JamiStrings.pushToTalk + color: JamiTheme.textColor + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + font.pixelSize: JamiTheme.settingsTitlePixelSize + font.kerning: true + } + ToggleSwitch { + id: pttToggle + labelText: JamiStrings.enablePTT + checked: UtilsAdapter.getAppValue(Settings.EnablePtt) + onSwitchToggled: { + UtilsAdapter.setAppValue(Settings.Key.EnablePtt, checked) + } + } + RowLayout { + visible: pttToggle.checked + Layout.preferredWidth: parent.width + + Label { + color: JamiTheme.textColor + wrapMode: Text.WordWrap + text: JamiStrings.keyboardShortcut + font.pointSize: JamiTheme.settingsFontSize + font.kerning: true + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + Label { + id: keyLabel + color: JamiTheme.blackColor + wrapMode: Text.WordWrap + text: key + font.pointSize: JamiTheme.settingsFontSize + font.kerning: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + background: Rectangle { + id: backgroundRect + anchors.centerIn: parent + width: keyLabel.width + 2 * JamiTheme.preferredMarginSize + height: keyLabel.height + JamiTheme.preferredMarginSize + color: JamiTheme.lightGrey_ + border.color: JamiTheme.darkGreyColor + radius: 4 + } + } + MaterialButton { + Layout.alignment: Qt.AlignRight + buttontextHeightMargin: JamiTheme.buttontextHeightMargin + primary: true + toolTipText: JamiStrings.changeKeyboardShortcut + text: JamiStrings.change + onClicked: { + var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/ChangePttKeyPopup.qml"); + dlg.choiceMade.connect(function (chosenKey) { + keyLabel.text = pttListener.keyToString(chosenKey); + }); + } + } + } + } } } diff --git a/src/app/systemtray.h b/src/app/systemtray.h index 219ac94fb..5b8fc0dd8 100644 --- a/src/app/systemtray.h +++ b/src/app/systemtray.h @@ -37,6 +37,11 @@ public: explicit SystemTray(AppSettingsManager* settingsManager, QObject* parent = nullptr); ~SystemTray(); + AppSettingsManager* getSettingsManager() + { + return settingsManager_; + } + void onNotificationCountChanged(int count); #ifdef Q_OS_LINUX bool hideNotification(const QString& id); diff --git a/src/libclient/callmodel.cpp b/src/libclient/callmodel.cpp index 27d6115ce..f9c3a4105 100644 --- a/src/libclient/callmodel.cpp +++ b/src/libclient/callmodel.cpp @@ -454,6 +454,8 @@ CallModel::muteMedia(const QString& callId, const QString& label, bool mute) return; auto proposedList = callInfo->mediaList; + if (proposedList.isEmpty()) + return; for (auto& media : proposedList) if (media[MediaAttributeKey::LABEL] == label) media[MediaAttributeKey::MUTED] = mute ? TRUE_STR : FALSE_STR; @@ -1413,8 +1415,7 @@ CallModelPimpl::slotCallStateChanged(const QString& accountId, callInfo->mediaList = {}; calls.emplace(callId, std::move(callInfo)); - if (!(details["CALL_TYPE"] == "1") - && !linked.owner.confProperties.allowIncoming + if (!(details["CALL_TYPE"] == "1") && !linked.owner.confProperties.allowIncoming && linked.owner.profileInfo.type == profile::Type::JAMI) { linked.refuse(callId); return; -- GitLab