From 44dba711794e78016392f8e0769339f2a189fd63 Mon Sep 17 00:00:00 2001
From: Ming Rui Zhang <mingrui.zhang@savoirfairelinux.com>
Date: Tue, 25 Aug 2020 14:32:34 -0400
Subject: [PATCH] mainview: add sip input panel

Add sip input panel to be able to use DTMF functionalities.

Gitlab: #18
Change-Id: Iaa53ae2b34d9ce0d5cf19aa82dd41a5607203c62
---
 images/icons/ic_keypad.svg                    |  51 ++++++
 images/icons/icon-keypad-24-2x.png            | Bin 4183 -> 0 bytes
 images/icons/icon-keypad-24.png               | Bin 879 -> 0 bytes
 qml.qrc                                       |   1 +
 ressources.qrc                                |   3 +-
 src/calladapter.cpp                           | 163 ++++++++++--------
 src/calladapter.h                             |  55 +++---
 src/commoncomponents/HoverableButton.qml      |  14 +-
 src/constant/JamiTheme.qml                    |   3 +
 src/mainview/components/CallOverlay.qml       |   6 +
 .../components/CallOverlayButtonGroup.qml     |   2 -
 .../components/CallViewContextMenu.qml        |  17 ++
 src/mainview/components/SipInputPanel.qml     |  92 ++++++++++
 13 files changed, 305 insertions(+), 102 deletions(-)
 create mode 100644 images/icons/ic_keypad.svg
 delete mode 100644 images/icons/icon-keypad-24-2x.png
 delete mode 100644 images/icons/icon-keypad-24.png
 create mode 100644 src/mainview/components/SipInputPanel.qml

diff --git a/images/icons/ic_keypad.svg b/images/icons/ic_keypad.svg
new file mode 100644
index 000000000..c55cdf4fa
--- /dev/null
+++ b/images/icons/ic_keypad.svg
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<g>
+			<path d="M64,0C28.715,0,0,28.715,0,64s28.715,64,64,64s64-28.715,64-64S99.285,0,64,0z"/>
+			<path d="M64,192c-35.285,0-64,28.715-64,64s28.715,64,64,64s64-28.715,64-64S99.285,192,64,192z"/>
+			<path d="M64,384c-35.285,0-64,28.715-64,64c0,35.285,28.715,64,64,64s64-28.715,64-64C128,412.715,99.285,384,64,384z"/>
+			<path d="M256,0c-35.285,0-64,28.715-64,64s28.715,64,64,64s64-28.715,64-64S291.285,0,256,0z"/>
+			<path d="M256,192c-35.285,0-64,28.715-64,64s28.715,64,64,64s64-28.715,64-64S291.285,192,256,192z"/>
+			<path d="M256,384c-35.285,0-64,28.715-64,64c0,35.285,28.715,64,64,64s64-28.715,64-64C320,412.715,291.285,384,256,384z"/>
+			<path d="M448,128c35.285,0,64-28.715,64-64S483.285,0,448,0c-35.285,0-64,28.715-64,64S412.715,128,448,128z"/>
+			<path d="M448,192c-35.285,0-64,28.715-64,64s28.715,64,64,64c35.285,0,64-28.715,64-64S483.285,192,448,192z"/>
+			<path d="M448,384c-35.285,0-64,28.715-64,64c0,35.285,28.715,64,64,64c35.285,0,64-28.715,64-64C512,412.715,483.285,384,448,384
+				z"/>
+		</g>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/images/icons/icon-keypad-24-2x.png b/images/icons/icon-keypad-24-2x.png
deleted file mode 100644
index 2b84863e648cf02bb13cbbdbe2e53168897fc5cd..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4183
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%0AO*cUbm$P+wQJWN)N9qMl|R?1UAuN?
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

diff --git a/images/icons/icon-keypad-24.png b/images/icons/icon-keypad-24.png
deleted file mode 100644
index 2abc9be80cb8393a2357c49383663951b5a4b88f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 879
zcmV-#1CacQP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0009vNkl<Zc-oa#
zOKVeM5N)Gva{EY3Y}1EHD<mKUyVI2jDp=`8e?z+|t!}bV5Wy656GAs#NP~+Ol0|4C
zNI}F!7Dn=D)lD{qw)@0Q$ip<0B&~D8d~v)@)AquFX70?K`OY^pw_RQTo^FPLaYf+j
z1t9%Zj&7OUpi9<6=oK(_fb{DTgobv>RWcJnBlKMY8pjz0QInvV-0Ssf)oRsOuh+jF
z9v*&JSy^$A-3;vS?>{*@I{E_a?d?q~`||R#lYCcewOYJfE-%f@%$S+X#KpF%@1^8#
zPsBsNKat(Gv9U2lKP$?9e0=;T91c&>yX)k$C1Z-kqL1u@f}}}+qZm=SbZ#^nC9>NO
z4i4r|bPt_kp5Bd+Pg&aOgx|?-5hTs(p_d7==$unzAB#q#kH9CnH#avQ(7SOu!H$e6
z6beC{nkDR41kg%@=8y$dA^H0H`aQDWAUNn0uSn+{AxqW2B%2dhTU)zNG24itXKQhB
z(M|Rt!ZpIce*}Ciu-j2NCr6@*3&6?#ql96CmGiX-pqCjJ3e6!4D(B!yx7+PXr_*z(
zRB9G~!=^wNKAp*AUIYSxyIfd%0<@y&mIh=2fyd)<)BXHXI@8}jTrL-+xVgK#`$pXl
zbyp=Gk3VAtY^Mm&<MP6wIS5d-zKarx#4HG?^L|TvrBeBN8G`J21SsOd1ej0^k&=kJ
zWdfQ*79r7&km$9PC>D!7J(B>J9En73V~DuVRR>KXVKSMVL***DLMRk^OqcxJ@Au!q
zY;+{x7bEQ0B9IB&V3PeD9SN|wuv;)Rm{1InB0#s`LIKF4k_Usqha}lg#FR><&kG9+
zx6UM>3tH7JyKNAl&-JSUvVa)LWlTPw|HyN2ia^84ZL{p0&{y9@uz_5^?5L<jyFtUU
z(`c9Yq%(F4#vc=k4iW3R!WZg*EHWmW&A!7L*YBd{HpRz+_W2T4F18{sF6@>uOelIB
z;8w(Cjze?Eg35Eb+<Th(QT;Y;eGj;k2ASQ)TXC3S?*Qd9#Bmm`UX!299{vI;5(^Vq
zIp{oC+272``?%=VM_+yL6-hrk4hoptK)xT~I8FVx@fQ{IB2X&}y<-3X002ovPDHLk
FV1j|#pF98n

diff --git a/qml.qrc b/qml.qrc
index 5d20bbbed..d9e8085f7 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -102,5 +102,6 @@
         <file>src/commoncomponents/MaterialButton.qml</file>
         <file>src/mainview/components/RecordBox.qml</file>
         <file>src/commoncomponents/ElidedTextLabel.qml</file>
+        <file>src/mainview/components/SipInputPanel.qml</file>
     </qresource>
 </RCC>
diff --git a/ressources.qrc b/ressources.qrc
index f64f592a1..357fe0bf7 100644
--- a/ressources.qrc
+++ b/ressources.qrc
@@ -44,6 +44,7 @@
         <file>images/icons/ic_content_copy.svg</file>
         <file>images/icons/ic_delete_black_18dp_2x.png</file>
         <file>images/icons/ic_done_white_24dp.png</file>
+        <file>images/icons/ic_keypad.svg</file>
         <file>images/icons/open_in_full-24px.svg</file>
         <file>images/icons/close_fullscreen-24px.svg</file>
         <file>images/icons/ic_group_add_white_24dp.png</file>
@@ -97,9 +98,7 @@
         <file>images/icons/screen_share-24px.svg</file>
         <file>images/icons/round-add_a_photo-24px.svg</file>
         <file>images/icons/ic_mic_white_24dp.png</file>
-        <file>images/icons/icon-keypad-24.png</file>
         <file>images/icons/ic_play_white_24dp.png</file>
-        <file>images/icons/icon-keypad-24-2x.png</file>
         <file>images/icons/ic_voicemail_black_24dp_2x_.png</file>
         <file>images/icons/av_icons/delete-24px.svg</file>
         <file>images/icons/av_icons/fiber_manual_record-24px.svg</file>
diff --git a/src/calladapter.cpp b/src/calladapter.cpp
index efc74102d..c4badc91d 100644
--- a/src/calladapter.cpp
+++ b/src/calladapter.cpp
@@ -27,7 +27,7 @@
 #include "globalsystemtray.h"
 #include "utils.h"
 
-CallAdapter::CallAdapter(QObject *parent)
+CallAdapter::CallAdapter(QObject* parent)
     : QmlAdapterBase(parent)
     , oneSecondTimer_(new QTimer(this))
 {}
@@ -78,7 +78,7 @@ CallAdapter::placeCall()
 }
 
 void
-CallAdapter::hangUpACall(const QString &accountId, const QString &convUid)
+CallAdapter::hangUpACall(const QString& accountId, const QString& convUid)
 {
     auto* convModel = LRCInstance::getCurrentConversationModel();
     const auto convInfo = convModel->getConversationForUID(convUid);
@@ -88,7 +88,7 @@ CallAdapter::hangUpACall(const QString &accountId, const QString &convUid)
 }
 
 void
-CallAdapter::refuseACall(const QString &accountId, const QString &convUid)
+CallAdapter::refuseACall(const QString& accountId, const QString& convUid)
 {
     auto* convModel = LRCInstance::getCurrentConversationModel();
     const auto convInfo = convModel->getConversationForUID(convUid);
@@ -98,20 +98,20 @@ CallAdapter::refuseACall(const QString &accountId, const QString &convUid)
 }
 
 void
-CallAdapter::acceptACall(const QString &accountId, const QString &convUid)
+CallAdapter::acceptACall(const QString& accountId, const QString& convUid)
 {
     emit incomingCallNeedToSetupMainView(accountId, convUid);
     auto* convModel = LRCInstance::getCurrentConversationModel();
     const auto convInfo = convModel->getConversationForUID(convUid);
     if (!convInfo.uid.isEmpty()) {
         LRCInstance::getAccountInfo(accountId).callModel->accept(convInfo.callId);
-        auto &accInfo = LRCInstance::getAccountInfo(convInfo.accountId);
+        auto& accInfo = LRCInstance::getAccountInfo(convInfo.accountId);
         accInfo.callModel->setCurrentCall(convInfo.callId);
     }
 }
 
 void
-CallAdapter::slotShowIncomingCallView(const QString &accountId, const conversation::Info &convInfo)
+CallAdapter::slotShowIncomingCallView(const QString& accountId, const conversation::Info& convInfo)
 {
     auto* callModel = LRCInstance::getCurrentCallModel();
 
@@ -119,17 +119,17 @@ CallAdapter::slotShowIncomingCallView(const QString &accountId, const conversati
         /*
          * Connection to close potential incoming call page when it is not current account.
          */
-        auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
+        auto& accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
 
         QObject::disconnect(closeIncomingCallPageConnection_);
 
         closeIncomingCallPageConnection_
             = QObject::connect(accInfo.callModel.get(),
                                &lrc::api::NewCallModel::callStatusChanged,
-                               [this, accountId, uid = convInfo.uid](const QString &callId) {
-                                   auto &accInfo = LRCInstance::accountModel().getAccountInfo(
+                               [this, accountId, uid = convInfo.uid](const QString& callId) {
+                                   auto& accInfo = LRCInstance::accountModel().getAccountInfo(
                                        accountId);
-                                   auto &callModel = accInfo.callModel;
+                                   auto& callModel = accInfo.callModel;
                                    auto call = callModel->getCall(callId);
 
                                    switch (call.status) {
@@ -182,18 +182,18 @@ CallAdapter::slotShowIncomingCallView(const QString &accountId, const conversati
 }
 
 void
-CallAdapter::slotShowCallView(const QString &accountId, const lrc::api::conversation::Info &convInfo)
+CallAdapter::slotShowCallView(const QString& accountId, const lrc::api::conversation::Info& convInfo)
 {
     updateCall(convInfo.uid, accountId);
 }
 
 void
-CallAdapter::updateCall(const QString &convUid, const QString &accountId, bool forceCallOnly)
+CallAdapter::updateCall(const QString& convUid, const QString& accountId, bool forceCallOnly)
 {
     accountId_ = accountId.isEmpty() ? accountId_ : accountId;
     convUid_ = convUid.isEmpty() ? convUid_ : convUid;
 
-    auto *convModel = LRCInstance::getCurrentConversationModel();
+    auto* convModel = LRCInstance::getCurrentConversationModel();
     const auto convInfo = convModel->getConversationForUID(convUid_);
     if (convInfo.uid.isEmpty()) {
         return;
@@ -223,7 +223,7 @@ CallAdapter::updateCall(const QString &convUid, const QString &accountId, bool f
 bool
 CallAdapter::shouldShowPreview(bool force)
 {
-    bool shouldShowPreview{false};
+    bool shouldShowPreview {false};
     auto* convModel = LRCInstance::getCurrentConversationModel();
     const auto convInfo = convModel->getConversationForUID(convUid_);
     if (convInfo.uid.isEmpty()) {
@@ -245,11 +245,11 @@ CallAdapter::getConferencesInfos()
     const auto convInfo = convModel->getConversationForUID(convUid_);
     if (convInfo.uid.isEmpty())
         return map;
-    auto callId = convInfo.confId.isEmpty()? convInfo.callId : convInfo.confId;
+    auto callId = convInfo.confId.isEmpty() ? convInfo.callId : convInfo.confId;
     if (!callId.isEmpty()) {
         try {
             auto call = LRCInstance::getCurrentCallModel()->getCall(callId);
-            for (const auto& participant: call.participantsInfos) {
+            for (const auto& participant : call.participantsInfos) {
                 QJsonObject data;
                 data["x"] = participant["x"].toInt();
                 data["y"] = participant["y"].toInt();
@@ -257,31 +257,34 @@ CallAdapter::getConferencesInfos()
                 data["h"] = participant["h"].toInt();
                 data["active"] = participant["active"] == "true";
                 auto bestName = participant["uri"];
-                auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
+                auto& accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
                 data["isLocal"] = false;
                 if (bestName == accInfo.profileInfo.uri) {
                     bestName = tr("me");
                     data["isLocal"] = true;
                 } else {
                     try {
-                        auto &contact = LRCInstance::getCurrentAccountInfo().contactModel->getContact(participant["uri"]);
+                        auto& contact = LRCInstance::getCurrentAccountInfo()
+                                            .contactModel->getContact(participant["uri"]);
                         bestName = Utils::bestNameForContact(contact);
-                    } catch (...) {}
+                    } catch (...) {
+                    }
                 }
                 data["bestName"] = bestName;
 
                 map.push_back(QVariant(data));
             }
             return map;
-        } catch (...) {}
+        } catch (...) {
+        }
     }
     return map;
 }
 
 void
-CallAdapter::connectCallModel(const QString &accountId)
+CallAdapter::connectCallModel(const QString& accountId)
 {
-    auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
+    auto& accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
 
     QObject::disconnect(callStatusChangedConnection_);
     QObject::disconnect(onParticipantsChangedConnection_);
@@ -289,15 +292,15 @@ CallAdapter::connectCallModel(const QString &accountId)
     onParticipantsChangedConnection_ = QObject::connect(
         accInfo.callModel.get(),
         &lrc::api::NewCallModel::onParticipantsChanged,
-        [this, accountId](const QString &confId) {
-            auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
-            auto &callModel = accInfo.callModel;
+        [this, accountId](const QString& confId) {
+            auto& accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
+            auto& callModel = accInfo.callModel;
             auto call = callModel->getCall(confId);
             const auto convInfo = LRCInstance::getConversationFromCallId(confId);
             if (!convInfo.uid.isEmpty()) {
                 // Convert to QML
                 QVariantList map;
-                for (const auto& participant: call.participantsInfos) {
+                for (const auto& participant : call.participantsInfos) {
                     QJsonObject data;
                     data["x"] = participant["x"].toInt();
                     data["y"] = participant["y"].toInt();
@@ -307,29 +310,31 @@ CallAdapter::connectCallModel(const QString &accountId)
                     data["active"] = participant["active"] == "true";
                     auto bestName = participant["uri"];
                     data["isLocal"] = false;
-                    auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
+                    auto& accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
                     if (bestName == accInfo.profileInfo.uri) {
                         bestName = tr("me");
                         data["isLocal"] = true;
                     } else {
                         try {
-                            auto &contact = LRCInstance::getCurrentAccountInfo().contactModel->getContact(participant["uri"]);
+                            auto& contact = LRCInstance::getCurrentAccountInfo()
+                                                .contactModel->getContact(participant["uri"]);
                             bestName = Utils::bestNameForContact(contact);
-                        } catch (...) {}
+                        } catch (...) {
+                        }
                     }
                     data["bestName"] = bestName;
                     map.push_back(QVariant(data));
                 }
                 emit updateParticipantsInfos(map, accountId, confId);
             }
-    });
+        });
 
     callStatusChangedConnection_ = QObject::connect(
         accInfo.callModel.get(),
         &lrc::api::NewCallModel::callStatusChanged,
-        [this, accountId](const QString &callId) {
-            auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
-            auto &callModel = accInfo.callModel;
+        [this, accountId](const QString& callId) {
+            auto& accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
+            auto& callModel = accInfo.callModel;
             const auto call = callModel->getCall(callId);
 
             /*
@@ -357,7 +362,7 @@ CallAdapter::connectCallModel(const QString &accountId)
                  * If it's a conference, change the smartlist index
                  * to the next remaining participant.
                  */
-                bool forceCallOnly{false};
+                bool forceCallOnly {false};
                 if (!convInfo.confId.isEmpty()) {
                     auto callList = LRCInstance::getAPI().getConferenceSubcalls(convInfo.confId);
                     if (callList.empty()) {
@@ -366,7 +371,7 @@ CallAdapter::connectCallModel(const QString &accountId)
                         callList.append(lastConferencee);
                         forceCallOnly = true;
                     }
-                    for (const auto &callId : callList) {
+                    for (const auto& callId : callList) {
                         if (!callModel->hasCall(callId)) {
                             continue;
                         }
@@ -408,17 +413,28 @@ CallAdapter::connectCallModel(const QString &accountId)
         });
 }
 
+void
+CallAdapter::sipInputPanelPlayDTMF(const QString& key)
+{
+    auto callId = LRCInstance::getCallIdForConversationUid(convUid_, accountId_);
+    if (callId.isEmpty() || !LRCInstance::getCurrentCallModel()->hasCall(callId)) {
+        return;
+    }
+
+    LRCInstance::getCurrentCallModel()->playDTMF(callId, key);
+}
+
 /*
  * For Call Overlay
  */
 void
-CallAdapter::updateCallOverlay(const lrc::api::conversation::Info &convInfo)
+CallAdapter::updateCallOverlay(const lrc::api::conversation::Info& convInfo)
 {
     setTime(accountId_, convUid_);
     QObject::disconnect(oneSecondTimer_);
     QObject::connect(oneSecondTimer_, &QTimer::timeout, [this] { setTime(accountId_, convUid_); });
     oneSecondTimer_->start(20);
-    auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
+    auto& accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
 
     auto call = LRCInstance::getCallInfoForConversation(convInfo);
     if (!call) {
@@ -450,13 +466,13 @@ CallAdapter::hangupCall(const QString& uri)
         auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get();
         if (callModel->hasCall(convInfo.callId)) {
             /*
-            * Store the last remaining participant of the conference,
-            * so we can switch the smartlist index after termination.
-            */
+             * Store the last remaining participant of the conference,
+             * so we can switch the smartlist index after termination.
+             */
             if (!convInfo.confId.isEmpty()) {
                 auto callList = LRCInstance::getAPI().getConferenceSubcalls(convInfo.confId);
                 if (callList.size() == 2) {
-                    for (const auto &cId : callList) {
+                    for (const auto& cId : callList) {
                         if (cId != convInfo.callId) {
                             LRCInstance::instance().pushLastConferencee(convInfo.confId, cId);
                         }
@@ -487,21 +503,23 @@ CallAdapter::maximizeParticipant(const QString& uri, bool isActive)
     try {
         const auto call = callModel->getCall(confId);
         switch (call.layout) {
-            case lrc::api::call::Layout::GRID:
-                callModel->setActiveParticipant(confId, callId);
-                callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
-                break;
-            case lrc::api::call::Layout::ONE_WITH_SMALL:
-                callModel->setActiveParticipant(confId, callId);
-                callModel->setConferenceLayout(confId,
-                    isActive? lrc::api::call::Layout::ONE : lrc::api::call::Layout::ONE_WITH_SMALL);
-                break;
-            case lrc::api::call::Layout::ONE:
-                callModel->setActiveParticipant(confId, callId);
-                callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
-                break;
+        case lrc::api::call::Layout::GRID:
+            callModel->setActiveParticipant(confId, callId);
+            callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
+            break;
+        case lrc::api::call::Layout::ONE_WITH_SMALL:
+            callModel->setActiveParticipant(confId, callId);
+            callModel->setConferenceLayout(confId,
+                                           isActive ? lrc::api::call::Layout::ONE
+                                                    : lrc::api::call::Layout::ONE_WITH_SMALL);
+            break;
+        case lrc::api::call::Layout::ONE:
+            callModel->setActiveParticipant(confId, callId);
+            callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
+            break;
         };
-    } catch (...) {}
+    } catch (...) {
+    }
 }
 
 void
@@ -514,16 +532,17 @@ CallAdapter::minimizeParticipant()
     try {
         auto call = callModel->getCall(confId);
         switch (call.layout) {
-            case lrc::api::call::Layout::GRID:
-                break;
-            case lrc::api::call::Layout::ONE_WITH_SMALL:
-                callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
-                break;
-            case lrc::api::call::Layout::ONE:
-                callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
-                break;
+        case lrc::api::call::Layout::GRID:
+            break;
+        case lrc::api::call::Layout::ONE_WITH_SMALL:
+            callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID);
+            break;
+        case lrc::api::call::Layout::ONE:
+            callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL);
+            break;
         };
-    } catch (...) {}
+    } catch (...) {
+    }
 }
 
 void
@@ -544,11 +563,11 @@ CallAdapter::hangUpThisCall()
 bool
 CallAdapter::isRecordingThisCall()
 {
-    auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
-    auto &convModel = accInfo.conversationModel;
+    auto& accInfo = LRCInstance::accountModel().getAccountInfo(accountId_);
+    auto& convModel = accInfo.conversationModel;
     const auto convInfo = convModel->getConversationForUID(convUid_);
     return accInfo.callModel->isRecording(convInfo.confId)
-    || accInfo.callModel->isRecording(convInfo.callId);
+           || accInfo.callModel->isRecording(convInfo.callId);
 }
 
 bool
@@ -565,7 +584,8 @@ CallAdapter::isCurrentMaster() const
                 auto call = callModel->getCall(convInfo.callId);
                 return call.participantsInfos.size() == 0;
             }
-        } catch (...) {}
+        } catch (...) {
+        }
     }
     return true;
 }
@@ -580,7 +600,8 @@ CallAdapter::getCurrentLayoutType() const
         try {
             auto call = callModel->getCall(convInfo.confId);
             return Utils::toUnderlyingValue(call.layout);
-        } catch (...) {}
+        } catch (...) {
+        }
     }
     return -1;
 }
@@ -640,7 +661,7 @@ CallAdapter::videoPauseThisCallToggle()
 }
 
 void
-CallAdapter::setTime(const QString &accountId, const QString &convUid)
+CallAdapter::setTime(const QString& accountId, const QString& convUid)
 {
     const auto callId = LRCInstance::getCallIdForConversationUid(convUid, accountId);
     if (callId.isEmpty() || !LRCInstance::getCurrentCallModel()->hasCall(callId)) {
@@ -652,4 +673,4 @@ CallAdapter::setTime(const QString &accountId, const QString &convUid)
         auto timeString = LRCInstance::getCurrentCallModel()->getFormattedCallDuration(callId);
         emit updateTimeText(timeString);
     }
-}
+}
\ No newline at end of file
diff --git a/src/calladapter.h b/src/calladapter.h
index ce439731b..fbf5ffc4d 100644
--- a/src/calladapter.h
+++ b/src/calladapter.h
@@ -31,7 +31,7 @@ class CallAdapter : public QmlAdapterBase
     Q_OBJECT
 
 public:
-    explicit CallAdapter(QObject *parent = nullptr);
+    explicit CallAdapter(QObject* parent = nullptr);
     ~CallAdapter();
 
     /*
@@ -41,11 +41,12 @@ public:
 
     Q_INVOKABLE void placeAudioOnlyCall();
     Q_INVOKABLE void placeCall();
-    Q_INVOKABLE void hangUpACall(const QString &accountId, const QString &convUid);
-    Q_INVOKABLE void refuseACall(const QString &accountId, const QString &convUid);
-    Q_INVOKABLE void acceptACall(const QString &accountId, const QString &convUid);
+    Q_INVOKABLE void hangUpACall(const QString& accountId, const QString& convUid);
+    Q_INVOKABLE void refuseACall(const QString& accountId, const QString& convUid);
+    Q_INVOKABLE void acceptACall(const QString& accountId, const QString& convUid);
 
-    Q_INVOKABLE void connectCallModel(const QString &accountId);
+    Q_INVOKABLE void connectCallModel(const QString& accountId);
+    Q_INVOKABLE void sipInputPanelPlayDTMF(const QString& key);
 
     /*
      * For Call Overlay
@@ -55,7 +56,7 @@ public:
     Q_INVOKABLE void minimizeParticipant();
     Q_INVOKABLE void hangUpThisCall();
     Q_INVOKABLE bool isCurrentMaster() const;
-    Q_INVOKABLE int  getCurrentLayoutType() const;
+    Q_INVOKABLE int getCurrentLayoutType() const;
     Q_INVOKABLE void holdThisCallToggle();
     Q_INVOKABLE void muteThisCallToggle();
     Q_INVOKABLE void recordThisCallToggle();
@@ -64,24 +65,26 @@ public:
     Q_INVOKABLE QVariantList getConferencesInfos();
 
 signals:
-    void showOutgoingCallPage(const QString &accountId, const QString &convUid);
-    void showIncomingCallPage(const QString &accountId, const QString &convUid);
-    void showAudioCallPage(const QString &accountId, const QString &convUid);
-    void showVideoCallPage(const QString &accountId, const QString &convUid, const QString &callId);
-    void showCallStack(const QString &accountId, const QString &convUid, bool forceReset = false);
-    void closeCallStack(const QString &accountId, const QString &convUid);
-    void closePotentialIncomingCallPageWindow(const QString &accountId, const QString &convUid);
-    void callStatusChanged(const QString &status, const QString &accountId, const QString &convUid);
+    void showOutgoingCallPage(const QString& accountId, const QString& convUid);
+    void showIncomingCallPage(const QString& accountId, const QString& convUid);
+    void showAudioCallPage(const QString& accountId, const QString& convUid);
+    void showVideoCallPage(const QString& accountId, const QString& convUid, const QString& callId);
+    void showCallStack(const QString& accountId, const QString& convUid, bool forceReset = false);
+    void closeCallStack(const QString& accountId, const QString& convUid);
+    void closePotentialIncomingCallPageWindow(const QString& accountId, const QString& convUid);
+    void callStatusChanged(const QString& status, const QString& accountId, const QString& convUid);
     void updateConversationSmartList();
-    void updateParticipantsInfos(const QVariantList& infos, const QString &accountId, const QString &callId);
+    void updateParticipantsInfos(const QVariantList& infos,
+                                 const QString& accountId,
+                                 const QString& callId);
 
-    void incomingCallNeedToSetupMainView(const QString &accountId, const QString &convUid);
+    void incomingCallNeedToSetupMainView(const QString& accountId, const QString& convUid);
     void previewVisibilityNeedToChange(bool visible);
 
     /*
      * For Call Overlay
      */
-    void updateTimeText(const QString &time);
+    void updateTimeText(const QString& time);
     void showOnHoldLabel(bool isPaused);
     void updateOverlay(bool isPaused,
                        bool isAudioOnly,
@@ -90,17 +93,17 @@ signals:
                        bool isRecording,
                        bool isSIP,
                        bool isConferenceCall,
-                       const QString &bestName);
+                       const QString& bestName);
 
 public slots:
-    void slotShowIncomingCallView(const QString &accountId,
-                                  const lrc::api::conversation::Info &convInfo);
-    void slotShowCallView(const QString &accountId, const lrc::api::conversation::Info &convInfo);
+    void slotShowIncomingCallView(const QString& accountId,
+                                  const lrc::api::conversation::Info& convInfo);
+    void slotShowCallView(const QString& accountId, const lrc::api::conversation::Info& convInfo);
     void slotAccountChanged();
 
 private:
-    void updateCall(const QString &convUid = {},
-                    const QString &accountId = {},
+    void updateCall(const QString& convUid = {},
+                    const QString& accountId = {},
                     bool forceCallOnly = false);
     bool shouldShowPreview(bool force);
 
@@ -117,7 +120,7 @@ private:
     /*
      * For Call Overlay
      */
-    void updateCallOverlay(const lrc::api::conversation::Info &convInfo);
-    void setTime(const QString &accountId, const QString &convUid);
-    QTimer *oneSecondTimer_;
+    void updateCallOverlay(const lrc::api::conversation::Info& convInfo);
+    void setTime(const QString& accountId, const QString& convUid);
+    QTimer* oneSecondTimer_;
 };
diff --git a/src/commoncomponents/HoverableButton.qml b/src/commoncomponents/HoverableButton.qml
index 4e96677d2..0668487b9 100644
--- a/src/commoncomponents/HoverableButton.qml
+++ b/src/commoncomponents/HoverableButton.qml
@@ -28,6 +28,8 @@ import QtGraphicalEffects 1.15
  * 2. Radius control (rounded)
  * 3. Text content or image content
  * 4. Can use OnClicked slot to implement some click logic
+ *
+ * Note: if use text property directly, buttonTextColor will not work.
  */
 Button {
     id: hoverableButton
@@ -39,6 +41,9 @@ Button {
     property int buttonImageHeight: hoverableButtonBackground.height - 10
     property int buttonImageWidth: hoverableButtonBackground.width - 10
 
+    property string buttonText: ""
+    property string buttonTextColor: "black"
+
     property string backgroundColor: JamiTheme.releaseColor
     property string onPressColor: JamiTheme.pressColor
     property string onReleaseColor: JamiTheme.releaseColor
@@ -58,6 +63,8 @@ Button {
 
     hoverEnabled: true
 
+    text: "<font color=" + "'" + buttonTextColor + "'>" + buttonText + "</font>"
+
     ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
     ToolTip.visible: hovered && (toolTipText.length > 0)
     ToolTip.text: toolTipText
@@ -79,7 +86,12 @@ Button {
             mipmap: true
             asynchronous: true
 
-            source: hoverableButton.checked && checkedImage? checkedImage : baseImage
+            source: {
+                if (checkable && checkedImage)
+                    return hoverableButton.checked ? checkedImage : baseImage
+                else
+                    return ""
+            }
 
             layer {
                 enabled: true
diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml
index a29330de1..d9086c69c 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -68,6 +68,9 @@ Item {
 
     property string draftRed: "#cf5300"
 
+    property string sipInputButtonBackgroundColor: "#336699"
+    property string sipInputButtonHoverColor: "#4477aa"
+    property string sipInputButtonPressColor: "#5588bb"
 
     /*
      * Font.
diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml
index 75774d34f..1f2ed4ede 100644
--- a/src/mainview/components/CallOverlay.qml
+++ b/src/mainview/components/CallOverlay.qml
@@ -110,6 +110,12 @@ Rectangle {
 
     anchors.fill: parent
 
+    SipInputPanel {
+        id: sipInputPanel
+
+        x: callOverlayRect.width / 2 - sipInputPanel.width / 2
+        y: callOverlayRect.height / 2 - sipInputPanel.height / 2
+    }
 
     /*
      * Timer to decide when overlay fade out.
diff --git a/src/mainview/components/CallOverlayButtonGroup.qml b/src/mainview/components/CallOverlayButtonGroup.qml
index c360a7cda..107ab9e25 100644
--- a/src/mainview/components/CallOverlayButtonGroup.qml
+++ b/src/mainview/components/CallOverlayButtonGroup.qml
@@ -50,8 +50,6 @@ Rectangle {
         root.isSip = isSIP
         noVideoButton.visible = !isAudioOnly
         addToConferenceButton.visible = !isSIP && isMaster
-        transferCallButton.visible = isSIP
-        sipInputPanelButton.visible = isSIP
 
         noMicButton.checked = isAudioMuted
         noVideoButton.checked = isVideoMuted
diff --git a/src/mainview/components/CallViewContextMenu.qml b/src/mainview/components/CallViewContextMenu.qml
index 23da3fd70..2deeeaac2 100644
--- a/src/mainview/components/CallViewContextMenu.qml
+++ b/src/mainview/components/CallViewContextMenu.qml
@@ -112,6 +112,23 @@ Menu {
         }
     }
 
+    GeneralMenuItem {
+        id: showSipInputPanelButton
+
+        visible: isSIP
+        height: isSIP? undefined : 0
+
+        itemName: qsTr("Sip Input Panel")
+        iconSource: "qrc:/images/icons/ic_keypad.svg"
+        leftBorderWidth: commonBorderWidth
+        rightBorderWidth: commonBorderWidth
+
+        onClicked: {
+            root.close()
+            sipInputPanel.open()
+        }
+    }
+
     GeneralMenuItem {
         id: transferCallButton
 
diff --git a/src/mainview/components/SipInputPanel.qml b/src/mainview/components/SipInputPanel.qml
new file mode 100644
index 000000000..8d547a26a
--- /dev/null
+++ b/src/mainview/components/SipInputPanel.qml
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@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
+ * 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 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+import QtQuick.Controls.Universal 2.12
+import net.jami.Models 1.0
+
+import "../../commoncomponents"
+
+/*
+ * SipInputPanel is a key pad that is designed to be
+ * used in sip calls.
+ */
+Popup {
+    id: sipInputPanelPopUp
+
+    /*
+     * Space between sipInputPanelRect and grid layout
+     */
+    property int sipPanelPadding: 20
+
+    contentWidth: sipInputPanelRectGridLayout.implicitWidth + 20
+    contentHeight: sipInputPanelRectGridLayout.implicitHeight + 20
+
+    padding: 0
+
+    modal: true
+
+    contentItem: Rectangle {
+        id: sipInputPanelRect
+
+        radius: 10
+
+        GridLayout {
+            id: sipInputPanelRectGridLayout
+
+            anchors.centerIn: parent
+
+            columns: 4
+
+            Repeater {
+                id: sipInputPanelRectGridLayoutRepeater
+                model: ["1", "2", "3", "A", "4", "5", "6", "B", "7",
+                        "8", "9", "C", "*", "0", "#", "D"]
+
+                HoverableButton {
+                    id: sipInputPanelButton
+
+                    Layout.preferredWidth: 30
+                    Layout.preferredHeight: 30
+
+                    radius: 30
+                    buttonText: modelData
+                    buttonTextColor: "white"
+                    checkable: false
+                    backgroundColor: JamiTheme.sipInputButtonBackgroundColor
+                    onEnterColor: JamiTheme.sipInputButtonHoverColor
+                    onExitColor: JamiTheme.sipInputButtonBackgroundColor
+                    onPressColor: JamiTheme.sipInputButtonPressColor
+                    onReleaseColor: JamiTheme.sipInputButtonHoverColor
+
+                    toolTipText: modelData
+
+                    onClicked: {
+                        CallAdapter.sipInputPanelPlayDTMF(modelData)
+                    }
+                }
+            }
+        }
+    }
+
+    background: Rectangle {
+        color: "transparent"
+    }
+}
-- 
GitLab