on xenial help sign in
Added an indicator which is displayed in the search bar whenever a search is in progress.
1e440daNicolas d'Offay
Components/PageHeader.qml+44−1
@@ -18,6 +18,7 @@ import QtQuick 2.0
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19import Ubuntu.Components.Popups 0.1
19import Ubuntu.Components.Popups 0.1
20import Ubuntu.Components.ListItems 0.1 as ListItem
20import Ubuntu.Components.ListItems 0.1 as ListItem
21+import Unity 0.1
21
22
22Item {
23Item {
23 /*!
24 /*!
@@ -30,6 +31,7 @@ Item {
30 property bool searchEntryEnabled: false
31 property bool searchEntryEnabled: false
31 property alias searchQuery: searchField.text
32 property alias searchQuery: searchField.text
32 property ListModel searchHistory: SearchHistoryModel {}
33 property ListModel searchHistory: SearchHistoryModel {}
34+ property Scope scope
33
35
34 height: units.gu(8.5)
36 height: units.gu(8.5)
35 implicitHeight: units.gu(8.5)
37 implicitHeight: units.gu(8.5)
@@ -148,7 +150,7 @@ Item {
148 hasClearButton: false
150 hasClearButton: false
149
151
150 primaryItem: AbstractButton {
152 primaryItem: AbstractButton {
151− enabled: searchField.text != ""
153+ enabled: searchField.text != "" && !searchIndicator.running
152 onClicked: {
154 onClicked: {
153 if (searchField.text != "") {
155 if (searchField.text != "") {
154 searchHistory.addQuery(searchField.text)
156 searchHistory.addQuery(searchField.text)
@@ -158,8 +160,22 @@ Item {
158 height: parent.height
160 height: parent.height
159 width: height
161 width: height
160
162
163+ ActivityIndicator {
164+ id: searchIndicator
165+ objectName: "searchIndicator"
166+
167+ anchors {
168+ verticalCenter: parent.verticalCenter
169+ left: parent.left
170+ leftMargin: units.gu(0.5)
171+ }
172+
173+ running: opacity > 0
174+ }
175+
161 Image {
176 Image {
162 id: primaryImage
177 id: primaryImage
178+ objectName: "primaryImage"
163 anchors {
179 anchors {
164 verticalCenter: parent.verticalCenter
180 verticalCenter: parent.verticalCenter
165 left: parent.left
181 left: parent.left
@@ -167,6 +183,7 @@ Item {
167 }
183 }
168 width: units.gu(3)
184 width: units.gu(3)
169 height: units.gu(3)
185 height: units.gu(3)
186+ visible: opacity > 0
170 }
187 }
171
188
172 Item {
189 Item {
@@ -185,6 +202,32 @@ Item {
185 onActiveFocusChanged: {
202 onActiveFocusChanged: {
186 if (!activeFocus) searchContainer.closePopover()
203 if (!activeFocus) searchContainer.closePopover()
187 }
204 }
205+
206+ states: [
207+ State {
208+ name: "searching"
209+ when: scope.searchInProgress && searchField.text !== ""
210+ PropertyChanges { target: searchIndicator; running: true; opacity: 1 }
211+ PropertyChanges { target: primaryImage; opacity: 0 }
212+ },
213+ State {
214+ name: "idle"
215+ when: !scope.searchInProgress || searchField.text === ""
216+ PropertyChanges { target: searchIndicator; opacity: 0 }
217+ PropertyChanges { target: primaryImage; opacity: 1 }
218+ }
219+ ]
220+
221+ transitions: [
222+ Transition {
223+ to: "searching"
224+ reversible: true
225+ SequentialAnimation {
226+ NumberAnimation { target: primaryImage; property: "opacity"; duration: UbuntuAnimation.FastDuration; easing.type: Easing.Linear }
227+ NumberAnimation { target: searchIndicator; property: "opacity"; duration: UbuntuAnimation.FastDuration; easing.type: Easing.Linear }
228+ }
229+ }
230+ ]
188 }
231 }
189
232
190 states: [
233 states: [
Dash/GenericScopeView.qml+1
@@ -179,6 +179,7 @@ ScopeView {
179 width: categoryView.width
179 width: categoryView.width
180 text: scopeView.scope.name
180 text: scopeView.scope.name
181 searchEntryEnabled: true
181 searchEntryEnabled: true
182+ scope: scopeView.scope
182 }
183 }
183 }
184 }
184
185
plugins/Unity/scope.cpp+30−5
@@ -33,12 +33,14 @@
33#include <QQmlEngine>
33#include <QQmlEngine>
34
34
35#include <UnityCore/Variant.h>
35#include <UnityCore/Variant.h>
36+#include <UnityCore/GLibWrapper.h>
36
37
37#include <libintl.h>
38#include <libintl.h>
38#include <glib.h>
39#include <glib.h>
39
40
40Scope::Scope(QObject *parent) : QObject(parent)
41Scope::Scope(QObject *parent) : QObject(parent)
41 , m_formFactor("phone")
42 , m_formFactor("phone")
43+ , m_searchInProgress(false)
42{
44{
43 m_categories.reset(new Categories(this));
45 m_categories.reset(new Categories(this));
44}
46}
@@ -68,6 +70,11 @@ QString Scope::searchHint() const
68 return QString::fromStdString(m_unityScope->search_hint());
70 return QString::fromStdString(m_unityScope->search_hint());
69}
71}
70
72
73+bool Scope::searchInProgress() const
74+{
75+ return m_searchInProgress;
76+}
77+
71bool Scope::visible() const
78bool Scope::visible() const
72{
79{
73 return m_unityScope->visible();
80 return m_unityScope->visible();
@@ -115,10 +122,15 @@ void Scope::setSearchQuery(const QString& search_query)
115 string ("") and m_searchQuery is the null string,
122 string ("") and m_searchQuery is the null string,
116 search_query != m_searchQuery is still true.
123 search_query != m_searchQuery is still true.
117 */
124 */
125+ using namespace std::placeholders;
126+
118 if (m_searchQuery.isNull() || search_query != m_searchQuery) {
127 if (m_searchQuery.isNull() || search_query != m_searchQuery) {
119 m_searchQuery = search_query;
128 m_searchQuery = search_query;
129+ m_cancellable.Renew();
130+ m_searchInProgress = true;
120− m_unityScope->Search(search_query.toStdString(), sigc::mem_fun(this, &Scope::searchFinished));
131+ m_unityScope->Search(search_query.toStdString(), std::bind(&Scope::onSearchFinished, this, _1, _2, _3), m_cancellable);
121 Q_EMIT searchQueryChanged();
132 Q_EMIT searchQueryChanged();
133+ Q_EMIT searchInProgressChanged();
122 }
134 }
123}
135}
124
136
@@ -294,8 +306,6 @@ void Scope::setUnityScope(const unity::dash::Scope::Ptr& scope)
294 m_unityScope->visible.changed.connect(sigc::mem_fun(this, &Scope::visibleChanged));
306 m_unityScope->visible.changed.connect(sigc::mem_fun(this, &Scope::visibleChanged));
295 m_unityScope->shortcut.changed.connect(sigc::mem_fun(this, &Scope::shortcutChanged));
307 m_unityScope->shortcut.changed.connect(sigc::mem_fun(this, &Scope::shortcutChanged));
296 m_unityScope->connected.changed.connect(sigc::mem_fun(this, &Scope::connectedChanged));
308 m_unityScope->connected.changed.connect(sigc::mem_fun(this, &Scope::connectedChanged));
297− /* Signals forwarding */
298− connect(this, SIGNAL(searchFinished(const std::string &, unity::glib::HintsMap const &, unity::glib::Error const &)), SLOT(onSearchFinished(const std::string &, unity::glib::HintsMap const &)));
299
309
300 /* FIXME: signal should be forwarded instead of calling the handler directly */
310 /* FIXME: signal should be forwarded instead of calling the handler directly */
301 m_unityScope->activated.connect(sigc::mem_fun(this, &Scope::onActivated));
311 m_unityScope->activated.connect(sigc::mem_fun(this, &Scope::onActivated));
@@ -316,18 +326,33 @@ unity::dash::Scope::Ptr Scope::unityScope() const
316
326
317void Scope::synchronizeStates()
327void Scope::synchronizeStates()
318{
328{
329+ using namespace std::placeholders;
330+
319 if (connected()) {
331 if (connected()) {
320 /* Forward local states to m_unityScope */
332 /* Forward local states to m_unityScope */
321 if (!m_searchQuery.isNull()) {
333 if (!m_searchQuery.isNull()) {
334+ m_cancellable.Renew();
335+ m_searchInProgress = true;
322− m_unityScope->Search(m_searchQuery.toStdString());
336+ m_unityScope->Search(m_searchQuery.toStdString(), std::bind(&Scope::onSearchFinished, this, _1, _2, _3), m_cancellable);
337+ Q_EMIT searchInProgressChanged();
323 }
338 }
324 }
339 }
325}
340}
326
341
327−void Scope::onSearchFinished(const std::string& /* query */, unity::glib::HintsMap const &hints)
342+void Scope::onSearchFinished(std::string const& /* query */, unity::glib::HintsMap const& hints, unity::glib::Error const& err)
328{
343{
329 QString hint;
344 QString hint;
330
345
346+ GError* error = const_cast<unity::glib::Error&>(err);
347+
348+ if (!err || !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
349+ m_searchInProgress = false;
350+ Q_EMIT searchInProgressChanged();
351+ } else {
352+ // no need to check the results hint, we're still searching
353+ return;
354+ }
355+
331 if (!m_unityScope->results()->count()) {
356 if (!m_unityScope->results()->count()) {
332 unity::glib::HintsMap::const_iterator it = hints.find("no-results-hint");
357 unity::glib::HintsMap::const_iterator it = hints.find("no-results-hint");
333 if (it != hints.end()) {
358 if (it != hints.end()) {
plugins/Unity/scope.h+7−2
@@ -28,6 +28,7 @@
28// libunity-core
28// libunity-core
29#include <UnityCore/Scope.h>
29#include <UnityCore/Scope.h>
30#include <UnityCore/Results.h>
30#include <UnityCore/Results.h>
31+#include <UnityCore/GLibWrapper.h>
31
32
32#include "categories.h"
33#include "categories.h"
33#include "filters.h"
34#include "filters.h"
@@ -43,6 +44,7 @@ class Scope : public QObject
43 Q_PROPERTY(QString iconHint READ iconHint NOTIFY iconHintChanged)
44 Q_PROPERTY(QString iconHint READ iconHint NOTIFY iconHintChanged)
44 Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
45 Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
45 Q_PROPERTY(QString searchHint READ searchHint NOTIFY searchHintChanged)
46 Q_PROPERTY(QString searchHint READ searchHint NOTIFY searchHintChanged)
47+ Q_PROPERTY(bool searchInProgress READ searchInProgress NOTIFY searchInProgressChanged)
46 Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged)
48 Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged)
47 Q_PROPERTY(QString shortcut READ shortcut NOTIFY shortcutChanged)
49 Q_PROPERTY(QString shortcut READ shortcut NOTIFY shortcutChanged)
48 Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
50 Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
@@ -65,6 +67,7 @@ public:
65 bool visible() const;
67 bool visible() const;
66 QString shortcut() const;
68 QString shortcut() const;
67 bool connected() const;
69 bool connected() const;
70+ bool searchInProgress() const;
68 Categories* categories() const;
71 Categories* categories() const;
69 Filters* filters() const;
72 Filters* filters() const;
70 QString searchQuery() const;
73 QString searchQuery() const;
@@ -95,11 +98,11 @@ Q_SIGNALS:
95 void iconHintChanged(const std::string&);
98 void iconHintChanged(const std::string&);
96 void descriptionChanged(const std::string&);
99 void descriptionChanged(const std::string&);
97 void searchHintChanged(const std::string&);
100 void searchHintChanged(const std::string&);
101+ void searchInProgressChanged();
98 void visibleChanged(bool);
102 void visibleChanged(bool);
99 void shortcutChanged(const std::string&);
103 void shortcutChanged(const std::string&);
100 void connectedChanged(bool);
104 void connectedChanged(bool);
101 void categoriesChanged();
105 void categoriesChanged();
102− void searchFinished(const std::string&, unity::glib::HintsMap const&, unity::glib::Error const&);
103 void searchQueryChanged();
106 void searchQueryChanged();
104 void noResultsHintChanged();
107 void noResultsHintChanged();
105 void formFactorChanged();
108 void formFactorChanged();
@@ -115,7 +118,7 @@ Q_SIGNALS:
115
118
116private Q_SLOTS:
119private Q_SLOTS:
117 void synchronizeStates();
120 void synchronizeStates();
118− void onSearchFinished(const std::string &, unity::glib::HintsMap const &);
121+ void onSearchFinished(std::string const &, unity::glib::HintsMap const &, unity::glib::Error const&);
119
122
120private:
123private:
121 unity::dash::LocalResult createLocalResult(const QVariant &uri, const QVariant &icon_hint,
124 unity::dash::LocalResult createLocalResult(const QVariant &uri, const QVariant &icon_hint,
@@ -133,6 +136,8 @@ private:
133 QString m_searchQuery;
136 QString m_searchQuery;
134 QString m_noResultsHint;
137 QString m_noResultsHint;
135 QString m_formFactor;
138 QString m_formFactor;
139+ bool m_searchInProgress;
140+ unity::glib::Cancellable m_cancellable;
136 unity::glib::Cancellable m_previewCancellable;
141 unity::glib::Cancellable m_previewCancellable;
137};
142};
138
143
tests/mocks/Unity/fake_scope.cpp+13
@@ -30,6 +30,7 @@ static DeeModel* create_results_model(unsigned category_count, unsigned result_c
30Scope::Scope(QObject* parent)
30Scope::Scope(QObject* parent)
31 : QObject(parent)
31 : QObject(parent)
32 , m_visible(false)
32 , m_visible(false)
33+ , m_searching(false)
33 , m_categories(new Categories(this))
34 , m_categories(new Categories(this))
34 , m_results(new DeeListModel(this))
35 , m_results(new DeeListModel(this))
35{
36{
@@ -44,6 +45,7 @@ Scope::Scope(QString const& id, QString const& name, bool visible, QObject* pare
44 , m_id(id)
45 , m_id(id)
45 , m_name(name)
46 , m_name(name)
46 , m_visible(visible)
47 , m_visible(visible)
48+ , m_searching(false)
47 , m_categories(new Categories(this))
49 , m_categories(new Categories(this))
48 , m_results(new DeeListModel(this))
50 , m_results(new DeeListModel(this))
49{
51{
@@ -85,6 +87,10 @@ bool Scope::connected() const {
85 return true;
87 return true;
86}
88}
87
89
90+bool Scope::searchInProgress() const {
91+ return m_searching;
92+}
93+
88Categories* Scope::categories() const {
94Categories* Scope::categories() const {
89 return m_categories;
95 return m_categories;
90}
96}
@@ -122,6 +128,13 @@ void Scope::setFormFactor(const QString &str) {
122 }
128 }
123}
129}
124
130
131+void Scope::setSearchInProgress(const bool inProg) {
132+ if (inProg != m_searching) {
133+ m_searching = inProg;
134+ Q_EMIT searchInProgressChanged();
135+ }
136+}
137+
125void Scope::setNoResultsHint(const QString& str) {
138void Scope::setNoResultsHint(const QString& str) {
126 if (str != m_noResultsHint) {
139 if (str != m_noResultsHint) {
127 m_noResultsHint = str;
140 m_noResultsHint = str;
tests/mocks/Unity/fake_scope.h+5−1
@@ -37,6 +37,7 @@ class Scope : public QObject
37 Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
37 Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
38 Q_PROPERTY(Categories* categories READ categories NOTIFY categoriesChanged)
38 Q_PROPERTY(Categories* categories READ categories NOTIFY categoriesChanged)
39
39
40+ Q_PROPERTY(bool searchInProgress READ searchInProgress WRITE setSearchInProgress NOTIFY searchInProgressChanged)
40 Q_PROPERTY(QString searchQuery READ searchQuery WRITE setSearchQuery NOTIFY searchQueryChanged)
41 Q_PROPERTY(QString searchQuery READ searchQuery WRITE setSearchQuery NOTIFY searchQueryChanged)
41 Q_PROPERTY(QString noResultsHint READ noResultsHint WRITE setNoResultsHint NOTIFY noResultsHintChanged)
42 Q_PROPERTY(QString noResultsHint READ noResultsHint WRITE setNoResultsHint NOTIFY noResultsHintChanged)
42 Q_PROPERTY(QString formFactor READ formFactor WRITE setFormFactor NOTIFY formFactorChanged)
43 Q_PROPERTY(QString formFactor READ formFactor WRITE setFormFactor NOTIFY formFactorChanged)
@@ -54,6 +55,7 @@ public:
54 bool visible() const;
55 bool visible() const;
55 QString shortcut() const;
56 QString shortcut() const;
56 bool connected() const;
57 bool connected() const;
58+ bool searchInProgress() const;
57 Categories* categories() const;
59 Categories* categories() const;
58 QString searchQuery() const;
60 QString searchQuery() const;
59 QString noResultsHint() const;
61 QString noResultsHint() const;
@@ -64,6 +66,7 @@ public:
64 void setSearchQuery(const QString& search_query);
66 void setSearchQuery(const QString& search_query);
65 void setNoResultsHint(const QString& hint);
67 void setNoResultsHint(const QString& hint);
66 void setFormFactor(const QString& form_factor);
68 void setFormFactor(const QString& form_factor);
69+ void setSearchInProgress(const bool inProg);
67
70
68 Q_INVOKABLE void activate(const QVariant &uri, const QVariant &icon_hint, const QVariant &category,
71 Q_INVOKABLE void activate(const QVariant &uri, const QVariant &icon_hint, const QVariant &category,
69 const QVariant &result_type, const QVariant &mimetype, const QVariant &title,
72 const QVariant &result_type, const QVariant &mimetype, const QVariant &title,
@@ -78,11 +81,11 @@ Q_SIGNALS:
78 void iconHintChanged(const QString&);
81 void iconHintChanged(const QString&);
79 void descriptionChanged(const QString&);
82 void descriptionChanged(const QString&);
80 void searchHintChanged(const QString&);
83 void searchHintChanged(const QString&);
84+ void searchInProgressChanged();
81 void visibleChanged(bool);
85 void visibleChanged(bool);
82 void shortcutChanged(const QString&);
86 void shortcutChanged(const QString&);
83 void connectedChanged(bool);
87 void connectedChanged(bool);
84 void categoriesChanged();
88 void categoriesChanged();
85− void searchFinished(const QString&);
86 void searchQueryChanged();
89 void searchQueryChanged();
87 void noResultsHintChanged();
90 void noResultsHintChanged();
88 void formFactorChanged();
91 void formFactorChanged();
@@ -103,6 +106,7 @@ protected:
103 QString m_noResultsHint;
106 QString m_noResultsHint;
104 QString m_formFactor;
107 QString m_formFactor;
105 bool m_visible;
108 bool m_visible;
109+ bool m_searching;
106
110
107 Categories* m_categories;
111 Categories* m_categories;
108 DeeListModel* m_results;
112 DeeListModel* m_results;
tests/qmltests/Components/tst_PageHeader.qml+28
@@ -19,12 +19,17 @@ import QtTest 1.0
19import ".."
19import ".."
20import "../../../Components"
20import "../../../Components"
21import Ubuntu.Components 0.1
21import Ubuntu.Components 0.1
22+import Unity 0.1
22import Unity.Test 0.1 as UT
23import Unity.Test 0.1 as UT
23
24
24Item {
25Item {
25 width: units.gu(110)
26 width: units.gu(110)
26 height: units.gu(30)
27 height: units.gu(30)
27
28
29+ Scope {
30+ id: scopeMock
31+ }
32+
28 UT.UnityTestCase {
33 UT.UnityTestCase {
29 name: "PageHeaderTest"
34 name: "PageHeaderTest"
30 when: windowShown
35 when: windowShown
@@ -132,6 +137,27 @@ Item {
132 compare(pageHeader.searchHistory.count, 3)
137 compare(pageHeader.searchHistory.count, 3)
133 compare(pageHeader.searchHistory.get(0).query, "humppa4")
138 compare(pageHeader.searchHistory.get(0).query, "humppa4")
134 }
139 }
140+
141+ function test_search_indicator() {
142+ var searchIndicator = findChild(pageHeader, "searchIndicator")
143+ var primaryImage = findChild(pageHeader, "primaryImage")
144+
145+ pageHeader.triggerSearch()
146+
147+ scopeMock.searchInProgress = false
148+ compare(searchIndicator.running, false, "Search indicator is running.")
149+ tryCompare(primaryImage, "visible", true)
150+
151+ scopeMock.searchInProgress = true
152+ compare(searchIndicator.running, true, "Search indicator isn't running.")
153+ tryCompare(primaryImage, "visible", false)
154+
155+ pageHeader.resetSearch()
156+ }
157+
158+ function cleanup() {
159+ scopeMock.searchInProgress = false
160+ }
135 }
161 }
136
162
137 Column {
163 Column {
@@ -145,6 +171,8 @@ Item {
145 right: parent.right
171 right: parent.right
146 }
172 }
147
173
174+ scope: scopeMock
175+
148 searchEntryEnabled: true
176 searchEntryEnabled: true
149 text: "%^$%^%^&%^&%^$%GHR%"
177 text: "%^$%^%^&%^&%^$%GHR%"
150 }
178 }