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"
+ }
+}