mainview: add sip input panel

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

Gitlab: #18
Change-Id: Iaa53ae2b34d9ce0d5cf19aa82dd41a5607203c62
diff --git a/src/calladapter.cpp b/src/calladapter.cpp
index efc7410..c4badc9 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 @@
 }
 
 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 @@
 }
 
 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 @@
 }
 
 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 @@
         /*
          * 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 @@
 }
 
 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 @@
 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 @@
     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 @@
                 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 @@
     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 @@
                     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 @@
                  * 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 @@
                         callList.append(lastConferencee);
                         forceCallOnly = true;
                     }
-                    for (const auto &callId : callList) {
+                    for (const auto& callId : callList) {
                         if (!callModel->hasCall(callId)) {
                             continue;
                         }
@@ -408,17 +413,28 @@
         });
 }
 
+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 @@
         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 @@
     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 @@
     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 @@
 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 @@
                 auto call = callModel->getCall(convInfo.callId);
                 return call.participantsInfos.size() == 0;
             }
-        } catch (...) {}
+        } catch (...) {
+        }
     }
     return true;
 }
@@ -580,7 +600,8 @@
         try {
             auto call = callModel->getCall(convInfo.confId);
             return Utils::toUnderlyingValue(call.layout);
-        } catch (...) {}
+        } catch (...) {
+        }
     }
     return -1;
 }
@@ -640,7 +661,7 @@
 }
 
 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 @@
         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 ce43973..fbf5ffc 100644
--- a/src/calladapter.h
+++ b/src/calladapter.h
@@ -31,7 +31,7 @@
     Q_OBJECT
 
 public:
-    explicit CallAdapter(QObject *parent = nullptr);
+    explicit CallAdapter(QObject* parent = nullptr);
     ~CallAdapter();
 
     /*
@@ -41,11 +41,12 @@
 
     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 @@
     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 @@
     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 @@
                        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 @@
     /*
      * 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 4e96677..0668487 100644
--- a/src/commoncomponents/HoverableButton.qml
+++ b/src/commoncomponents/HoverableButton.qml
@@ -28,6 +28,8 @@
  * 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 @@
     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 @@
 
     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 @@
             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 a29330d..d9086c6 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -68,6 +68,9 @@
 
     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 75774d3..1f2ed4e 100644
--- a/src/mainview/components/CallOverlay.qml
+++ b/src/mainview/components/CallOverlay.qml
@@ -110,6 +110,12 @@
 
     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 c360a7c..107ab9e 100644
--- a/src/mainview/components/CallOverlayButtonGroup.qml
+++ b/src/mainview/components/CallOverlayButtonGroup.qml
@@ -50,8 +50,6 @@
         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 23da3fd..2deeeaa 100644
--- a/src/mainview/components/CallViewContextMenu.qml
+++ b/src/mainview/components/CallViewContextMenu.qml
@@ -113,6 +113,23 @@
     }
 
     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
 
         visible: isSIP
diff --git a/src/mainview/components/SipInputPanel.qml b/src/mainview/components/SipInputPanel.qml
new file mode 100644
index 0000000..8d547a2
--- /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"
+    }
+}