From 66edc82b823f104a83972e3c9a6a854fb7c8f786 Mon Sep 17 00:00:00 2001
From: Ming Rui Zhang <mingrui.zhang@savoirfairelinux.com>
Date: Wed, 11 Aug 2021 14:08:34 -0400
Subject: [PATCH] settingsview: LinkedDevices view refactor

1. Remove deviceModel from AccountAdapter
2. Use QSortFilterProxyModel to make sure that
   current device is always on top and the rest are
   in alphabetical order
3. Elide device name properly in DeviceItemDelegate

Change-Id: Ic66a11fb4a4a8cca65916653e0981b15c939bce6
---
 src/accountadapter.cpp                        |  6 --
 src/accountadapter.h                          |  3 -
 src/constant/JamiStrings.qml                  |  3 +
 src/deviceitemlistmodel.cpp                   | 49 +++++++++++-
 src/deviceitemlistmodel.h                     | 72 +++++++++++++++---
 src/qmlregister.cpp                           |  7 +-
 src/settingsadapter.cpp                       |  5 +-
 src/settingsview/SettingsView.qml             |  2 -
 .../components/CurrentAccountSettings.qml     | 11 ---
 .../components/DeviceItemDelegate.qml         | 75 ++++++++++++-------
 src/settingsview/components/LinkedDevices.qml | 57 +++-----------
 11 files changed, 175 insertions(+), 115 deletions(-)

diff --git a/src/accountadapter.cpp b/src/accountadapter.cpp
index f81675974..42f93be9b 100644
--- a/src/accountadapter.cpp
+++ b/src/accountadapter.cpp
@@ -54,12 +54,6 @@ AccountAdapter::getModel()
     return &(lrcInstance_->accountModel());
 }
 
-NewDeviceModel*
-AccountAdapter::getDeviceModel()
-{
-    return lrcInstance_->getCurrentAccountInfo().deviceModel.get();
-}
-
 void
 AccountAdapter::changeAccount(int row)
 {
diff --git a/src/accountadapter.h b/src/accountadapter.h
index c41dfc12c..1096439b5 100644
--- a/src/accountadapter.h
+++ b/src/accountadapter.h
@@ -34,15 +34,12 @@ class AccountAdapter final : public QmlAdapterBase
     Q_OBJECT
 
     Q_PROPERTY(lrc::api::NewAccountModel* model READ getModel NOTIFY modelChanged)
-    Q_PROPERTY(lrc::api::NewDeviceModel* deviceModel READ getDeviceModel NOTIFY deviceModelChanged)
 
 public:
     lrc::api::NewAccountModel* getModel();
-    lrc::api::NewDeviceModel* getDeviceModel();
 
 Q_SIGNALS:
     void modelChanged();
-    void deviceModelChanged();
 
 public:
     explicit AccountAdapter(AppSettingsManager* settingsManager,
diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index d918879e0..58553511a 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -312,6 +312,9 @@ Item {
     // LinkedDevices
     property string tipLinkNewDevice: qsTr("Link a new device to this account")
     property string linkAnotherDevice: qsTr("Link another device")
+    property string removeDevice: qsTr("Remove Device")
+    property string sureToRemoveDevice: qsTr("Are you sure you wish to remove this device?")
+    property string linkedDevices: qsTr("Linked Devices")
 
     // BannedContacts
     property string tipBannedContacts: qsTr("Display or hide banned contacts")
diff --git a/src/deviceitemlistmodel.cpp b/src/deviceitemlistmodel.cpp
index fc2d3e410..105de9585 100644
--- a/src/deviceitemlistmodel.cpp
+++ b/src/deviceitemlistmodel.cpp
@@ -1,6 +1,7 @@
 /*
- * Copyright (C) 2019-2020 by Savoir-faire Linux
+ * Copyright (C) 2021 by Savoir-faire Linux
  * Author: Yang Wang   <yang.wang@savoirfairelinux.com>
+ * 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
@@ -27,7 +28,19 @@
 
 DeviceItemListModel::DeviceItemListModel(QObject* parent)
     : AbstractListModelBase(parent)
-{}
+{
+    connect(this, &AbstractListModelBase::lrcInstanceChanged, [this] {
+        if (lrcInstance_) {
+            if (!lrcInstance_->get_currentAccountId().isEmpty())
+                onAccountChanged();
+            connect(lrcInstance_,
+                    &LRCInstance::currentAccountIdChanged,
+                    this,
+                    &DeviceItemListModel::onAccountChanged,
+                    Qt::UniqueConnection);
+        }
+    });
+}
 
 DeviceItemListModel::~DeviceItemListModel() {}
 
@@ -122,3 +135,35 @@ DeviceItemListModel::reset()
     beginResetModel();
     endResetModel();
 }
+
+void
+DeviceItemListModel::revokeDevice(QString deviceId, QString password)
+{
+    lrcInstance_->getCurrentAccountInfo().deviceModel->revokeDevice(deviceId, password);
+}
+
+void
+DeviceItemListModel::onAccountChanged()
+{
+    reset();
+
+    auto* deviceModel = lrcInstance_->getCurrentAccountInfo().deviceModel.get();
+
+    connect(deviceModel,
+            &lrc::api::NewDeviceModel::deviceAdded,
+            this,
+            &DeviceItemListModel::reset,
+            Qt::UniqueConnection);
+
+    connect(deviceModel,
+            &lrc::api::NewDeviceModel::deviceRevoked,
+            this,
+            &DeviceItemListModel::reset,
+            Qt::UniqueConnection);
+
+    connect(deviceModel,
+            &lrc::api::NewDeviceModel::deviceUpdated,
+            this,
+            &DeviceItemListModel::reset,
+            Qt::UniqueConnection);
+}
diff --git a/src/deviceitemlistmodel.h b/src/deviceitemlistmodel.h
index 938657000..96bb787fc 100644
--- a/src/deviceitemlistmodel.h
+++ b/src/deviceitemlistmodel.h
@@ -1,6 +1,7 @@
 /*
- * Copyright (C) 2019-2020 by Savoir-faire Linux
+ * Copyright (C) 2021 by Savoir-faire Linux
  * Author: Yang Wang   <yang.wang@savoirfairelinux.com>
+ * 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
@@ -20,6 +21,8 @@
 
 #include "abstractlistmodelbase.h"
 
+#include <QSortFilterProxyModel>
+
 class DeviceItemListModel : public AbstractListModelBase
 {
     Q_OBJECT
@@ -31,22 +34,71 @@ public:
     explicit DeviceItemListModel(QObject* parent = nullptr);
     ~DeviceItemListModel();
 
-    /*
-     * QAbstractListModel override.
-     */
+    // QAbstractListModel override.
     int rowCount(const QModelIndex& parent = QModelIndex()) const override;
     int columnCount(const QModelIndex& parent) const override;
     QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
-    /*
-     * Override role name as access point in qml.
-     */
+
+    // Override role name as access point in qml.
     QHash<int, QByteArray> roleNames() const override;
     QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const;
     QModelIndex parent(const QModelIndex& child) const;
     Qt::ItemFlags flags(const QModelIndex& index) const;
 
-    /*
-     * This function is to reset the model when there's new account added.
-     */
+    // This function is to reset the model when there's new account added.
     Q_INVOKABLE void reset();
+
+    Q_INVOKABLE void revokeDevice(QString deviceId, QString password);
+
+public Q_SLOTS:
+    void onAccountChanged();
 };
+
+class DeviceItemProxyModel : public QSortFilterProxyModel
+{
+    Q_OBJECT
+    Q_PROPERTY(LRCInstance* lrcInstance READ getLrcInstance WRITE setLrcInstance)
+
+public:
+    explicit DeviceItemProxyModel(QObject* parent = nullptr)
+        : QSortFilterProxyModel(parent)
+    {
+        sourceModel_ = new DeviceItemListModel(this);
+
+        setSourceModel(sourceModel_);
+        setSortRole(DeviceItemListModel::Role::IsCurrent);
+        sort(0, Qt::DescendingOrder);
+        setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
+    }
+
+    bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
+    {
+        QVariant leftIsCurrent = sourceModel()->data(left, DeviceItemListModel::Role::IsCurrent);
+        QVariant rightIsCurrent = sourceModel()->data(right, DeviceItemListModel::Role::IsCurrent);
+
+        if (leftIsCurrent.toBool())
+            return false;
+        if (rightIsCurrent.toBool())
+            return true;
+
+        QChar leftDeviceNameFirstChar
+            = sourceModel()->data(left, DeviceItemListModel::Role::DeviceName).toString().at(0);
+        QChar rightDeviceNameFirstChar
+            = sourceModel()->data(right, DeviceItemListModel::Role::DeviceName).toString().at(0);
+
+        return leftDeviceNameFirstChar < rightDeviceNameFirstChar;
+    }
+
+    LRCInstance* getLrcInstance()
+    {
+        return sourceModel_->property("lrcInstance").value<LRCInstance*>();
+    }
+
+    void setLrcInstance(LRCInstance* instance)
+    {
+        sourceModel_->setProperty("lrcInstance", QVariant::fromValue(instance));
+    }
+
+private:
+    DeviceItemListModel* sourceModel_;
+};
\ No newline at end of file
diff --git a/src/qmlregister.cpp b/src/qmlregister.cpp
index dc6e6501c..d9342d49f 100644
--- a/src/qmlregister.cpp
+++ b/src/qmlregister.cpp
@@ -148,7 +148,7 @@ registerTypes(QQmlEngine* engine,
     QML_REGISTERNAMESPACE(NS_ENUMS, dummy::staticMetaObject, "");
 
     // QAbstractListModels
-    QML_REGISTERTYPE(NS_MODELS, DeviceItemListModel);
+    QML_REGISTERTYPE(NS_MODELS, DeviceItemProxyModel);
     QML_REGISTERTYPE(NS_MODELS, BannedListModel);
     QML_REGISTERTYPE(NS_MODELS, ModeratorListModel);
     QML_REGISTERTYPE(NS_MODELS, MediaCodecListModel);
@@ -215,9 +215,10 @@ registerTypes(QQmlEngine* engine,
     QML_REGISTERUNCREATABLE_IN_NAMESPACE(PeerDiscoveryModel, lrc::api);
 
     // Enums
-    QML_REGISTERUNCREATABLE(NS_ENUMS, Settings);
-    QML_REGISTERUNCREATABLE(NS_ENUMS, NetWorkManager);
+    QML_REGISTERUNCREATABLE(NS_ENUMS, Settings)
+    QML_REGISTERUNCREATABLE(NS_ENUMS, NetWorkManager)
     QML_REGISTERUNCREATABLE(NS_ENUMS, WizardViewStepModel)
+    QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceItemListModel)
 
     engine->addImageProvider(QLatin1String("qrImage"), new QrImageProvider(lrcInstance));
     engine->addImageProvider(QLatin1String("avatarImage"),
diff --git a/src/settingsadapter.cpp b/src/settingsadapter.cpp
index 7ec10f603..8b871a089 100644
--- a/src/settingsadapter.cpp
+++ b/src/settingsadapter.cpp
@@ -1017,10 +1017,7 @@ SettingsAdapter::tlsProtocolComboBoxIndexChanged(const int& index)
 void
 SettingsAdapter::setDeviceName(QString text)
 {
-    auto confProps = lrcInstance_->accountModel().getAccountConfig(
-        lrcInstance_->get_currentAccountId());
-    confProps.deviceName = text;
-    lrcInstance_->accountModel().setAccountConfig(lrcInstance_->get_currentAccountId(), confProps);
+    lrcInstance_->getCurrentAccountInfo().deviceModel->setCurrentDeviceName(text);
 }
 
 void
diff --git a/src/settingsview/SettingsView.qml b/src/settingsview/SettingsView.qml
index 3462007ea..176309a89 100644
--- a/src/settingsview/SettingsView.qml
+++ b/src/settingsview/SettingsView.qml
@@ -52,7 +52,6 @@ Rectangle {
         if(selectedMenu === sel && (!recovery)) { return }
         switch(sel) {
             case SettingsView.Account:
-                pageIdCurrentAccountSettings.connectCurrentAccount()
                 AccountAdapter.stopPreviewing()
                 selectedMenu = sel
                 pageIdCurrentAccountSettings.updateAccountInfoDisplayed()
@@ -97,7 +96,6 @@ Rectangle {
         var accountList = AccountAdapter.model.getAccountList()
         if(accountList.length === 0)
             return
-        pageIdCurrentAccountSettings.disconnectAccountConnections()
         var device = AVModel.getDefaultDevice()
         if(device.length === 0) {
             AVModel.setCurrentVideoCaptureDevice(device)
diff --git a/src/settingsview/components/CurrentAccountSettings.qml b/src/settingsview/components/CurrentAccountSettings.qml
index 23b65de7e..bb73b7474 100644
--- a/src/settingsview/components/CurrentAccountSettings.qml
+++ b/src/settingsview/components/CurrentAccountSettings.qml
@@ -44,7 +44,6 @@ Rectangle {
         accountEnableCheckBox.checked = SettingsAdapter.get_CurrentAccountInfo_Enabled()
         accountProfile.updateAccountInfo()
         userIdentity.updateAccountInfo()
-        linkedDevices.updateAndShowDevicesSlot()
         bannedContacts.updateAndShowBannedContactsSlot()
         advancedSettings.updateAdvancedAccountInfos()
         var isJams = !isSIP && SettingsAdapter.getAccountConfig_Manageruri() !== ""
@@ -69,16 +68,6 @@ Rectangle {
         deleteAccountDialog.openDialog()
     }
 
-    function connectCurrentAccount() {
-        if (!isSIP) {
-            linkedDevices.connectCurrentAccount(true)
-        }
-    }
-
-    function disconnectAccountConnections() {
-        linkedDevices.connectCurrentAccount(false)
-    }
-
     function getAdvancedSettingsScrollPosition() {
         return advancedSettings.y
     }
diff --git a/src/settingsview/components/DeviceItemDelegate.qml b/src/settingsview/components/DeviceItemDelegate.qml
index fd54624fb..fcb1337ff 100644
--- a/src/settingsview/components/DeviceItemDelegate.qml
+++ b/src/settingsview/components/DeviceItemDelegate.qml
@@ -38,26 +38,11 @@ ItemDelegate {
 
     signal btnRemoveDeviceClicked
 
-    function toggleEditable() {
-        editable = !editable
-        if (!editable) {
-           SettingsAdapter.setDeviceName(elidedTextDeviceName.text)
-        }
-    }
+    highlighted: ListView.isCurrentItem
 
     background: Rectangle {
         color: highlighted? JamiTheme.selectedColor : JamiTheme.editBackgroundColor
     }
-    highlighted: ListView.isCurrentItem
-
-    CustomBorder {
-        commonBorder: false
-        lBorderwidth: 0
-        rBorderwidth: 0
-        tBorderwidth: 0
-        bBorderwidth: 2
-        borderColor: JamiTheme.selectedColor
-    }
 
     RowLayout {
         anchors.fill: root
@@ -80,6 +65,8 @@ ItemDelegate {
         }
 
         ColumnLayout {
+            id: deviceInfoColumnLayout
+
             Layout.fillWidth: true
             Layout.fillHeight: true
             Layout.leftMargin: JamiTheme.preferredMarginSize
@@ -91,32 +78,50 @@ ItemDelegate {
                 Layout.fillWidth: true
                 Layout.preferredHeight: 30
 
+                padding: 8
                 font.pointSize: JamiTheme.textFontSize
 
+                horizontalAlignment: Text.AlignLeft
+                verticalAlignment: Text.AlignVCenter
+
                 wrapMode: Text.NoWrap
                 readOnly: !editable
+                loseFocusWhenEnterPressed: true
                 backgroundColor: JamiTheme.editBackgroundColor
-                text: elidedTextDeviceName.elidedText
-                padding: 8
-            }
 
-            TextMetrics {
-                id: elidedTextDeviceName
+                onEditingFinished: {
+                    SettingsAdapter.setDeviceName(editDeviceName.text)
+                    editable = !editable
+                }
+                onReadOnlyChanged: {
+                    if (readOnly)
+                        editDeviceName.text = Qt.binding(function() {
+                            return elidedTextDeviceName.elidedText
+                        })
+                    else
+                        editDeviceName.text = deviceName
+                }
 
-                elide: Text.ElideRight
-                elideWidth: root.width - btnEditDevice.width - deviceImage.width
-                            - editDeviceName.leftPadding
-                text: deviceName
+                TextMetrics {
+                    id: elidedTextDeviceName
+
+                    font: editDeviceName.font
+                    elide: Text.ElideRight
+                    elideWidth: editDeviceName.width - editDeviceName.leftPadding * 2
+                    text: deviceName
+                }
             }
 
-            ElidedTextLabel {
+            Text {
                 id: labelDeviceId
 
                 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                Layout.fillWidth: true
                 Layout.leftMargin: editDeviceName.leftPadding
 
-                maxWidth: root.width - btnEditDevice.width - deviceImage.width
-                eText: deviceId === "" ? qsTr("Device Id") : deviceId
+                elide: Text.ElideRight
+                color: JamiTheme.textColor
+                text: deviceId === "" ? qsTr("Device Id") : deviceId
             }
         }
 
@@ -147,11 +152,23 @@ ItemDelegate {
 
             onClicked: {
                 if (isCurrent) {
-                    toggleEditable()
+                    if (!editable)
+                        editable = !editable
+                    else
+                        editDeviceName.focus = false
                 } else {
                     btnRemoveDeviceClicked()
                 }
             }
         }
     }
+
+    CustomBorder {
+        commonBorder: false
+        lBorderwidth: 0
+        rBorderwidth: 0
+        tBorderwidth: 0
+        bBorderwidth: 2
+        borderColor: JamiTheme.selectedColor
+    }
 }
diff --git a/src/settingsview/components/LinkedDevices.qml b/src/settingsview/components/LinkedDevices.qml
index 458c0461c..a57e91f14 100644
--- a/src/settingsview/components/LinkedDevices.qml
+++ b/src/settingsview/components/LinkedDevices.qml
@@ -23,31 +23,13 @@ import QtQuick.Layouts 1.15
 import net.jami.Models 1.1
 import net.jami.Adapters 1.1
 import net.jami.Constants 1.1
+import net.jami.Enums 1.1
 
 import "../../commoncomponents"
 
 ColumnLayout {
     id:root
 
-    Connections {
-        id: accountConnections_DeviceModel
-
-        target: AccountAdapter.deviceModel
-        enabled: root.visible
-
-        function onDeviceAdded(id) {
-            updateAndShowDevicesSlot()
-        }
-
-        function onDeviceRevoked(id, status) {
-            updateAndShowDevicesSlot()
-        }
-
-        function onDeviceUpdated(id) {
-            updateAndShowDevicesSlot()
-        }
-    }
-
     Connections {
         id: accountConnections
 
@@ -61,24 +43,9 @@ ColumnLayout {
         }
     }
 
-    function connectCurrentAccount(status) {
-        accountConnections_DeviceModel.enabled = status
-    }
-
-    function updateAndShowDevicesSlot() {
-        if (SettingsAdapter.getAccountConfig_Manageruri() === ""){
-            linkDevPushButton.visible = SettingsAdapter.get_CurrentAccountInfo_Enabled()
-        }
-        settingsListView.model.reset()
-    }
-
-    function revokeDeviceWithIDAndPassword(idDevice, password){
-        AccountAdapter.deviceModel.revokeDevice(idDevice, password)
-        updateAndShowDevicesSlot()
-    }
-
     function removeDeviceSlot(index){
-        var idOfDevice = settingsListView.model.data(settingsListView.model.index(index,0), DeviceItemListModel.DeviceID)
+        var idOfDevice = settingsListView.model.data(settingsListView.model.index(index,0),
+                                                     DeviceItemListModel.DeviceID)
         if(AccountAdapter.hasPassword()){
             revokeDevicePasswordDialog.openRevokeDeviceDialog(idOfDevice)
         } else {
@@ -89,14 +56,12 @@ ColumnLayout {
 
     LinkDeviceDialog {
         id: linkDeviceDialog
-
-        onAccepted: updateAndShowDevicesSlot()
     }
 
     RevokeDevicePasswordDialog{
         id: revokeDevicePasswordDialog
 
-        onRevokeDeviceWithPassword: revokeDeviceWithIDAndPassword(idOfDevice, password)
+        onRevokeDeviceWithPassword: deviceItemListModel.revokeDevice(idOfDevice, password)
     }
 
     SimpleMessageDialog {
@@ -104,19 +69,19 @@ ColumnLayout {
 
         property string idOfDev: ""
 
-        title: qsTr("Remove Device")
-        infoText: qsTr("Are you sure you wish to remove this device?")
+        title: JamiStrings.removeDevice
+        infoText: JamiStrings.sureToRemoveDevice
 
-        buttonTitles: [qsTr("Ok"), qsTr("Cancel")]
+        buttonTitles: [JamiStrings.optionOk, JamiStrings.optionCancel]
         buttonStyles: [SimpleMessageDialog.ButtonStyle.TintedBlue,
                        SimpleMessageDialog.ButtonStyle.TintedBlack]
-        buttonCallBacks: [function() {revokeDeviceWithIDAndPassword(idOfDev, "")}]
+        buttonCallBacks: [function() {deviceItemListModel.revokeDevice(idOfDev, "")}]
     }
 
     Label {
         Layout.preferredHeight: JamiTheme.preferredFieldHeight
 
-        text: qsTr("Linked Devices")
+        text: JamiStrings.linkedDevices
         color: JamiTheme.textColor
 
         font.pointSize: JamiTheme.headerFontSize
@@ -129,7 +94,9 @@ ColumnLayout {
         Layout.fillWidth: true
         Layout.preferredHeight: 160
 
-        model: DeviceItemListModel {
+        model: DeviceItemProxyModel {
+            id: deviceItemListModel
+
             lrcInstance: LRCInstance
         }
 
-- 
GitLab