From 25f85712d732c6d824db939b5465dacb2efa982e Mon Sep 17 00:00:00 2001 From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> Date: Fri, 14 May 2021 16:53:57 -0400 Subject: [PATCH] calladapter: add calloverlay model Introduce a model to manage the overlay logic. It is not used in this commit. Gitlab: #411 Change-Id: I6a666fe00e7f66c7e217ae1f77360327e83fdcd6 --- CMakeLists.txt | 6 +- src/calladapter.cpp | 262 +++++++++++++++++++++------------------ src/calladapter.h | 6 + src/calloverlaymodel.cpp | 192 ++++++++++++++++++++++++++++ src/calloverlaymodel.h | 117 +++++++++++++++++ 5 files changed, 462 insertions(+), 121 deletions(-) create mode 100644 src/calloverlaymodel.cpp create mode 100644 src/calloverlaymodel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ab6a09bd..b4afcb941 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,7 +71,8 @@ set(COMMON_SOURCES ${SRC_DIR}/selectablelistproxymodel.cpp ${SRC_DIR}/conversationlistmodelbase.cpp ${SRC_DIR}/conversationlistmodel.cpp - ${SRC_DIR}/searchresultslistmodel.cpp) + ${SRC_DIR}/searchresultslistmodel.cpp + ${SRC_DIR}/calloverlaymodel.cpp) set(COMMON_HEADERS ${SRC_DIR}/avatarimageprovider.h @@ -126,7 +127,8 @@ set(COMMON_HEADERS ${SRC_DIR}/selectablelistproxymodel.h ${SRC_DIR}/conversationlistmodelbase.h ${SRC_DIR}/conversationlistmodel.h - ${SRC_DIR}/searchresultslistmodel.h) + ${SRC_DIR}/searchresultslistmodel.h + ${SRC_DIR}/calloverlaymodel.h) set(QML_LIBS Qt5::Quick diff --git a/src/calladapter.cpp b/src/calladapter.cpp index adbaed90b..22fc61bb6 100644 --- a/src/calladapter.cpp +++ b/src/calladapter.cpp @@ -26,6 +26,7 @@ #include "systemtray.h" #include "utils.h" +#include "qmlregister.h" #include <QApplication> #include <QTimer> @@ -35,6 +36,9 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject* : QmlAdapterBase(instance, parent) , systemTray_(systemTray) { + overlayModel_.reset(new CallOverlayModel(lrcInstance_, this)); + QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, overlayModel_.get(), "CallOverlayModel"); + accountId_ = lrcInstance_->getCurrentAccountId(); if (!accountId_.isEmpty()) connectCallModel(accountId_); @@ -75,7 +79,7 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject* connect(&lrcInstance_->behaviorController(), &BehaviorController::callStatusChanged, this, - &CallAdapter::onCallStatusChanged); + QOverload<const QString&, const QString&>::of(&CallAdapter::onCallStatusChanged)); } void @@ -129,6 +133,129 @@ CallAdapter::onCallStatusChanged(const QString& accountId, const QString& callId #endif } +void +CallAdapter::onParticipantsChanged(const QString& confId) +{ + auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); + auto& callModel = accInfo.callModel; + try { + auto call = callModel->getCall(confId); + const auto& convInfo = lrcInstance_->getConversationFromCallId(confId); + if (!convInfo.uid.isEmpty()) { + QVariantList map; + for (const auto& participant : call.participantsInfos) { + QJsonObject data = fillParticipantData(participant); + map.push_back(QVariant(data)); + updateCallOverlay(convInfo); + } + Q_EMIT updateParticipantsInfos(map, accountId_, confId); + } + } catch (...) { + } +} + +void +CallAdapter::onCallStatusChanged(const QString& callId, int code) +{ + Q_UNUSED(code) + + auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); + auto& callModel = accInfo.callModel; + + try { + const auto call = callModel->getCall(callId); + /* + * Change status label text. + */ + const auto& convInfo = lrcInstance_->getConversationFromCallId(callId); + if (!convInfo.uid.isEmpty()) { + Q_EMIT callStatusChanged(static_cast<int>(call.status), accountId_, convInfo.uid); + updateCallOverlay(convInfo); + } + + switch (call.status) { + case lrc::api::call::Status::INVALID: + case lrc::api::call::Status::INACTIVE: + case lrc::api::call::Status::ENDED: + case lrc::api::call::Status::PEER_BUSY: + case lrc::api::call::Status::TIMEOUT: + case lrc::api::call::Status::TERMINATING: { + lrcInstance_->renderer()->removeDistantRenderer(callId); + Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId_); + if (convInfo.uid.isEmpty()) { + break; + } + /* + * If it's a conference, change the smartlist index + * to the next remaining participant. + */ + bool forceCallOnly {false}; + if (!convInfo.confId.isEmpty()) { + auto callList = lrcInstance_->getConferenceSubcalls(convInfo.confId); + if (callList.empty()) { + auto lastConference = lrcInstance_->poplastConference(convInfo.confId); + if (!lastConference.isEmpty()) { + callList.append(lastConference); + forceCallOnly = true; + } + } + if (callList.isEmpty()) { + callList = lrcInstance_->getActiveCalls(); + forceCallOnly = true; + } + for (const auto& callId : callList) { + if (!callModel->hasCall(callId)) { + continue; + } + auto currentCall = callModel->getCall(callId); + if (currentCall.status == lrc::api::call::Status::IN_PROGRESS) { + const auto& otherConv = lrcInstance_->getConversationFromCallId(callId); + if (!otherConv.uid.isEmpty() && otherConv.uid != convInfo.uid) { + /* + * Reset the call view corresponding accountId, uid. + */ + lrcInstance_->selectConversation(otherConv.uid); + updateCall(otherConv.uid, otherConv.accountId, forceCallOnly); + } + } + } + + return; + } + preventScreenSaver(false); + break; + } + case lrc::api::call::Status::CONNECTED: + case lrc::api::call::Status::IN_PROGRESS: { + const auto& convInfo = lrcInstance_->getConversationFromCallId(callId, accountId_); + if (!convInfo.uid.isEmpty() && convInfo.uid == lrcInstance_->get_selectedConvUid()) { + accInfo.conversationModel->selectConversation(convInfo.uid); + } + updateCall(convInfo.uid, accountId_); + preventScreenSaver(true); + break; + } + case lrc::api::call::Status::PAUSED: + updateCall(); + default: + break; + } + } catch (...) { + } +} + +void +CallAdapter::onRemoteRecordingChanged(const QString& callId, + const QSet<QString>& peerRec, + bool state) +{ + Q_UNUSED(peerRec) + Q_UNUSED(state) + const auto currentCallId = lrcInstance_->getCallIdForConversationUid(convUid_, accountId_); + if (callId == currentCallId) + updateRecordingPeers(); +} + void CallAdapter::placeAudioOnlyCall() { @@ -429,126 +556,23 @@ CallAdapter::connectCallModel(const QString& accountId) { auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); - connect( - accInfo.callModel.get(), - &lrc::api::NewCallModel::onParticipantsChanged, - this, - [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()) { - QVariantList map; - for (const auto& participant : call.participantsInfos) { - QJsonObject data = fillParticipantData(participant); - map.push_back(QVariant(data)); - updateCallOverlay(convInfo); - } - Q_EMIT updateParticipantsInfos(map, accountId, confId); - } - }, - Qt::UniqueConnection); - - connect( - accInfo.callModel.get(), - &lrc::api::NewCallModel::callStatusChanged, - this, - [this, accountId](const QString& callId) { - auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); - auto& callModel = accInfo.callModel; - const auto call = callModel->getCall(callId); - - /* - * Change status label text. - */ - const auto& convInfo = lrcInstance_->getConversationFromCallId(callId); - if (!convInfo.uid.isEmpty()) { - Q_EMIT callStatusChanged(static_cast<int>(call.status), accountId, convInfo.uid); - updateCallOverlay(convInfo); - } + connect(accInfo.callModel.get(), + &NewCallModel::onParticipantsChanged, + this, + &CallAdapter::onParticipantsChanged, + Qt::UniqueConnection); - switch (call.status) { - case lrc::api::call::Status::INVALID: - case lrc::api::call::Status::INACTIVE: - case lrc::api::call::Status::ENDED: - case lrc::api::call::Status::PEER_BUSY: - case lrc::api::call::Status::TIMEOUT: - case lrc::api::call::Status::TERMINATING: { - lrcInstance_->renderer()->removeDistantRenderer(callId); - Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId); - if (convInfo.uid.isEmpty()) { - break; - } - /* - * If it's a conference, change the smartlist index - * to the next remaining participant. - */ - bool forceCallOnly {false}; - if (!convInfo.confId.isEmpty()) { - auto callList = lrcInstance_->getConferenceSubcalls(convInfo.confId); - if (callList.empty()) { - auto lastConference = lrcInstance_->poplastConference(convInfo.confId); - if (!lastConference.isEmpty()) { - callList.append(lastConference); - forceCallOnly = true; - } - } - if (callList.isEmpty()) { - callList = lrcInstance_->getActiveCalls(); - forceCallOnly = true; - } - for (const auto& callId : callList) { - if (!callModel->hasCall(callId)) { - continue; - } - auto currentCall = callModel->getCall(callId); - if (currentCall.status == lrc::api::call::Status::IN_PROGRESS) { - const auto& otherConv = lrcInstance_->getConversationFromCallId(callId); - if (!otherConv.uid.isEmpty() && otherConv.uid != convInfo.uid) { - /* - * Reset the call view corresponding accountId, uid. - */ - lrcInstance_->selectConversation(otherConv.uid); - updateCall(otherConv.uid, otherConv.accountId, forceCallOnly); - } - } - } + connect(accInfo.callModel.get(), + &NewCallModel::callStatusChanged, + this, + QOverload<const QString&, int>::of(&CallAdapter::onCallStatusChanged), + Qt::UniqueConnection); - return; - } - preventScreenSaver(false); - break; - } - case lrc::api::call::Status::CONNECTED: - case lrc::api::call::Status::IN_PROGRESS: { - const auto& convInfo = lrcInstance_->getConversationFromCallId(callId, accountId); - if (!convInfo.uid.isEmpty() && convInfo.uid == lrcInstance_->get_selectedConvUid()) { - accInfo.conversationModel->selectConversation(convInfo.uid); - } - updateCall(convInfo.uid, accountId); - preventScreenSaver(true); - break; - } - case lrc::api::call::Status::PAUSED: - updateCall(); - default: - break; - } - }, - Qt::UniqueConnection); - - connect( - accInfo.callModel.get(), - &lrc::api::NewCallModel::remoteRecordingChanged, - this, - [this](const QString& callId, const QSet<QString>&, bool) { - const auto currentCallId = lrcInstance_->getCallIdForConversationUid(convUid_, - accountId_); - if (callId == currentCallId) - updateRecordingPeers(); - }, - Qt::UniqueConnection); + connect(accInfo.callModel.get(), + &NewCallModel::remoteRecordingChanged, + this, + &CallAdapter::onRemoteRecordingChanged, + Qt::UniqueConnection); } void diff --git a/src/calladapter.h b/src/calladapter.h index 2c4a363da..bdd7015d7 100644 --- a/src/calladapter.h +++ b/src/calladapter.h @@ -22,6 +22,7 @@ #include "lrcinstance.h" #include "qmladapterbase.h" #include "screensaver.h" +#include "calloverlaymodel.h" #include <QObject> #include <QString> @@ -104,6 +105,9 @@ public Q_SLOTS: void onShowCallView(const QString& accountId, const QString& convUid); void onAccountChanged(); void onCallStatusChanged(const QString& accountId, const QString& callId); + void onParticipantsChanged(const QString& confId); + void onCallStatusChanged(const QString& callId, int code); + void onRemoteRecordingChanged(const QString& callId, const QSet<QString>& peerRec, bool state); private: void updateRecordingPeers(bool eraseLabelOnEmpty = false); @@ -122,4 +126,6 @@ private: void preventScreenSaver(bool state); SystemTray* systemTray_; + + QScopedPointer<CallOverlayModel> overlayModel_; }; diff --git a/src/calloverlaymodel.cpp b/src/calloverlaymodel.cpp new file mode 100644 index 000000000..aae7c7520 --- /dev/null +++ b/src/calloverlaymodel.cpp @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2021 by Savoir-faire Linux + * Author: Andreas Traczyk <andreas.traczyk@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 <http://www.gnu.org/licenses/>. + */ + +#include "calloverlaymodel.h" + +CallControlListModel::CallControlListModel(QObject* parent) + : QAbstractListModel(parent) +{} + +int +CallControlListModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return data_.size(); +} + +QVariant +CallControlListModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + using namespace CallControl; + auto item = data_.at(index.row()); + + switch (role) { + case Role::DummyRole: + break; +#define X(t, role) \ + case Role::role: \ + return QVariant::fromValue(item.role); + CC_ROLES +#undef X + default: + break; + } + return QVariant(); +} + +QHash<int, QByteArray> +CallControlListModel::roleNames() const +{ + using namespace CallControl; + QHash<int, QByteArray> roles; +#define X(t, role) roles[role] = #role; + CC_ROLES +#undef X + return roles; +} + +void +CallControlListModel::setBadgeCount(int row, int count) +{ + if (row >= rowCount()) + return; + data_[row].BadgeCount = count; + auto idx = index(row, 0); + Q_EMIT dataChanged(idx, idx); +} + +void +CallControlListModel::addItem(const CallControl::Item& item) +{ + beginResetModel(); + data_.append(item); + endResetModel(); +} + +IndexRangeFilterProxyModel::IndexRangeFilterProxyModel(QAbstractListModel* parent) + : QSortFilterProxyModel(parent) +{ + setSourceModel(parent); + sourceModel()->data(QModelIndex()); +} + +bool +IndexRangeFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const +{ + auto index = sourceModel()->index(sourceRow, 0, sourceParent); + bool predicate = true; + if (filterRole() != Qt::DisplayRole) { + predicate = sourceModel()->data(index, filterRole()).toInt() != 0; + } + return sourceRow <= max_ && sourceRow >= min_ && predicate; +} + +void +IndexRangeFilterProxyModel::setRange(int min, int max) +{ + min_ = min; + max_ = max; + invalidateFilter(); +} + +CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent) + : QObject(parent) + , lrcInstance_(instance) + , primaryModel_(new CallControlListModel(this)) + , secondaryModel_(new CallControlListModel(this)) + , overflowModel_(new IndexRangeFilterProxyModel(secondaryModel_)) + , overflowVisibleModel_(new IndexRangeFilterProxyModel(secondaryModel_)) + , overflowHiddenModel_(new IndexRangeFilterProxyModel(secondaryModel_)) +{ + connect(this, + &CallOverlayModel::overflowIndexChanged, + this, + &CallOverlayModel::setControlRanges); + overflowVisibleModel_->setFilterRole(CallControl::Role::BadgeCount); +} + +void +CallOverlayModel::addPrimaryControl(const QVariantMap& props) +{ + CallControl::Item item { +#define X(t, role) props[#role].value<t>(), + CC_ROLES +#undef X + }; + primaryModel_->addItem(item); +} + +void +CallOverlayModel::addSecondaryControl(const QVariantMap& props) +{ + CallControl::Item item { +#define X(t, role) props[#role].value<t>(), + CC_ROLES +#undef X + }; + secondaryModel_->addItem(item); + setControlRanges(); +} + +void +CallOverlayModel::setBadgeCount(int row, int count) +{ + secondaryModel_->setBadgeCount(row, count); +} + +QVariant +CallOverlayModel::primaryModel() +{ + return QVariant::fromValue(primaryModel_); +} + +QVariant +CallOverlayModel::secondaryModel() +{ + return QVariant::fromValue(secondaryModel_); +} + +QVariant +CallOverlayModel::overflowModel() +{ + return QVariant::fromValue(overflowModel_); +} + +QVariant +CallOverlayModel::overflowVisibleModel() +{ + return QVariant::fromValue(overflowVisibleModel_); +} + +QVariant +CallOverlayModel::overflowHiddenModel() +{ + return QVariant::fromValue(overflowHiddenModel_); +} + +void +CallOverlayModel::setControlRanges() +{ + overflowModel_->setRange(0, overflowIndex_ - 1); + overflowVisibleModel_->setRange(overflowIndex_, secondaryModel_->rowCount()); + overflowHiddenModel_->setRange(overflowIndex_, secondaryModel_->rowCount()); +} diff --git a/src/calloverlaymodel.h b/src/calloverlaymodel.h new file mode 100644 index 000000000..f3f70fb1f --- /dev/null +++ b/src/calloverlaymodel.h @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2021 by Savoir-faire Linux + * Author: Andreas Traczyk <andreas.traczyk@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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "lrcinstance.h" +#include "qtutils.h" + +#include <QAbstractListModel> +#include <QObject> +#include <QQmlEngine> +#include <QSortFilterProxyModel> +#include <QDebug> + +#define CC_ROLES \ + X(QObject*, ItemAction) \ + X(int, BadgeCount) \ + X(bool, HasBackground) \ + X(QObject*, MenuAction) \ + X(QString, Name) + +namespace CallControl { +Q_NAMESPACE +enum Role { + DummyRole = Qt::UserRole + 1, +#define X(t, role) role, + CC_ROLES +#undef X +}; +Q_ENUM_NS(Role) + +struct Item +{ +#define X(t, role) t role; + CC_ROLES +#undef X +}; +} // namespace CallControl + +class CallControlListModel : public QAbstractListModel +{ + Q_OBJECT +public: + CallControlListModel(QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + void setBadgeCount(int row, int count); + void addItem(const CallControl::Item& item); + +private: + QList<CallControl::Item> data_; +}; + +class IndexRangeFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit IndexRangeFilterProxyModel(QAbstractListModel* parent = nullptr); + + virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + + void setRange(int min, int max); + +private: + int min_ {-1}; + int max_ {-1}; +}; + +class CallOverlayModel : public QObject +{ + Q_OBJECT + QML_PROPERTY(int, overflowIndex) + +public: + CallOverlayModel(LRCInstance* instance, QObject* parent = nullptr); + + Q_INVOKABLE void addPrimaryControl(const QVariantMap& props); + Q_INVOKABLE void addSecondaryControl(const QVariantMap& props); + Q_INVOKABLE void setBadgeCount(int row, int count); + + Q_INVOKABLE QVariant primaryModel(); + Q_INVOKABLE QVariant secondaryModel(); + Q_INVOKABLE QVariant overflowModel(); + Q_INVOKABLE QVariant overflowVisibleModel(); + Q_INVOKABLE QVariant overflowHiddenModel(); + +private Q_SLOTS: + void setControlRanges(); + +private: + LRCInstance* lrcInstance_ {nullptr}; + + CallControlListModel* primaryModel_; + CallControlListModel* secondaryModel_; + IndexRangeFilterProxyModel* overflowModel_; + IndexRangeFilterProxyModel* overflowVisibleModel_; + IndexRangeFilterProxyModel* overflowHiddenModel_; +}; -- GitLab