Merge lp:~tpeeters/ubuntu-ui-toolkit/cppHeader into lp:ubuntu-ui-toolkit/staging

Proposed by Tim Peeters
Status: Merged
Approved by: Tim Peeters
Approved revision: 1681
Merged at revision: 1648
Proposed branch: lp:~tpeeters/ubuntu-ui-toolkit/cppHeader
Merge into: lp:ubuntu-ui-toolkit/staging
Diff against target: 908 lines (+775/-7)
10 files modified
components.api (+4/-0)
src/Ubuntu/Components/ListItems/1.2/ListItemHeader.qml (+1/-0)
src/Ubuntu/Components/ListItems/1.3/ListItemHeader.qml (+1/-0)
src/Ubuntu/Components/ListItems/ListItems.pro (+2/-2)
src/Ubuntu/Components/ListItems/qmldir (+3/-3)
src/Ubuntu/Components/plugin/plugin.cpp (+2/-0)
src/Ubuntu/Components/plugin/plugin.pri (+4/-2)
src/Ubuntu/Components/plugin/ucheader.cpp (+328/-0)
src/Ubuntu/Components/plugin/ucheader.h (+76/-0)
tests/unit_x11/tst_components/tst_header.qml (+354/-0)
To merge this branch: bzr merge lp:~tpeeters/ubuntu-ui-toolkit/cppHeader
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Zsombor Egri Approve
Review via email: mp+271347@code.launchpad.net

Commit message

New Header C++ component.

Description of the change

New Header C++ component.

This will be used as the application header in following MRs.

Note that I had to disable the docs of the ListItems.Header by renaming ListItems/1.x/Header.qml to ListItemsHeader.qml and marking the component as deprecated. If I do not do this, The list item header 'hijacks" the properties of Ubuntu.Components.Header defined with \qmlproperty Header::flickable (for example).

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Zsombor Egri (zsombi) wrote :

Nice tests! I see you included some bug guards as well.

I have commented inline, reply on each if you want to keep the inline commenting active. I'll see them inline.

review: Needs Fixing
Revision history for this message
Tim Peeters (tpeeters) wrote :

All the proposed changes done (see inline comments), except the flickable.topMargin PropertyChange and the CPO. I'll push an update later with those two.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Zsombor Egri (zsombi) wrote :

Do you expect to be able to override the Header properties? if not (especially exposed and flickable, but I think all) then please make them FINAL!!!!

Also, see 2 comments inline. ListItem.header tests woudl be welcome!!!

review: Needs Fixing
Revision history for this message
Tim Peeters (tpeeters) wrote :

> Do you expect to be able to override the Header properties? if not (especially
> exposed and flickable, but I think all) then please make them FINAL!!!!
>
> Also, see 2 comments inline. ListItem.header tests woudl be welcome!!!

I made the properties FINAL in r1676.

About the ListItem.header I am not sure what to test. You can have code like this:
Item {
  Header {
    flickable: listView
  }
  ListView {
    id: listView
    anchors.fill: parent
    model: 50
  }
}

here the Header works exactly like with a Flickable, so no additional tests are needed.

Alternatively you can have code like this:

ListView {
    model: 50
    headerPositioning: ListView.PullBackHeader
    header: Component {
        Header { }
    }
}

here, (since you don't set the flickable of the header), the Header does not manipulate its y-value so it is positioned like any Item. What would I have to test there?

The two cases give similar results, but there is a difference when scrolling, and then releasing while the header is partially exposed: in the first case it will either show or hide completely (depending on how much it was exposed), it the second case it will just stay where it is.

Revision history for this message
Zsombor Egri (zsombi) wrote :

Looks good now! Looking forward to see the code removals when we put these in Page :)

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1681. By Tim Peeters

fix init

Revision history for this message
Zsombor Egri (zsombi) wrote :

Still good :)

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Tim Peeters (tpeeters) wrote :

CI approved it and jenkins?? set it back to "needs review"? Weird. I re-happroved it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'components.api'
2--- components.api 2015-09-14 11:32:40 +0000
3+++ components.api 2015-09-23 11:35:46 +0000
4@@ -389,6 +389,10 @@
5 property bool useDeprecatedToolbar
6 Ubuntu.Components.ListItems.Header 1.3: Item
7 property string text
8+Ubuntu.Components.Header 1.3: StyledItem
9+ property bool exposed
10+ property Flickable flickable
11+ readonly property bool moving
12 Ubuntu.Components.Icon 1.0 0.1: Item
13 property color color
14 property color keyColor
15
16=== renamed file 'src/Ubuntu/Components/ListItems/1.2/Header.qml' => 'src/Ubuntu/Components/ListItems/1.2/ListItemHeader.qml'
17--- src/Ubuntu/Components/ListItems/1.2/Header.qml 2015-04-30 08:32:44 +0000
18+++ src/Ubuntu/Components/ListItems/1.2/ListItemHeader.qml 2015-09-23 11:35:46 +0000
19@@ -18,6 +18,7 @@
20
21 /*!
22 \qmltype Header
23+ \deprecated
24 \inqmlmodule Ubuntu.Components.ListItems 1.0
25 \ingroup ubuntu-listitems
26 \brief Header for grouping list items together
27
28=== renamed file 'src/Ubuntu/Components/ListItems/1.3/Header.qml' => 'src/Ubuntu/Components/ListItems/1.3/ListItemHeader.qml'
29--- src/Ubuntu/Components/ListItems/1.3/Header.qml 2015-04-29 07:21:29 +0000
30+++ src/Ubuntu/Components/ListItems/1.3/ListItemHeader.qml 2015-09-23 11:35:46 +0000
31@@ -18,6 +18,7 @@
32
33 /*!
34 \qmltype Header
35+ \deprecated
36 \inqmlmodule Ubuntu.Components.ListItems 1.0
37 \ingroup ubuntu-listitems
38 \brief Header for grouping list items together
39
40=== modified file 'src/Ubuntu/Components/ListItems/ListItems.pro'
41--- src/Ubuntu/Components/ListItems/ListItems.pro 2015-05-19 09:23:01 +0000
42+++ src/Ubuntu/Components/ListItems/ListItems.pro 2015-09-23 11:35:46 +0000
43@@ -6,7 +6,7 @@
44 1.2/Empty.qml \
45 1.2/Expandable.qml \
46 1.2/ExpandablesColumn.qml \
47- 1.2/Header.qml \
48+ 1.2/ListItemHeader.qml \
49 1.2/IconVisual.qml \
50 1.2/ImageWithFallback.qml \
51 1.2/ItemSelector.qml \
52@@ -25,7 +25,7 @@
53 1.3/Empty.qml \
54 1.3/Expandable.qml \
55 1.3/ExpandablesColumn.qml \
56- 1.3/Header.qml \
57+ 1.3/ListItemHeader.qml \
58 1.3/IconVisual.qml \
59 1.3/ImageWithFallback.qml \
60 1.3/ItemSelector.qml \
61
62=== modified file 'src/Ubuntu/Components/ListItems/qmldir'
63--- src/Ubuntu/Components/ListItems/qmldir 2015-05-02 13:27:39 +0000
64+++ src/Ubuntu/Components/ListItems/qmldir 2015-09-23 11:35:46 +0000
65@@ -3,7 +3,7 @@
66 Base 0.1 1.2/Base.qml
67 Caption 0.1 1.2/Caption.qml
68 Divider 0.1 1.2/Divider.qml
69-Header 0.1 1.2/Header.qml
70+Header 0.1 1.2/ListItemHeader.qml
71 internal IconVisual IconVisual.qml
72 internal LabelVisual LabelVisual.qml
73 MultiValue 0.1 1.2/MultiValue.qml
74@@ -23,7 +23,7 @@
75 Base 1.0 1.2/Base.qml
76 Caption 1.0 1.2/Caption.qml
77 Divider 1.0 1.2/Divider.qml
78-Header 1.0 1.2/Header.qml
79+Header 1.0 1.2/ListItemHeader.qml
80 MultiValue 1.0 1.2/MultiValue.qml
81 ItemSelector 1.0 1.2/ItemSelector.qml
82 ValueSelector 1.0 1.2/ValueSelector.qml
83@@ -41,7 +41,7 @@
84 Base 1.3 1.3/Base.qml
85 Caption 1.3 1.3/Caption.qml
86 Divider 1.3 1.3/Divider.qml
87-Header 1.3 1.3/Header.qml
88+Header 1.3 1.3/ListItemHeader.qml
89 MultiValue 1.3 1.3/MultiValue.qml
90 ItemSelector 1.3 1.3/ItemSelector.qml
91 ValueSelector 1.3 1.3/ValueSelector.qml
92
93=== modified file 'src/Ubuntu/Components/plugin/plugin.cpp'
94--- src/Ubuntu/Components/plugin/plugin.cpp 2015-09-21 11:40:22 +0000
95+++ src/Ubuntu/Components/plugin/plugin.cpp 2015-09-23 11:35:46 +0000
96@@ -66,6 +66,7 @@
97 #include "ucactionitem.h"
98 #include "uchaptics.h"
99 #include "ucabstractbutton.h"
100+#include "ucheader.h"
101
102 #include <sys/types.h>
103 #include <unistd.h>
104@@ -240,6 +241,7 @@
105 qmlRegisterType<UCProportionalShape>(uri, 1, 3, "ProportionalShape");
106 qmlRegisterType<LiveTimer>(uri, 1, 3, "LiveTimer");
107 qmlRegisterType<UCAbstractButton>(uri, 1, 3, "AbstractButton");
108+ qmlRegisterType<UCHeader>(uri, 1, 3, "Header");
109 }
110
111 void UbuntuComponentsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
112
113=== modified file 'src/Ubuntu/Components/plugin/plugin.pri'
114--- src/Ubuntu/Components/plugin/plugin.pri 2015-09-21 11:40:22 +0000
115+++ src/Ubuntu/Components/plugin/plugin.pri 2015-09-23 11:35:46 +0000
116@@ -84,7 +84,8 @@
117 $$PWD/ucactionitem.h \
118 $$PWD/uchaptics.h \
119 $$PWD/ucabstractbutton.h \
120- $$PWD/ucthemingextension.h
121+ $$PWD/ucthemingextension.h \
122+ $$PWD/ucheader.h
123
124 SOURCES += $$PWD/plugin.cpp \
125 $$PWD/uctheme.cpp \
126@@ -141,7 +142,8 @@
127 $$PWD/ucactionitem.cpp \
128 $$PWD/uchaptics.cpp \
129 $$PWD/ucabstractbutton.cpp \
130- $$PWD/ucthemingextension.cpp
131+ $$PWD/ucthemingextension.cpp \
132+ $$PWD/ucheader.cpp
133
134 # adapters
135 SOURCES += $$PWD/adapters/alarmsadapter_organizer.cpp
136
137=== added file 'src/Ubuntu/Components/plugin/ucheader.cpp'
138--- src/Ubuntu/Components/plugin/ucheader.cpp 1970-01-01 00:00:00 +0000
139+++ src/Ubuntu/Components/plugin/ucheader.cpp 2015-09-23 11:35:46 +0000
140@@ -0,0 +1,328 @@
141+/*
142+ * Copyright 2015 Canonical Ltd.
143+ *
144+ * This program is free software; you can redistribute it and/or modify
145+ * it under the terms of the GNU Lesser General Public License as published by
146+ * the Free Software Foundation; version 3.
147+ *
148+ * This program is distributed in the hope that it will be useful,
149+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
150+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
151+ * GNU Lesser General Public License for more details.
152+ *
153+ * You should have received a copy of the GNU Lesser General Public License
154+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
155+ */
156+
157+#include "ucheader.h"
158+#include <QDebug>
159+#include <QtQuick/private/qquickflickable_p.h>
160+#include <QtQuick/private/qquickanchors_p.h>
161+#include <QtQuick/private/qquickanimation_p.h>
162+#include "ucubuntuanimation.h"
163+#include "ucunits.h"
164+#include "propertychange_p.h"
165+
166+
167+/*!
168+ \qmltype Header
169+ \instantiates UCHeader
170+ \inherits StyledItem
171+ \inqmlmodule Ubuntu.Components 1.3
172+ \ingroup ubuntu
173+ \since Ubuntu.Components 1.3
174+ \brief Header bar that can contain the title and controls for the current view.
175+
176+ The Header can be exposed and hidden by setting the
177+ \l exposed property, and when a \l flickable is set, the header will
178+ scroll together with the flickable and expose or hide when the
179+ Flickable movement ends.
180+
181+ \qml
182+ import QtQuick 2.4
183+ import Ubuntu.Components 1.3
184+
185+ Item {
186+ width: units.gu(50)
187+ height: units.gu(70)
188+
189+ Header {
190+ id: header
191+ width: parent.width
192+ height: units.gu(6)
193+ z: 1 // ensure the header goes on top of the flickable contents
194+ flickable: scrollableContent
195+
196+ Rectangle {
197+ // to visualize the header
198+ anchors.fill: parent
199+ color: UbuntuColors.blue
200+ }
201+ }
202+
203+ Flickable {
204+ id: scrollableContent
205+ anchors.fill: parent
206+ contentHeight: height * 2
207+ Label {
208+ text: "Flick me"
209+ }
210+ }
211+ }
212+ \endqml
213+
214+ The default z-value is 0, so declare the Header after any Items that it should
215+ overlay, or set its z-value to be larger than that of the other Items.
216+ The initial y-value is 0, but scrolling the flickable or setting \l exposed to
217+ false will change the y-value in the range from -height to 0.
218+*/
219+
220+UCUbuntuAnimation *UCHeader::s_ubuntuAnimation = new UCUbuntuAnimation();
221+
222+UCHeader::UCHeader(QQuickItem *parent)
223+ : UCStyledItemBase(parent)
224+ , m_flickable(Q_NULLPTR)
225+ , m_showHideAnimation(new QQuickNumberAnimation)
226+ , m_flickableTopMarginBackup(Q_NULLPTR)
227+ , m_previous_contentY(0)
228+ , m_exposed(true)
229+ , m_moving(false)
230+{
231+ m_showHideAnimation->setParent(this);
232+ m_showHideAnimation->setTargetObject(this);
233+ m_showHideAnimation->setProperty("y");
234+ m_showHideAnimation->setEasing(s_ubuntuAnimation->StandardEasing());
235+ m_showHideAnimation->setDuration(s_ubuntuAnimation->BriskDuration());
236+
237+ connect(m_showHideAnimation, SIGNAL(runningChanged(bool)),
238+ this, SLOT(_q_showHideAnimationRunningChanged()));
239+ connect(this, SIGNAL(heightChanged()), this, SLOT(_q_heightChanged()));
240+}
241+
242+UCHeader::~UCHeader() {
243+ if (m_flickable != Q_NULLPTR) {
244+ Q_ASSERT(m_flickableTopMarginBackup != Q_NULLPTR);
245+ delete m_flickableTopMarginBackup;
246+ }
247+}
248+
249+void UCHeader::_q_heightChanged() {
250+ updateFlickableMargins();
251+ if (m_exposed || (!m_flickable.isNull() && m_flickable->contentY() <= 0.0)) {
252+ // Header was exposed before, or the flickable is scrolled up close to
253+ // the top so that the header should be visible.
254+ show();
255+ } else {
256+ hide();
257+ }
258+}
259+
260+/*!
261+ * \qmlproperty Flickable Header::flickable
262+ *
263+ * When flickable is set, scrolling vertically in the flickable, or setting the
264+ * Flickable's y-value will move the header y-position by the same amount as the
265+ * flickable content movement. When scrolling the flickable, upon release, the header
266+ * will animate to fully exposed or fully hidden state, depending on whether it was
267+ * more or less than half exposed when the user stopped moving the flickable.
268+ *
269+ * When flickable is null, the header can be exposed or
270+ * hidden by setting the \l exposed property.
271+ *
272+ * The topMargin of the flickable will automatically be updated to always match
273+ * the height of the header. When changing the flickable, the topMargin of the previous
274+ * flickable is set to 0.
275+ *
276+ * It is permitted to use a ListView as the value of flickable, but this works
277+ * well only if the ListView.header property is not set. Alternatively,
278+ * a Header component may be used for ListView.header, but in that case the flickable
279+ * of the Header must be null.
280+ */
281+QQuickFlickable* UCHeader::flickable() {
282+ return m_flickable.data();
283+}
284+
285+void UCHeader::setFlickable(QQuickFlickable *flickable) {
286+ if (m_flickable == flickable) {
287+ return;
288+ }
289+ if (!m_flickable.isNull()) {
290+ Q_ASSERT(m_flickableTopMarginBackup != Q_NULLPTR);
291+
292+ // Finish the current header movement in case the current
293+ // flickable is disconnected while scrolling.
294+ _q_flickableMovementEnded();
295+ m_flickable->disconnect(this);
296+
297+ delete m_flickableTopMarginBackup; // Restores previous value/binding for topMargin.
298+ m_flickableTopMarginBackup = Q_NULLPTR;
299+ }
300+
301+ m_flickable = flickable;
302+ Q_EMIT flickableChanged();
303+
304+ Q_ASSERT(m_flickableTopMarginBackup == Q_NULLPTR);
305+ if (!m_flickable.isNull()) {
306+ m_flickableTopMarginBackup = new PropertyChange(m_flickable, "topMargin");
307+ connect(m_flickable, SIGNAL(contentYChanged()),
308+ this, SLOT(_q_scrolledContents()));
309+ connect(m_flickable, SIGNAL(movementEnded()),
310+ this, SLOT(_q_flickableMovementEnded()));
311+ connect(m_flickable, SIGNAL(contentHeightChanged()),
312+ this, SLOT(_q_contentHeightChanged()));
313+ connect(m_flickable, SIGNAL(interactiveChanged()),
314+ this, SLOT(_q_flickableInteractiveChanged()));
315+ m_previous_contentY = m_flickable->contentY();
316+ updateFlickableMargins();
317+ show();
318+ }
319+}
320+
321+void UCHeader::updateFlickableMargins() {
322+ if (m_flickable.isNull()) {
323+ return;
324+ }
325+ qreal headerHeight = height();
326+ qreal previousHeaderHeight = m_flickable->topMargin();
327+ if (headerHeight != previousHeaderHeight) {
328+ qreal previousContentY = m_flickable->contentY();
329+ PropertyChange::setValue(m_flickableTopMarginBackup, headerHeight);
330+ // Push down contents when header grows,
331+ // pull up contents when header shrinks.
332+ m_flickable->setContentY(previousContentY - headerHeight + previousHeaderHeight);
333+ }
334+}
335+
336+void UCHeader::show() {
337+ if (!m_exposed) {
338+ m_exposed = true;
339+ Q_EMIT exposedChanged();
340+ if (m_showHideAnimation->isRunning()) {
341+ // The header was in the process of hiding.
342+ m_showHideAnimation->stop();
343+ }
344+ }
345+ if (isComponentComplete()) {
346+ m_showHideAnimation->setFrom(y());
347+ m_showHideAnimation->setTo(0.0);
348+ m_showHideAnimation->start();
349+ } else {
350+ this->setY(0.0);
351+ if (m_moving) {
352+ m_moving = false;
353+ Q_EMIT movingChanged();
354+ }
355+ }
356+}
357+
358+void UCHeader::hide() {
359+ if (m_exposed) {
360+ m_exposed = false;
361+ Q_EMIT exposedChanged();
362+ if (m_showHideAnimation->isRunning()) {
363+ // The header was in the process of showing.
364+ m_showHideAnimation->stop();
365+ }
366+ }
367+ if (isComponentComplete()) {
368+ m_showHideAnimation->setFrom(y());
369+ m_showHideAnimation->setTo(-1.0*height());
370+ m_showHideAnimation->start();
371+ } else {
372+ this->setY(-1.0*height());
373+ if (m_moving) {
374+ m_moving = false;
375+ Q_EMIT movingChanged();
376+ }
377+ }
378+}
379+
380+void UCHeader::_q_showHideAnimationRunningChanged() {
381+ if (!m_showHideAnimation->isRunning()) {
382+ // Animation finished.
383+ Q_ASSERT(m_moving);
384+ m_moving = false;
385+ Q_EMIT movingChanged();
386+ } else if (!m_moving) {
387+ // Animation started.
388+ m_moving = true;
389+ Q_EMIT movingChanged();
390+ } // else: Transition from flickable movement to showHideAnimation running.
391+}
392+
393+/*!
394+ * \qmlproperty bool Header::exposed
395+ * Exposes and hides the header by animating its y-value between -height and 0
396+ * to move it in or out of its parent Item. The value of exposed can be set directly,
397+ * or it will be automatically updated when the user exposes or hides the Header
398+ * by scrolling the Header's \l flickable.
399+ */
400+void UCHeader::setExposed(bool exposed) {
401+ if (exposed) {
402+ show();
403+ } else {
404+ hide();
405+ }
406+}
407+
408+/*!
409+ * \qmlproperty bool Header::moving
410+ * \readonly
411+ * Indicates whether the header is currently moving, either because contentY of
412+ * the \l flickable changes (due to user interaction or by setting it directly),
413+ * or because the header is animating in or out because the value of \l exposed
414+ * was updated.
415+ */
416+bool UCHeader::moving() {
417+ return m_moving;
418+}
419+
420+// Called when moving due to user interaction with the flickable, or by
421+// setting m_flickable.contentY programatically.
422+void UCHeader::_q_scrolledContents() {
423+ Q_ASSERT(!m_flickable.isNull());
424+ // Avoid moving the header when rebounding or being dragged over the bounds.
425+ if (!m_flickable->isAtYBeginning() && !m_flickable->isAtYEnd()) {
426+ qreal dy = m_flickable->contentY() - m_previous_contentY;
427+ // Restrict the header y between -height and 0.
428+ qreal clampedY = qMin(qMax(-height(), y() - dy), 0.0);
429+ setY(clampedY);
430+ }
431+ m_previous_contentY = m_flickable->contentY();
432+ if (!m_moving) {
433+ m_moving = true;
434+ Q_EMIT movingChanged();
435+ }
436+ if (!m_flickable->isMoving()) {
437+ // m_flickable.contentY was set directly, so no user flicking.
438+ _q_flickableMovementEnded();
439+ }
440+}
441+
442+void UCHeader::_q_flickableMovementEnded() {
443+ Q_ASSERT(!m_flickable.isNull());
444+ if ((m_flickable->contentY() < 0)
445+ || (y() > -height()/2.0)) {
446+ show();
447+ } else {
448+ hide();
449+ }
450+}
451+
452+void UCHeader::_q_contentHeightChanged() {
453+ Q_ASSERT(!m_flickable.isNull());
454+ if (m_flickable->height() >= m_flickable->contentHeight()) {
455+ // The user cannot scroll down to expose the header, so ensure
456+ // that it is visible.
457+ show();
458+ }
459+}
460+
461+void UCHeader::_q_flickableInteractiveChanged() {
462+ Q_ASSERT(!m_flickable.isNull());
463+ if (!m_flickable->isInteractive()) {
464+ // The user cannot scroll down to expose the header, so ensure
465+ // that it is visible.
466+ show();
467+ }
468+}
469
470=== added file 'src/Ubuntu/Components/plugin/ucheader.h'
471--- src/Ubuntu/Components/plugin/ucheader.h 1970-01-01 00:00:00 +0000
472+++ src/Ubuntu/Components/plugin/ucheader.h 2015-09-23 11:35:46 +0000
473@@ -0,0 +1,76 @@
474+/*
475+ * Copyright 2015 Canonical Ltd.
476+ *
477+ * This program is free software; you can redistribute it and/or modify
478+ * it under the terms of the GNU Lesser General Public License as published by
479+ * the Free Software Foundation; version 3.
480+ *
481+ * This program is distributed in the hope that it will be useful,
482+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
483+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
484+ * GNU Lesser General Public License for more details.
485+ *
486+ * You should have received a copy of the GNU Lesser General Public License
487+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
488+ */
489+
490+#ifndef UCHEADER_H
491+#define UCHEADER_H
492+
493+#include "ucstyleditembase.h"
494+#include <QtCore/QPointer>
495+
496+class QQuickFlickable;
497+class QQuickNumberAnimation;
498+class UCUbuntuAnimation;
499+class PropertyChange;
500+
501+class UCHeader : public UCStyledItemBase
502+{
503+ Q_OBJECT
504+ Q_PROPERTY(QQuickFlickable* flickable READ flickable WRITE setFlickable NOTIFY flickableChanged FINAL)
505+ Q_PROPERTY(bool exposed MEMBER m_exposed WRITE setExposed NOTIFY exposedChanged FINAL)
506+ Q_PROPERTY(bool moving READ moving NOTIFY movingChanged FINAL)
507+
508+public:
509+ explicit UCHeader(QQuickItem *parent = 0);
510+ ~UCHeader();
511+
512+ QQuickFlickable* flickable();
513+ void setFlickable(QQuickFlickable* flickable);
514+ void setExposed(bool exposed);
515+ bool moving();
516+
517+Q_SIGNALS:
518+ void flickableChanged();
519+ void exposedChanged();
520+ void movingChanged();
521+
522+protected:
523+ void show();
524+ void hide();
525+
526+private Q_SLOTS:
527+ void _q_scrolledContents();
528+ void _q_showHideAnimationRunningChanged();
529+ void _q_flickableMovementEnded();
530+ void _q_contentHeightChanged();
531+ void _q_flickableInteractiveChanged();
532+ void _q_heightChanged();
533+
534+private:
535+ QPointer<QQuickFlickable> m_flickable;
536+ QQuickNumberAnimation* m_showHideAnimation;
537+ PropertyChange* m_flickableTopMarginBackup;
538+
539+ qreal m_previous_contentY;
540+ bool m_exposed:1;
541+ bool m_moving:1;
542+
543+ // used to set the easing and duration of m_showHideAnimation
544+ static UCUbuntuAnimation *s_ubuntuAnimation;
545+
546+ void updateFlickableMargins();
547+};
548+
549+#endif // UCHEADER_H
550
551=== added file 'tests/unit_x11/tst_components/tst_header.qml'
552--- tests/unit_x11/tst_components/tst_header.qml 1970-01-01 00:00:00 +0000
553+++ tests/unit_x11/tst_components/tst_header.qml 2015-09-23 11:35:46 +0000
554@@ -0,0 +1,354 @@
555+/*
556+ * Copyright (C) 2015 Canonical Ltd.
557+ *
558+ * This program is free software; you can redistribute it and/or modify
559+ * it under the terms of the GNU Lesser General Public License as published by
560+ * the Free Software Foundation; version 3.
561+ *
562+ * This program is distributed in the hope that it will be useful,
563+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
564+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
565+ * GNU Lesser General Public License for more details.
566+ *
567+ * You should have received a copy of the GNU Lesser General Public License
568+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
569+ */
570+
571+import QtQuick 2.4
572+import Ubuntu.Components 1.3
573+import Ubuntu.Test 1.0
574+
575+Item {
576+ id: root
577+ width: units.gu(50)
578+ height: units.gu(70)
579+
580+ property real initialHeaderHeight: units.gu(6)
581+
582+ Header {
583+ id: header
584+ flickable: flickable
585+ z:1
586+ width: parent.width
587+ height: root.initialHeaderHeight
588+
589+ Rectangle {
590+ // to visualize the header
591+ anchors.fill: parent
592+ color: UbuntuColors.red
593+ opacity: 0.5
594+ border {
595+ color: "black"
596+ width: 2
597+ }
598+ }
599+ }
600+
601+ Flickable {
602+ id: flickable
603+ anchors.fill: parent
604+ contentHeight: height * 2
605+
606+ Grid {
607+ id: switchGrid
608+ columns: 2
609+ spacing: units.gu(1)
610+ anchors {
611+ top: parent.top
612+ left: parent.left
613+ margins: units.gu(5)
614+ }
615+ Switch {
616+ id: lockedSwitch
617+ checked: null === header.flickable
618+ function trigger() {
619+ if (header.flickable) {
620+ header.flickable = null;
621+ } else {
622+ header.flickable = flickable;
623+ }
624+ }
625+ }
626+ Label {
627+ text: "header locked"
628+ }
629+ Switch {
630+ id: hiddenSwitch
631+ checked: header.exposed
632+ function trigger() {
633+ header.exposed = !header.exposed;
634+ }
635+ }
636+ Label {
637+ text: "header exposed"
638+ }
639+ Item {
640+ width: 1
641+ height: 1
642+ }
643+ }
644+ Button {
645+ id: contentYButton
646+ anchors {
647+ top: switchGrid.bottom
648+ topMargin: units.gu(4)
649+ horizontalCenter: parent.horizontalCenter
650+ }
651+ property real newY: flickable.contentY == 0 ? -header.height : 0
652+ onClicked: flickable.contentY = newY;
653+ text: "Set contentY to " + newY
654+ }
655+ Label {
656+ anchors {
657+ top : contentYButton.bottom
658+ horizontalCenter: parent.horizontalCenter
659+ topMargin: units.gu(8)
660+ }
661+ text: "Flick me"
662+ }
663+ }
664+
665+ Rectangle {
666+ id: reparentTestItem
667+ anchors {
668+ right: parent.right
669+ bottom: parent.bottom
670+ }
671+ height: units.gu(15)
672+ width: parent.width / 2
673+ color: "blue"
674+
675+ Label {
676+ anchors {
677+ horizontalCenter: parent.horizontalCenter
678+ bottom: parent.bottom
679+ }
680+ text: "Click to reparent"
681+ color: "white"
682+ }
683+
684+ MouseArea {
685+ anchors.fill: parent
686+ onClicked: {
687+ if (header.parent === root) {
688+ header.parent = reparentTestItem;
689+ } else {
690+ header.parent = root;
691+ }
692+ }
693+ }
694+ }
695+
696+ Flickable {
697+ id: otherFlickable
698+ property real initialTopMargin: 123
699+ topMargin: initialTopMargin
700+ height: units.gu(10)
701+ contentHeight: units.gu(20);
702+ }
703+
704+ Header {
705+ id: otherHeader
706+ }
707+
708+ UbuntuTestCase {
709+ name: "Header"
710+ when: windowShown
711+ id: testCase
712+
713+ function initTestCase() {
714+ wait_for_exposed(true, "Header is not exposed initially.");
715+ compare(otherHeader.flickable, null, "Flickable not null by default.");
716+ compare(header.flickable, flickable, "Flickable not properly intialized.");
717+ // note: moving may be true briefly due to header height changes, but
718+ // it does not change in the initialization after wait_for_exposed() above.
719+ compare(header.moving, false, "Header moving initially.");
720+ }
721+
722+ function init() {
723+ flickable.contentHeight = 2*flickable.height;
724+ flickable.interactive = true;
725+ flickable.contentY = -header.height;
726+ header.exposed = true;
727+ wait_for_exposed(true);
728+ }
729+
730+ function scroll(dy) {
731+ var p = centerOf(flickable);
732+ // Use mouseWheel to scroll because mouseDrag is very unreliable
733+ // and does not properly handle negative values for dy.
734+ mouseWheel(flickable, p.x, p.y, 0,dy);
735+ }
736+
737+ function scroll_down() {
738+ scroll(-2.0*header.height);
739+ }
740+
741+ function scroll_up() {
742+ scroll(header.height);
743+ }
744+
745+ function wait_for_exposed(exposed, errorMessage) {
746+ tryCompare(header, "exposed", exposed, 5000, errorMessage);
747+ // wait for the animation to finish:
748+ tryCompare(header, "moving", false, 5000, "Header still moving?");
749+ if (exposed) {
750+ compare(header.y, 0, errorMessage +
751+ " y-value/exposed mismatch for exposed header!");
752+ } else {
753+ compare(header.y, -header.height, errorMessage +
754+ " y-value/exposed mismatch for hidden header!");
755+ }
756+ }
757+
758+ function test_reparent_width() {
759+ // test initial header width:
760+ compare(header.parent, root);
761+ compare(header.width, root.width);
762+ compare(header.y, 0);
763+
764+ // test width update when changing parent:
765+ header.parent = reparentTestItem;
766+ compare(header.parent, reparentTestItem);
767+ compare(header.width, reparentTestItem.width);
768+ compare(header.y, 0);
769+
770+ // test width update when changing width of parent:
771+ var old_width = reparentTestItem.width;
772+ reparentTestItem.width = units.gu(5);
773+ compare(header.width, reparentTestItem.width);
774+ compare(header.y, 0);
775+
776+ // revert to original parent:
777+ header.parent = root;
778+ compare(header.parent, root);
779+ compare(header.width, root.width);
780+ compare(header.y, 0);
781+ reparentTestItem.width = old_width;
782+ }
783+
784+ function test_height_change() {
785+ // first scroll down for this test, the following test repeats
786+ // the cases at the top of the flickable.
787+ scroll_down();
788+ header.height = units.gu(15);
789+ wait_for_exposed(true, "Increasing header height hides header.");
790+ scroll_down();
791+ header.exposed = false;
792+ wait_for_exposed(false, "Header with height set does not hide.");
793+ header.height = units.gu(2);
794+ wait_for_exposed(false, "Decreasing header height exposes it.");
795+ header.exposed = true;
796+ wait_for_exposed(true, "Header with decreased height does not expose.");
797+
798+ // revert to initial state
799+ header.height = root.initialHeaderHeight;
800+ flickable.contentY = -header.height;
801+ wait_for_exposed(true, "Setting flickable.contentY hides the header.");
802+ }
803+
804+ function test_height_change_at_top() {
805+ // Near the top, changing the header height exposes the header
806+ // to avoid the header becoming inaccessible because it cannot
807+ // be pulled down.
808+ header.exposed = false;
809+ wait_for_exposed(false);
810+ header.height = units.gu(15);
811+ wait_for_exposed(true, "Increasing header height at top hides header.");
812+
813+ // making the header smaller does not need to expose it, because there is
814+ // enough space to pull it down.
815+
816+ // revert to original state.
817+ header.height = root.initialHeaderHeight;
818+ flickable.contentY = -header.height;
819+ header.exposed = true;
820+ wait_for_exposed(true);
821+ }
822+
823+ function test_set_exposed_to_hide_and_show() {
824+ header.exposed = false;
825+ wait_for_exposed(false, "Cannot hide header by setting visible to false.");
826+ header.exposed = true;
827+ wait_for_exposed(true, "Cannot show header by setting visible to true.");
828+
829+ // change the value of exposed twice quickly:
830+ header.exposed = false;
831+ header.exposed = true;
832+ wait_for_exposed(true, "Quickly hiding and showing header does not result in exposed header.");
833+
834+ // and the other way around:
835+ header.exposed = false;
836+ wait_for_exposed(false);
837+ header.exposed = true;
838+ header.exposed = false;
839+ wait_for_exposed(false, "Quickly showing and hiding header does not result in hidden header.");
840+
841+ header.exposed = true;
842+ wait_for_exposed(true);
843+ }
844+
845+ function test_scroll_updates_visible() {
846+ scroll_down();
847+ wait_for_exposed(false, "Scrolling down does not hide header.");
848+ scroll_up();
849+ wait_for_exposed(true, "Scrolling up does not show header.");
850+ }
851+
852+ function test_flickable_margins() {
853+ compare(flickable.topMargin, header.height, "Flickable top margin does not match header height.");
854+ header.height = units.gu(15);
855+ wait_for_exposed(true, "Increasing header height at top hides header.");
856+ compare(flickable.topMargin, header.height, "Updating header height does not update flickable top margin.");
857+
858+ header.height = root.initialHeaderHeight; // revert
859+ wait_for_exposed(true, "Reverting header height at top hides header.");
860+ compare(flickable.topMargin, header.height, "Reverting header height does not revert flickable top margin.");
861+
862+ compare(otherFlickable.topMargin, otherFlickable.initialTopMargin, "Flickable top margin is not initialized properly.");
863+ header.flickable = otherFlickable;
864+ compare(otherFlickable.topMargin, header.height, "Setting flickable does not update flickable top margin.");
865+ compare(flickable.topMargin, 0, "Changing the flickable does not reset the previous flickable top margin to 0.");
866+
867+ header.flickable = flickable;
868+ compare(otherFlickable.topMargin, otherFlickable.initialTopMargin, "Reverting flickable does not restore the other flickable top margin.");
869+ compare(flickable.topMargin, header.height, "Reverting flickable breaks flickable top margin.");
870+ }
871+
872+ function test_flickable_contentHeight_bug1156573() {
873+ var old_height = flickable.contentHeight;
874+ header.exposed = false;
875+ wait_for_exposed(false);
876+ flickable.contentHeight = flickable.height / 2;
877+ wait_for_exposed(true, "Small content height does not expose the header.");
878+
879+ // revert:
880+ flickable.contentHeight = old_height;
881+ compare(header.exposed, true, "Reverting flickable content height hides the header.");
882+ }
883+
884+ function test_flickable_interactive() {
885+ header.exposed = false;
886+ wait_for_exposed(false);
887+ flickable.interactive = false;
888+ wait_for_exposed(true, "Making flickable not interactive does not expose the header.");
889+
890+ // revert:
891+ flickable.interactive = true;
892+ compare(header.exposed, true, "Reverting flickable exposed hides the header.");
893+ }
894+
895+ function test_scroll_disconnected_flickable() {
896+ var hy = header.y;
897+ header.flickable = null;
898+ scroll_down();
899+ compare(header.y, hy, "Header scrolls when disconnected flickable scrolls down.");
900+ wait_for_exposed(true, "Scrolling down disconnected flickable hides header.");
901+ scroll_up();
902+ compare(header.y, hy, "Header scrolls when disconnected flickable scrolls up.");
903+ wait_for_exposed(true, "Scrolling up disconnected flickable hides header.");
904+
905+ header.flickable = flickable;
906+ }
907+ }
908+}

Subscribers

People subscribed via source and target branches