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