on xenial help sign in
Add pull-to-refresh to app drawer

We have XdgWatcher that watches the change for Click apps. However, there isn't such a thing for Libertine apps. This commit implement pull-to-refresh in the app drawer as a stop-gap solution. In the long term, when we have such mechanism, we can remove it. Or, we can just leave it here as a failback when watcher things work incorrectly. Changelog: v2: - Switch to PullToRefresh component from Ubuntu.Components instead of homegrown solution. PullToRefreshScopeStyle is copied from the legacy Unity8 code. - Make sure AppDrawerModel clears the list before re-read. Add a test for that. v3: - Remove unnecessary change left over from v2. - Make sure all items in the list are deleted before being cleared. v4: - Completely re-implement AppDrawerModel to refresh asynchronously, including when first constructed. Expose a new "refreshing" property. - Display a progressbar when refresing is in progress.

commit2a12bafe606d59898588ccece7812655f225cab3 authorRatchanan Srirattanamet <ratchanan@ubports.com> dateFri 10 Apr 2020, 23:43 (7 years ago) parentdb503ee
11 files changed+276−22
plugins/Unity/Launcher/CMakeLists.txt+1−1 plugins/Unity/Launcher/appdrawermodel.cpp+83−16 plugins/Unity/Launcher/appdrawermodel.h+23−2 plugins/Unity/Launcher/launcheritem.cpp−1 qml/Launcher/Drawer.qml+6 qml/Launcher/DrawerGridView.qml+31 qml/Launcher/PullToRefreshScopeStyle.qml+83 tests/mocks/Unity/Launcher/MockAppDrawerModel.cpp+25−1 tests/mocks/Unity/Launcher/MockAppDrawerModel.h+11 tests/plugins/Unity/Launcher/CMakeLists.txt+1−1 tests/plugins/Unity/Launcher/appdrawermodeltest.cpp+12
plugins/Unity/Launcher/CMakeLists.txt+1−1
@@ -50,6 +50,6 @@ target_link_libraries(UnityLauncher-qml
5050 ${UAL_LIBRARIES}
5151 )
5252
53−qt5_use_modules(UnityLauncher-qml DBus Qml Gui)
53+qt5_use_modules(UnityLauncher-qml DBus Qml Gui Concurrent)
5454
5555add_unity8_plugin(Unity.Launcher 0.1 Unity/Launcher TARGETS UnityLauncher-qml)
plugins/Unity/Launcher/appdrawermodel.cpp+83−16
@@ -1,5 +1,6 @@
11/*
22 * Copyright (C) 2016 Canonical, Ltd.
3+ * Copyright (C) 2020 UBports Foundation.
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU General Public License as published by
@@ -20,28 +21,31 @@
2021
2122#include <QDebug>
2223#include <QDateTime>
24+#include <QtConcurrentRun>
25+
26+static std::shared_ptr<LauncherItem> makeSharedLauncherItem(
27+ const QString &appId, const QString &name, const QString &icon, QObject * parent)
28+{
29+ return std::shared_ptr<LauncherItem>(
30+ new LauncherItem(appId, name, icon, parent),
31+ [] (LauncherItem *item) { item->deleteLater(); });
32+}
2333
2434AppDrawerModel::AppDrawerModel(QObject *parent):
2535 AppDrawerModelInterface(parent),
2636 m_ual(new UalWrapper(this)),
27− m_xdgWatcher(new XdgWatcher(this))
37+ m_xdgWatcher(new XdgWatcher(this)),
38+ m_refreshing(false)
2839{
29− Q_FOREACH (const QString &appId, UalWrapper::installedApps()) {
30− UalWrapper::AppInfo info = UalWrapper::getApplicationInfo(appId);
31− if (!info.valid) {
32− qWarning() << "Failed to get app info for app" << appId;
33− continue;
34− }
35− LauncherItem* item = new LauncherItem(appId, info.name, info.icon, this);
36− item->setKeywords(info.keywords);
37− item->setPopularity(info.popularity);
38− m_list.append(item);
39− }
40+ connect(&m_refreshFutureWatcher, &QFutureWatcher<ItemList>::finished,
41+ this, &AppDrawerModel::onRefreshFinished);
4042
4143 // keep this a queued connection as it's coming from another thread.
4244 connect(m_xdgWatcher, &XdgWatcher::appAdded, this, &AppDrawerModel::appAdded, Qt::QueuedConnection);
4345 connect(m_xdgWatcher, &XdgWatcher::appRemoved, this, &AppDrawerModel::appRemoved, Qt::QueuedConnection);
4446 connect(m_xdgWatcher, &XdgWatcher::appInfoChanged, this, &AppDrawerModel::appInfoChanged, Qt::QueuedConnection);
47+
48+ refresh();
4549}
4650
4751int AppDrawerModel::rowCount(const QModelIndex &parent) const
@@ -70,6 +74,10 @@ QVariant AppDrawerModel::data(const QModelIndex &index, int role) const
7074
7175void AppDrawerModel::appAdded(const QString &appId)
7276{
77+ if (m_refreshing)
78+ // Will be replaced by the refresh result anyway.
79+ return;
80+
7381 UalWrapper::AppInfo info = UalWrapper::getApplicationInfo(appId);
7482 if (!info.valid) {
7583 qWarning() << "App added signal received but failed to get app info for app" << appId;
@@ -77,15 +85,19 @@ void AppDrawerModel::appAdded(const QString &appId)
7785 }
7886
7987 beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
80− LauncherItem* item = new LauncherItem(appId, info.name, info.icon, this);
88+ auto item = makeSharedLauncherItem(appId, info.name, info.icon, /* parent */ nullptr);
8189 item->setKeywords(info.keywords);
8290 item->setPopularity(info.popularity);
83− m_list.append(item);
91+ m_list.append(std::move(item));
8492 endInsertRows();
8593}
8694
8795void AppDrawerModel::appRemoved(const QString &appId)
8896{
97+ if (m_refreshing)
98+ // Will be replaced by the refresh result anyway.
99+ return;
100+
89101 int idx = -1;
90102 for (int i = 0; i < m_list.count(); i++) {
91103 if (m_list.at(i)->appId() == appId) {
@@ -98,13 +110,17 @@ void AppDrawerModel::appRemoved(const QString &appId)
98110 return;
99111 }
100112 beginRemoveRows(QModelIndex(), idx, idx);
101− m_list.takeAt(idx)->deleteLater();
113+ m_list.removeAt(idx);
102114 endRemoveRows();
103115}
104116
105117void AppDrawerModel::appInfoChanged(const QString &appId)
106118{
107− LauncherItem *item = nullptr;
119+ if (m_refreshing)
120+ // Will be replaced by the refresh result anyway.
121+ return;
122+
123+ std::shared_ptr<LauncherItem> item;
108124 int idx = -1;
109125
110126 for(int i = 0; i < m_list.count(); i++) {
@@ -123,3 +139,54 @@ void AppDrawerModel::appInfoChanged(const QString &appId)
123139 item->setPopularity(info.popularity);
124140 Q_EMIT dataChanged(index(idx), index(idx), {AppDrawerModelInterface::RoleUsage});
125141}
142+
143+bool AppDrawerModel::refreshing() {
144+ return m_refreshing;
145+}
146+
147+void AppDrawerModel::refresh() {
148+ if (m_refreshing)
149+ return;
150+
151+ m_refreshFutureWatcher.setFuture(QtConcurrent::run([](QThread *thread) {
152+ ItemList list;
153+
154+ Q_FOREACH (const QString &appId, UalWrapper::installedApps()) {
155+ UalWrapper::AppInfo info = UalWrapper::getApplicationInfo(appId);
156+ if (!info.valid) {
157+ qWarning() << "Failed to get app info for app" << appId;
158+ continue;
159+ }
160+ // We don't pass parent in because this may run after the model is destroyed.
161+ // (And, in fact, we can't, because the model is in a diferent thread.)
162+ auto item = makeSharedLauncherItem(appId, info.name, info.icon, /* parent */ nullptr);
163+ item->setKeywords(info.keywords);
164+ item->setPopularity(info.popularity);
165+ item->moveToThread(thread);
166+ list.append(std::move(item));
167+ }
168+
169+ return list;
170+ }, this->thread()));
171+
172+ m_refreshing = true;
173+ Q_EMIT refreshingChanged();
174+}
175+
176+void AppDrawerModel::onRefreshFinished() {
177+ if (m_refreshFutureWatcher.isCanceled())
178+ // This is the result of setting canceled future below.
179+ return;
180+
181+ beginResetModel();
182+
183+ m_list = m_refreshFutureWatcher.result();
184+ // Remove the future & its result, so that future modifications won't
185+ // create a copy.
186+ m_refreshFutureWatcher.setFuture(QFuture<ItemList>());
187+
188+ endResetModel();
189+
190+ m_refreshing = false;
191+ Q_EMIT refreshingChanged();
192+}
plugins/Unity/Launcher/appdrawermodel.h+23−2
@@ -1,5 +1,6 @@
11/*
22 * Copyright (C) 2016 Canonical, Ltd.
3+ * Copyright (C) 2020 UBports Foundation.
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU General Public License as published by
@@ -14,7 +15,8 @@
1415 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1516 */
1617
17−
18+#include <memory>
19+#include <QFutureWatcher>
1820#include <unity/shell/launcher/AppDrawerModelInterface.h>
1921
2022#include "launcheritem.h"
@@ -25,19 +27,38 @@ class XdgWatcher;
2527class AppDrawerModel: public AppDrawerModelInterface
2628{
2729 Q_OBJECT
30+ // TODO: Add this to AppDrawerModelInterface in unity-api.
31+ // Or, better yet, remove AppDrawerModelInterface from unity-api.
32+ Q_PROPERTY(bool refreshing READ refreshing NOTIFY refreshingChanged)
2833public:
2934 AppDrawerModel(QObject* parent = nullptr);
3035
3136 int rowCount(const QModelIndex &parent) const override;
3237 QVariant data(const QModelIndex &index, int role) const override;
3338
39+ // TODO: add these to AppDrawerModelInterface too.
40+ bool refreshing();
41+ Q_INVOKABLE void refresh();
42+
43+Q_SIGNALS:
44+ void refreshingChanged();
45+
3446private Q_SLOTS:
3547 void appAdded(const QString &appId);
3648 void appRemoved(const QString &appId);
3749 void appInfoChanged(const QString &appId);
3850
51+ void onRefreshFinished();
52+
3953private:
40− QList<LauncherItem*> m_list;
54+ // Using shared_ptr is unavoidable in order to share the refresh result
55+ // from the worker thread safely without a memory leak, in case the model
56+ // is destructed before the worker finishes.
57+ typedef QList<std::shared_ptr<LauncherItem>> ItemList;
58+
59+ ItemList m_list;
4160 UalWrapper *m_ual;
4261 XdgWatcher *m_xdgWatcher;
62+ QFutureWatcher<ItemList> m_refreshFutureWatcher;
63+ bool m_refreshing;
4364};
plugins/Unity/Launcher/launcheritem.cpp−1
@@ -37,7 +37,6 @@ LauncherItem::LauncherItem(const QString &appId, const QString &name, const QStr
3737 m_alerting(false),
3838 m_quickList(new QuickListModel(this))
3939{
40− Q_ASSERT(parent != nullptr);
4140 QuickListEntry nameAction;
4241 nameAction.setActionId(QStringLiteral("launch_item"));
4342 nameAction.setText(m_name);
qml/Launcher/Drawer.qml+6
@@ -1,5 +1,6 @@
11/*
22 * Copyright (C) 2016 Canonical, Ltd.
3+ * Copyright (C) 2020 UBports Foundation.
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU General Public License as published by
@@ -284,6 +285,11 @@ FocusScope {
284285 unFocusInput();
285286 }
286287 }
288+
289+ refreshing: appDrawerModel.refreshing
290+ onRefresh: {
291+ appDrawerModel.refresh();
292+ }
287293 }
288294 }
289295
qml/Launcher/DrawerGridView.qml+31
@@ -1,5 +1,6 @@
11/*
22 * Copyright (C) 2016 Canonical, Ltd.
3+ * Copyright (C) 2020 UBports Foundation.
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU General Public License as published by
@@ -15,6 +16,7 @@
1516 */
1617
1718import QtQuick 2.4
19+import Ubuntu.Components 1.3
1820import "../Components"
1921
2022FocusScope {
@@ -35,6 +37,9 @@ FocusScope {
3537 readonly property int columns: width / delegateWidth
3638 readonly property int rows: Math.ceil(gridView.model.count / root.columns)
3739
40+ property alias refreshing: pullToRefresh.refreshing
41+ signal refresh();
42+
3843 GridView {
3944 id: gridView
4045 anchors.fill: parent
@@ -47,6 +52,32 @@ FocusScope {
4752
4853 cellWidth: root.delegateWidth + spacing
4954 cellHeight: root.delegateHeight
55+
56+ PullToRefresh {
57+ id: pullToRefresh
58+ parent: gridView
59+ target: gridView
60+
61+ readonly property real contentY: gridView.contentY - gridView.originY
62+ y: -contentY - units.gu(5)
63+
64+ readonly property color pullLabelColor: "white"
65+ style: PullToRefreshScopeStyle {
66+ activationThreshold: Math.min(units.gu(14), gridView.height / 5)
67+ }
68+
69+ onRefresh: root.refresh();
70+ }
71+ }
72+
73+ ProgressBar {
74+ anchors {
75+ left: parent.left
76+ right: parent.right
77+ bottom: parent.bottom
78+ }
79+ visible: refreshing
80+ indeterminate: true
5081 }
5182
5283 function getFirstAppId() {
qml/Launcher/PullToRefreshScopeStyle.qmlnew file+83
@@ -0,0 +1,83 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Components.Styles 1.3
20+
21+/**
22+ * TODO: Once the SDK version of PullToRefreshStyle doesn't have bug 1375799
23+ * (https://launchpad.net/bugs/1375799) anymore, we should switch to using a
24+ * a subclass of the Ambiance version with a hidden ActivityIndicator.
25+ */
26+PullToRefreshStyle {
27+ releaseToRefresh: styledItem.target.originY - styledItem.target.contentY > activationThreshold
28+
29+ Connections {
30+ property bool willRefresh: false
31+
32+ target: styledItem.target
33+ onDraggingChanged: {
34+ if (!styledItem.target.dragging && releaseToRefresh) {
35+ willRefresh = true
36+ }
37+ }
38+ onContentYChanged: {
39+ if (styledItem.target.originY - styledItem.target.contentY == 0 && willRefresh) {
40+ styledItem.refresh()
41+ willRefresh = false
42+ }
43+ }
44+ }
45+
46+ Label {
47+ id: pullLabel
48+ anchors.horizontalCenter: parent.horizontalCenter
49+ horizontalAlignment: Text.AlignHCenter
50+ verticalAlignment: Text.AlignVCenter
51+ color: styledItem.pullLabelColor
52+ states: [
53+ State {
54+ name: "pulling"
55+ when: styledItem.target.dragging && !releaseToRefresh
56+ PropertyChanges { target: pullLabel; text: i18n.tr("Pull to refresh…") }
57+ },
58+ State {
59+ name: "releasable"
60+ when: styledItem.target.dragging && releaseToRefresh
61+ PropertyChanges { target: pullLabel; text: i18n.tr("Release to refresh…") }
62+ }
63+ ]
64+ transitions: Transition {
65+ SequentialAnimation {
66+ UbuntuNumberAnimation {
67+ target: pullLabel
68+ property: "opacity"
69+ to: 0.0
70+ }
71+ PropertyAction {
72+ target: pullLabel
73+ property: "text"
74+ }
75+ UbuntuNumberAnimation {
76+ target: pullLabel
77+ property: "opacity"
78+ to: 1.0
79+ }
80+ }
81+ }
82+ }
83+}
tests/mocks/Unity/Launcher/MockAppDrawerModel.cpp+25−1
@@ -1,5 +1,6 @@
11/*
22 * Copyright (C) 2016 Canonical, Ltd.
3+ * Copyright (C) 2020 UBports Foundation.
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU General Public License as published by
@@ -18,9 +19,11 @@
1819
1920#include <QDebug>
2021#include <QDateTime>
22+#include <QTimer>
2123
2224MockAppDrawerModel::MockAppDrawerModel(QObject *parent):
23− AppDrawerModelInterface(parent)
25+ AppDrawerModelInterface(parent),
26+ m_refresing(false)
2427{
2528 MockLauncherItem *item = new MockLauncherItem("dialer-app", "/usr/share/applications/dialer-app.desktop", "Dialer", "dialer-app", this);
2629 m_list.append(item);
@@ -72,3 +75,24 @@ QVariant MockAppDrawerModel::data(const QModelIndex &index, int role) const
7275
7376 return QVariant();
7477}
78+
79+bool MockAppDrawerModel::refreshing() {
80+ return m_refresing;
81+}
82+
83+void MockAppDrawerModel::refresh() {
84+ if (m_refresing)
85+ return;
86+
87+ m_refresing = true;
88+ Q_EMIT refreshingChanged();
89+
90+ QTimer::singleShot(/* msec */ 1000, this, [this] {
91+ beginResetModel();
92+ // Pretended that the data has changed;
93+ endResetModel();
94+
95+ m_refresing = false;
96+ Q_EMIT refreshingChanged();
97+ });
98+}
tests/mocks/Unity/Launcher/MockAppDrawerModel.h+11
@@ -1,5 +1,6 @@
11/*
22 * Copyright (C) 2016 Canonical, Ltd.
3+ * Copyright (C) 2020 UBports Foundation.
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU General Public License as published by
@@ -21,12 +22,22 @@
2122class MockAppDrawerModel: public AppDrawerModelInterface
2223{
2324 Q_OBJECT
25+ // TODO: Add this to AppDrawerModelInterface in unity-api.
26+ // Or, better yet, remove AppDrawerModelInterface from unity-api.
27+ Q_PROPERTY(bool refreshing READ refreshing NOTIFY refreshingChanged)
2428public:
2529 MockAppDrawerModel(QObject* parent = nullptr);
2630
2731 int rowCount(const QModelIndex &parent) const override;
2832 QVariant data(const QModelIndex &index, int role) const override;
2933
34+ bool refreshing();
35+ Q_INVOKABLE void refresh();
36+
37+Q_SIGNALS:
38+ void refreshingChanged();
39+
3040private:
3141 QList<MockLauncherItem*> m_list;
42+ bool m_refresing;
3243};
tests/plugins/Unity/Launcher/CMakeLists.txt+1−1
@@ -81,7 +81,7 @@ target_link_libraries(appdrawermodeltestExec
8181 ${UAL_LIBRARIES}
8282 )
8383add_dependencies(appdrawermodeltestExec mock-server)
84−qt5_use_modules(appdrawermodeltestExec Test Core DBus Xml Gui Qml)
84+qt5_use_modules(appdrawermodeltestExec Test Core DBus Xml Gui Qml Concurrent)
8585install(TARGETS appdrawermodeltestExec
8686 DESTINATION "${SHELL_PRIVATE_LIBDIR}/tests/plugins/Unity/Launcher"
8787 )
tests/plugins/Unity/Launcher/appdrawermodeltest.cpp+12
@@ -32,6 +32,8 @@ private Q_SLOTS:
3232 void initTestCase() {
3333 UalWrapper::s_list << QStringLiteral("app1") << QStringLiteral("app2");
3434 appDrawerModel = new AppDrawerModel(this);
35+ QTRY_VERIFY(!appDrawerModel->refreshing());
36+
3537 QCOMPARE(appDrawerModel->rowCount(QModelIndex()), 2);
3638 }
3739
@@ -50,6 +52,16 @@ private Q_SLOTS:
5052
5153 QCOMPARE(appDrawerModel->rowCount(QModelIndex()), 2);
5254 }
55+
56+ void testRefresh() {
57+ QSignalSpy refreshingSpy(appDrawerModel, &AppDrawerModel::refreshingChanged);
58+
59+ appDrawerModel->refresh();
60+ QTRY_VERIFY(!appDrawerModel->refreshing());
61+
62+ QCOMPARE(refreshingSpy.count(), 2);
63+ QCOMPARE(appDrawerModel->rowCount(QModelIndex()), 2);
64+ }
5365};
5466
5567QTEST_GUILESS_MAIN(AppDrawerModelTest)