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