on xenial help sign in
Make Greeter nicely handle being resized

- Introduced support for bidirectional DragHandles - Refactored greeter to use a bidirectional DragHandle - Added code to ensure Greeter retains its relative positioning when Shell is resized Approved by: Michael Terry

commit852e1cfca72bdae1a6b48ba07b185e187320b771 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateMon 15 Dec 2014, 22:41 (12 years ago) parent2dc0cae3cd3f62
15 files changed+422−187
plugins/Ubuntu/Gestures/Direction.cpp+6−2 plugins/Ubuntu/Gestures/Direction.h+2−1 plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+27−37 qml/Components/DragHandle.qml+10−2 qml/Components/EdgeDragEvaluator.qml+11−5 qml/Components/Showable.qml+13 qml/Greeter/Greeter.qml+91−82 qml/Greeter/Infographics.qml−3 qml/Shell.qml+12−4 tests/qmltests/Components/tst_DragHandle.cpp+4 tests/qmltests/Components/tst_DragHandle.qml+43−8 tests/qmltests/Components/tst_DragHandle/BidirectionalShowable.qml+69 tests/qmltests/Greeter/tst_MultiGreeter.qml+15−15 tests/qmltests/Greeter/tst_SingleGreeter.qml+104−16 tests/qmltests/tst_TabletShell.qml+15−12
plugins/Ubuntu/Gestures/Direction.cpp+6−2
@@ -18,7 +18,9 @@
1818
1919bool Direction::isHorizontal(Direction::Type type)
2020{
21− return type == Direction::Leftwards || type == Direction::Rightwards;
21+ return type == Direction::Leftwards
22+ || type == Direction::Rightwards
23+ || type == Direction::Horizontal;
2224}
2325
2426bool Direction::isVertical(Direction::Type type)
@@ -28,5 +30,7 @@ bool Direction::isVertical(Direction::Type type)
2830
2931bool Direction::isPositive(Direction::Type type)
3032{
31− return type == Rightwards || type == Downwards;
33+ return type == Rightwards
34+ || type == Downwards
35+ || type == Horizontal;
3236}
plugins/Ubuntu/Gestures/Direction.h+2−1
@@ -33,7 +33,8 @@ public:
3333 Rightwards, // Along the positive direction of the X axis
3434 Leftwards, // Along the negative direction of the X axis
3535 Downwards, // Along the positive direction of the Y axis
36− Upwards // Along the negative direction of the Y axis
36+ Upwards, // Along the negative direction of the Y axis
37+ Horizontal // Along the X axis, in any direction
3738 };
3839
3940 Q_INVOKABLE static bool isHorizontal(Direction::Type type);
plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+27−37
@@ -36,7 +36,7 @@
3636using namespace UbuntuGestures;
3737
3838#if DIRECTIONALDRAGAREA_DEBUG
39−#define DDA_DEBUG(msg) qDebug("[DDA] " msg)
39+#define ddaDebug(params) qDebug().nospace() << "[DDA(" << qPrintable(objectName()) << ")] " << params
4040#include "DebugHelpers.h"
4141
4242namespace {
@@ -53,7 +53,7 @@ const char *statusToString(DirectionalDragArea::Status status)
5353
5454} // namespace {
5555#else // DIRECTIONALDRAGAREA_DEBUG
56−#define DDA_DEBUG(msg) do{}while(0)
56+#define ddaDebug(params) ((void)0)
5757#endif // DIRECTIONALDRAGAREA_DEBUG
5858
5959
@@ -253,7 +253,7 @@ void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event)
253253 if (event->gained()) {
254254 QVector<int> ids;
255255 ids.append(event->touchId());
256− DDA_DEBUG("grabbing touch");
256+ ddaDebug("grabbing touch");
257257 grabTouchPoints(ids);
258258
259259 // Work around for Qt bug. If we grab a touch that is being used for mouse pointer
@@ -265,7 +265,7 @@ void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event)
265265 if (window()) {
266266 QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(window());
267267 if (windowPrivate->touchMouseId == event->touchId() && window()->mouseGrabberItem()) {
268− DDA_DEBUG("removing mouse grabber");
268+ ddaDebug("removing mouse grabber");
269269 window()->mouseGrabberItem()->ungrabMouse();
270270 }
271271 }
@@ -283,11 +283,7 @@ void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent
283283
284284 Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed));
285285
286− #if DIRECTIONALDRAGAREA_DEBUG
287− // TODO Consider using qCDebug() when available (Qt 5.2)
288− qDebug() << "[DDA] Unowned" << m_timeSource->msecsSinceReference()
289− << qPrintable(touchEventToString(event));
290− #endif
286+ ddaDebug("Unowned " << m_timeSource->msecsSinceReference() << " " << qPrintable(touchEventToString(event)));
291287
292288 switch (m_status) {
293289 case WaitingForTouch:
@@ -322,7 +318,7 @@ void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unowned
322318
323319 if (touchPoint->state() == Qt::TouchPointReleased) {
324320 // touch has ended before recognition concluded
325− DDA_DEBUG("Touch has ended before recognition concluded");
321+ ddaDebug("Touch has ended before recognition concluded");
326322 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
327323 emitSignalIfTapped();
328324 setStatus(WaitingForTouch);
@@ -335,7 +331,7 @@ void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unowned
335331 updateVelocityCalculator(touchScenePos);
336332
337333 if (!pointInsideAllowedArea()) {
338− DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
334+ ddaDebug("Rejecting gesture because touch point is outside allowed area.");
339335 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
340336 // We still wanna know when it ends for keeping the composition time window up-to-date
341337 TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
@@ -344,7 +340,7 @@ void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unowned
344340 }
345341
346342 if (!movingInRightDirection()) {
347− DDA_DEBUG("Rejecting gesture because touch point is moving in the wrong direction.");
343+ ddaDebug("Rejecting gesture because touch point is moving in the wrong direction.");
348344 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
349345 // We still wanna know when it ends for keeping the composition time window up-to-date
350346 TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
@@ -358,7 +354,7 @@ void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unowned
358354 if (isWithinTouchCompositionWindow()) {
359355 // There's still time for some new touch to appear and ruin our party as it would be combined
360356 // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
361− DDA_DEBUG("Sill within composition window. Let's wait more.");
357+ ddaDebug("Sill within composition window. Let's wait more.");
362358 return;
363359 }
364360
@@ -366,7 +362,7 @@ void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unowned
366362 TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
367363 setStatus(Recognized);
368364 } else {
369− DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
365+ ddaDebug("Didn't move far enough yet. Let's wait more.");
370366 }
371367}
372368
@@ -375,11 +371,7 @@ void DirectionalDragArea::touchEvent(QTouchEvent *event)
375371 // TODO: Consider when more than one touch starts in the same event (although it's not possible
376372 // with Mir's android-input). Have to track them all. Consider it a plus/bonus.
377373
378− #if DIRECTIONALDRAGAREA_DEBUG
379− // TODO Consider using qCDebug() when available (Qt 5.2)
380− qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
381− << qPrintable(touchEventToString(event));
382− #endif
374+ ddaDebug(m_timeSource->msecsSinceReference() << " " << qPrintable(touchEventToString(event)));
383375
384376 if (!isEnabled() || !isVisible()) {
385377 QQuickItem::touchEvent(event);
@@ -416,9 +408,7 @@ void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
416408 if (isWithinTouchCompositionWindow()) {
417409 // too close to the last touch start. So we consider them as starting roughly at the same time.
418410 // Can't be a single-touch gesture.
419− #if DIRECTIONALDRAGAREA_DEBUG
420− qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
421− #endif
411+ ddaDebug("A new touch point came in but we're still within time composition window. Ignoring it.");
422412 allGood = false;
423413 }
424414
@@ -456,6 +446,7 @@ void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
456446
457447 if (recognitionIsDisabled()) {
458448 // Behave like a dumb TouchArea
449+ ddaDebug("Gesture recognition is disabled. Requesting touch ownership immediately.");
459450 TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
460451 setStatus(Recognized);
461452 event->accept();
@@ -487,7 +478,7 @@ void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
487478
488479 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
489480 // multi-finger drags are not accepted
490− DDA_DEBUG("Multi-finger drags are not accepted");
481+ ddaDebug("Multi-finger drags are not accepted");
491482
492483 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
493484 // We still wanna know when it ends for keeping the composition time window up-to-date
@@ -581,12 +572,16 @@ bool DirectionalDragArea::pointInsideAllowedArea() const
581572
582573bool DirectionalDragArea::movingInRightDirection() const
583574{
584− QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
585− m_dampedScenePos.y() - m_previousDampedScenePos.y());
575+ if (m_direction == Direction::Horizontal) {
576+ return true;
577+ } else {
578+ QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
579+ m_dampedScenePos.y() - m_previousDampedScenePos.y());
586580
587− qreal scalarProjection = projectOntoDirectionVector(movementVector);
581+ qreal scalarProjection = projectOntoDirectionVector(movementVector);
588582
589− return scalarProjection >= 0.;
583+ return scalarProjection >= 0.;
584+ }
590585}
591586
592587bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
@@ -614,7 +609,7 @@ void DirectionalDragArea::checkSpeed()
614609 qreal minSpeedMsecs = m_minSpeed / 1000.0;
615610
616611 if (speed < minSpeedMsecs) {
617− DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
612+ ddaDebug("Rejecting gesture because it's below minimum speed.");
618613 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
619614 TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
620615 setStatus(WaitingForTouch);
@@ -625,7 +620,7 @@ void DirectionalDragArea::checkSpeed()
625620 m_silenceTime += m_recognitionTimer->interval();
626621
627622 if (m_silenceTime > m_maxSilenceTime) {
628− DDA_DEBUG("Rejecting gesture because its silence time has been exceeded.");
623+ ddaDebug("Rejecting gesture because its silence time has been exceeded.");
629624 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
630625 TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
631626 setStatus(WaitingForTouch);
@@ -646,7 +641,7 @@ void DirectionalDragArea::giveUpIfDisabledOrInvisible()
646641 }
647642
648643 if (m_status != WaitingForTouch) {
649− DDA_DEBUG("Resetting status because got disabled or made invisible");
644+ ddaDebug("Resetting status because got disabled or made invisible");
650645 setStatus(WaitingForTouch);
651646 }
652647 }
@@ -666,9 +661,7 @@ void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
666661 m_status = newStatus;
667662 Q_EMIT statusChanged(m_status);
668663
669− #if DIRECTIONALDRAGAREA_DEBUG
670− qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
671− #endif
664+ ddaDebug(statusToString(oldStatus) << " -> " << statusToString(newStatus));
672665
673666 switch (newStatus) {
674667 case WaitingForTouch:
@@ -875,7 +868,7 @@ void DirectionalDragArea::updateSceneDirectionVector()
875868 localDirection.rx() = -1.;
876869 localDirection.ry() = 0.;
877870 break;
878− default: // Direction::Rightwards:
871+ default: // Direction::Rightwards || Direction.Horizontal
879872 localDirection.rx() = 1.;
880873 localDirection.ry() = 0.;
881874 break;
@@ -891,6 +884,3 @@ qreal DirectionalDragArea::projectOntoDirectionVector(const QPointF &sceneVector
891884 return sceneVector.x() * m_sceneDirectionVector.x() +
892885 sceneVector.y() * m_sceneDirectionVector.y();
893886}
894−
895−// Because we are defining a new QObject-based class (RecognitionTimer) here.
896−#include "DirectionalDragArea.moc"
qml/Components/DragHandle.qml+10−2
@@ -100,8 +100,16 @@ EdgeDragArea {
100100 id: d
101101 property var previousStatus: DirectionalDragArea.WaitingForTouch
102102 property real startValue
103− property real minValue: Direction.isPositive(direction) ? startValue
104− : startValue - maxTotalDragDistance
103+ property real minValue: {
104+ if (direction == Direction.Horizontal) {
105+ return startValue - maxTotalDragDistance;
106+ } else if (Direction.isPositive(direction)) {
107+ return startValue;
108+ } else {
109+ return startValue - maxTotalDragDistance;
110+ }
111+ }
112+
105113 property real maxValue: Direction.isPositive(direction) ? startValue + maxTotalDragDistance
106114 : startValue
107115
qml/Components/EdgeDragEvaluator.qml+11−5
@@ -53,20 +53,26 @@ AxisVelocityCalculator {
5353 // Returns whether the drag should continue by itself until completed.
5454 function shouldAutoComplete() {
5555 var deltaPos = trackedPosition - __startPosition;
56+
5657 if (Math.abs(deltaPos) < minDragDistance) {
5758 velocity = 0;
5859 minVelocity = 0;
5960 return false;
6061 }
6162
62− minVelocity = __calculateMinimumVelocityForAutoCompletion(deltaPos);
6363 velocity = calculate();
6464
65− if (Direction.isPositive(direction) && (velocity >= minVelocity)
66− || !Direction.isPositive(direction) && (velocity <= minVelocity)) {
67− return true;
65+ if (direction == Direction.Horizontal) {
66+ minVelocity = __calculateMinimumVelocityForAutoCompletion(Math.abs(deltaPos));
67+ return Math.abs(velocity) >= minVelocity;
6868 } else {
69− return false;
69+ minVelocity = __calculateMinimumVelocityForAutoCompletion(deltaPos);
70+ if (Direction.isPositive(direction) && (velocity >= minVelocity)
71+ || !Direction.isPositive(direction) && (velocity <= minVelocity)) {
72+ return true;
73+ } else {
74+ return false;
75+ }
7076 }
7177 }
7278
qml/Components/Showable.qml+13
@@ -99,10 +99,23 @@ Item {
9999 return true;
100100 }
101101
102+ /*
103+ Will be called right before starting the hideAnimation.
104+ */
105+ property var prepareToHide: function(){}
106+
102107 function hide() {
103108 if (showAnimation != undefined && showAnimation.running) {
104109 showAnimation.stop()
105110 }
111+
112+ if (typeof prepareToHide === "function") {
113+ prepareToHide();
114+ } else {
115+ console.warn("Showable.prepareToHide should be a function, but it's a " +
116+ (typeof prepareToHide) + " instead");
117+ }
118+
106119 if (hideAnimation != undefined) {
107120 if (!hideAnimation.running) {
108121 hideAnimation.restart()
qml/Greeter/Greeter.qml+91−82
@@ -25,16 +25,26 @@ Showable {
2525 enabled: shown
2626 created: greeterContentLoader.status == Loader.Ready && greeterContentLoader.item.ready
2727
28+ property real dragHandleLeftMargin: 0
29+ property alias dragging: dragHandle.dragging
30+
2831 property url background
32+
33+ prepareToHide: function () {
34+ hideTranslation.to = greeter.x > 0 || d.forceRightOnNextHideAnimation ? greeter.width : -greeter.width;
35+ d.forceRightOnNextHideAnimation = false;
36+ }
37+
38+ QtObject {
39+ id: d
40+ property bool forceRightOnNextHideAnimation: false
41+ }
42+
2943 property bool loadContent: required
3044
3145 // 1 when fully shown and 0 when fully hidden
32− property real showProgress: MathUtils.clamp((width - Math.abs(x)) / width, 0, 1)
33−
34− showAnimation: StandardAnimation { property: "x"; to: 0; duration: UbuntuAnimation.FastDuration }
35− hideAnimation: __leftHideAnimation
46+ property real showProgress: visible ? MathUtils.clamp((width - Math.abs(x)) / width, 0, 1) : 0
3647
37− property alias dragHandleWidth: dragHandle.width
3848 property alias model: greeterContentLoader.model
3949 property bool locked: true
4050
@@ -43,18 +53,13 @@ Showable {
4353
4454 readonly property int currentIndex: greeterContentLoader.currentIndex
4555
46− property var __leftHideAnimation: StandardAnimation { property: "x"; to: -width }
47− property var __rightHideAnimation: StandardAnimation { property: "x"; to: width }
48−
4956 signal selected(int uid)
5057 signal unlocked(int uid)
51− signal tease()
58+ signal tapped()
5259
5360 function hideRight() {
54− if (shown) {
55− hideAnimation = __rightHideAnimation
56− hide()
57− }
61+ d.forceRightOnNextHideAnimation = true;
62+ hide();
5863 }
5964
6065 function tryToUnlock() {
@@ -69,71 +74,9 @@ Showable {
6974 }
7075 }
7176
72− onRequiredChanged: {
73− // Reset hide animation to default once we're finished with it
74− if (required) {
75− // Reset hide animation so that a hide() call is reliably left
76− hideAnimation = __leftHideAnimation
77− }
78− }
79−
80− // Bi-directional revealer
81− DraggingArea {
82− id: dragHandle
83− anchors.fill: parent
84− enabled: (greeter.narrowMode || !greeter.locked) && greeter.enabled && greeter.shown
85− orientation: Qt.Horizontal
86− propagateComposedEvents: true
87−
88− Component.onCompleted: {
89− // set evaluators to baseline of dragValue == 0
90− leftEvaluator.reset()
91− rightEvaluator.reset()
92− }
93−
94− function maybeTease() {
95− if (!greeter.locked || greeter.narrowMode)
96− greeter.tease();
97− }
98−
99− onClicked: maybeTease()
100− onDragStart: maybeTease()
101− onPressAndHold: {} // eat event, but no need to tease, as drag will cover it
102−
103− onDragEnd: {
104− if (greeter.x > 0 && rightEvaluator.shouldAutoComplete()) {
105− greeter.hideRight()
106− } else if (greeter.x < 0 && leftEvaluator.shouldAutoComplete()) {
107− greeter.hide();
108− } else {
109− greeter.show(); // undo drag
110− }
111− }
112−
113− onDragValueChanged: {
114− // dragValue is kept as a "step" value since we do this x adjusting on the fly
115− greeter.x += dragValue
116− }
117−
118− EdgeDragEvaluator {
119− id: rightEvaluator
120− trackedPosition: dragHandle.dragValue + greeter.x
121− maxDragDistance: parent.width
122− direction: Direction.Rightwards
123− }
124−
125− EdgeDragEvaluator {
126− id: leftEvaluator
127− trackedPosition: dragHandle.dragValue + greeter.x
128− maxDragDistance: parent.width
129− direction: Direction.Leftwards
130− }
131− }
132− TouchGate {
133− targetItem: dragHandle
134− anchors.fill: targetItem
135− enabled: targetItem.enabled
136− }
77+ // event eater
78+ // Nothing should leak to items behind the greeter
79+ MouseArea { anchors.fill: parent }
13780
13881 Loader {
13982 id: greeterContentLoader
@@ -161,11 +104,27 @@ Showable {
161104 }
162105 }
163106
164− onTease: showLabelAnimation.start()
107+ DragHandle {
108+ id: dragHandle
109+ anchors.fill: parent
110+ anchors.leftMargin: greeter.dragHandleLeftMargin
111+ enabled: (greeter.narrowMode || !greeter.locked) && greeter.enabled && greeter.shown
112+ direction: Direction.Horizontal
113+
114+ onTapped: {
115+ greeter.tapped();
116+ showLabelAnimation.start();
117+ }
118+
119+ onDraggingChanged: {
120+ if (dragging) {
121+ showLabelAnimation.start();
122+ }
123+ }
124+ }
165125
166126 Label {
167127 id: swipeHint
168− visible: greeter.shown
169128 property real baseOpacity: 0.5
170129 opacity: 0.0
171130 anchors.horizontalCenter: parent.horizontalCenter
@@ -199,7 +158,6 @@ Showable {
199158 anchors.left: parent.right
200159 anchors.top: parent.top
201160 anchors.bottom: parent.bottom
202− visible: parent.required
203161 fillMode: Image.Tile
204162 source: "../graphics/dropshadow_right.png"
205163 }
@@ -209,8 +167,59 @@ Showable {
209167 anchors.right: parent.left
210168 anchors.top: parent.top
211169 anchors.bottom: parent.bottom
212− visible: parent.required
213170 fillMode: Image.Tile
214171 source: "../graphics/dropshadow_left.png"
215172 }
173+
174+ Binding {
175+ id: positionLock
176+
177+ property bool enabled: false
178+ onEnabledChanged: {
179+ if (enabled === __enabled) {
180+ return;
181+ }
182+
183+ if (enabled) {
184+ if (greeter.x > 0) {
185+ value = Qt.binding(function() { return greeter.width; })
186+ } else {
187+ value = Qt.binding(function() { return -greeter.width; })
188+ }
189+ }
190+
191+ __enabled = enabled;
192+ }
193+
194+ property bool __enabled: false
195+
196+ target: greeter
197+ when: __enabled
198+ property: "x"
199+ }
200+
201+ hideAnimation: SequentialAnimation {
202+ id: hideAnimation
203+ objectName: "hideAnimation"
204+ StandardAnimation {
205+ id: hideTranslation
206+ property: "x"
207+ target: greeter
208+ }
209+ PropertyAction { target: greeter; property: "visible"; value: false }
210+ PropertyAction { target: positionLock; property: "enabled"; value: true }
211+ }
212+
213+ showAnimation: SequentialAnimation {
214+ id: showAnimation
215+ objectName: "showAnimation"
216+ PropertyAction { target: greeter; property: "visible"; value: true }
217+ PropertyAction { target: positionLock; property: "enabled"; value: false }
218+ StandardAnimation {
219+ property: "x"
220+ target: greeter
221+ to: 0
222+ duration: UbuntuAnimation.FastDuration
223+ }
224+ }
216225}
qml/Greeter/Infographics.qml−3
@@ -406,8 +406,5 @@ Item {
406406 infographic.model.nextDataSource()
407407 }
408408 }
409−
410− onClicked: mouse.accepted = false
411− onPressed: mouse.accepted = false
412409 }
413410}
qml/Shell.qml+12−4
@@ -554,9 +554,7 @@ Item {
554554 property string lockedApp: ""
555555 property bool hasLockedApp: lockedApp !== ""
556556
557− available: true
558557 hides: [launcher, panel.indicators]
559− shown: true
560558 loadContent: required || lockscreen.required // keeps content in memory for quick show()
561559
562560 locked: shell.locked
@@ -566,7 +564,12 @@ Item {
566564 width: parent.width
567565 height: parent.height
568566
569− dragHandleWidth: shell.edgeSize
567+
568+ // avoid overlapping with Launcher's edge drag area
569+ // FIXME: Fix TouchRegistry & friends and remove this workaround
570+ // Issue involves launcher's DDA getting disabled on a long
571+ // left-edge drag
572+ dragHandleLeftMargin: launcher.available ? launcher.dragAreaWidth + 1 : 0
570573
571574 function startUnlock() {
572575 if (narrowMode) {
@@ -620,7 +623,12 @@ Item {
620623 LauncherModel.setUser(user);
621624 }
622625
623− onTease: launcher.tease()
626+ onTapped: launcher.tease()
627+ onDraggingChanged: {
628+ if (dragging) {
629+ launcher.tease();
630+ }
631+ }
624632
625633 Binding {
626634 target: ApplicationManager
tests/qmltests/Components/tst_DragHandle.cpp+4
@@ -101,6 +101,10 @@ void tst_DragHandle::init()
101101 QVERIFY(m_view->rootObject() != 0);
102102 qApp->processEvents();
103103
104+ // Hide the controls to ensure we don't hit them accidentally
105+ QQuickItem *controls = m_view->rootObject()->findChild<QQuickItem*>("controls");
106+ controls->setVisible(false);
107+
104108 m_fakeTimer = new FakeTimer;
105109 m_fakeTimeSource.reset(new FakeTimeSource);
106110}
tests/qmltests/Components/tst_DragHandle.qml+43−8
@@ -27,8 +27,9 @@ import Ubuntu.Gestures 0.1
2727 A black vertical line marks the point where a drag needs no forward velocity to
2828 achieve auto-completion (i.e., get Showable.show() called after touch release).
2929 */
30−Item {
30+Rectangle {
3131 id: root
32+ color: "darkblue"
3233 width: units.gu(70)
3334 height: units.gu(70)
3435
@@ -36,19 +37,35 @@ Item {
3637
3738 property bool stretch: false
3839 property real hintDisplacement: 0
40+ property bool bidirectional: false
3941
4042 Item {
4143 id: baseItem
4244 objectName: "baseItem"
4345 anchors.fill: parent
4446
47+ Button {
48+ visible: root.bidirectional && bidirectionalShowable.fullyHidden
49+ text: "bidirectionalShowable.show()"
50+ anchors.centerIn: parent
51+ onClicked: { bidirectionalShowable.show(); }
52+ }
53+
54+ BidirectionalShowable {
55+ id: bidirectionalShowable
56+ visible: root.bidirectional
57+ onDragHandleRecognizedGesture: { root.dragHandle = dragHandle }
58+ }
59+
4560 VerticalShowable {
61+ visible: !root.bidirectional
4662 onDragHandleRecognizedGesture: { root.dragHandle = dragHandle }
4763 stretch: root.stretch
4864 hintDisplacement: root.hintDisplacement
4965 }
5066
5167 HorizontalShowable {
68+ visible: !root.bidirectional
5269 onDragHandleRecognizedGesture: { root.dragHandle = dragHandle }
5370 stretch: root.stretch
5471 hintDisplacement: root.hintDisplacement
@@ -60,15 +77,28 @@ Item {
6077 width: 2
6178 height: parent.height
6279
63− visible: dragHandle !== undefined && Direction.isHorizontal(dragHandle.direction)
80+ visible: dragHandle !== undefined
81+ && (dragHandle.direction === Direction.Horizontal || dragHandle.direction === Direction.Rightwards)
6482
6583 x: {
6684 if (dragHandle) {
67− if (dragHandle.direction === Direction.Rightwards) {
68− dragHandle.edgeDragEvaluator.dragThreshold;
69− } else {
70− parent.width - dragHandle.edgeDragEvaluator.dragThreshold;
71− }
85+ dragHandle.edgeDragEvaluator.dragThreshold;
86+ } else {
87+ 0
88+ }
89+ }
90+ }
91+ Rectangle {
92+ color: "black"
93+ width: 2
94+ height: parent.height
95+
96+ visible: dragHandle !== undefined
97+ && (dragHandle.direction === Direction.Horizontal || dragHandle.direction === Direction.Leftwards)
98+
99+ x: {
100+ if (dragHandle) {
101+ parent.width - dragHandle.edgeDragEvaluator.dragThreshold;
72102 } else {
73103 0
74104 }
@@ -126,13 +156,13 @@ Item {
126156 }
127157
128158 Row {
159+ objectName: "controls"
129160 anchors.bottom: parent.bottom
130161 anchors.left: parent.left
131162 anchors.margins: units.gu(1)
132163 spacing: units.gu(1)
133164
134165 Button {
135− id: stretchButton
136166 text: root.stretch ? "stretch" : "move"
137167 onClicked: { root.stretch = !root.stretch; }
138168 }
@@ -158,5 +188,10 @@ Item {
158188 }
159189 }
160190 }
191+
192+ Button {
193+ text: root.bidirectional ? "bidirectional" : "directional"
194+ onClicked: { root.bidirectional = !root.bidirectional; }
195+ }
161196 }
162197}
tests/qmltests/Components/tst_DragHandle/BidirectionalShowable.qmlnew file+69
@@ -0,0 +1,69 @@
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.0
18+import "../../../../qml/Components"
19+import Ubuntu.Components 0.1
20+import Ubuntu.Gestures 0.1
21+
22+Item {
23+ id: root
24+ x: 0
25+ width: parent.width
26+ height: parent.height
27+
28+ property bool shown: true
29+
30+ signal dragHandleRecognizedGesture(var dragHandle)
31+
32+ property bool fullyHidden: x == -width || x == width
33+
34+ function show() {
35+ shown = true;
36+ animation.stop();
37+ animation.to = 0;
38+ animation.start();
39+ }
40+
41+ function hide() {
42+ shown = false;
43+ animation.stop();
44+ animation.to = x > 0 ? width : -width;
45+ animation.start();
46+ }
47+
48+ StandardAnimation {
49+ id: animation
50+ target: root
51+ property: "x"
52+ }
53+
54+ Image { source: "../../UnityLogo.png"; anchors.fill: parent }
55+
56+ DragHandle {
57+ id: dragHandle
58+ anchors.fill: parent
59+ direction: Direction.Horizontal
60+ maxTotalDragDistance: root.parent.width
61+ autoCompleteDragThreshold: parent.width / 3
62+
63+ onStatusChanged: {
64+ if (status === DirectionalDragArea.Recognized) {
65+ dragHandleRecognizedGesture(dragHandle);
66+ }
67+ }
68+ }
69+}
tests/qmltests/Greeter/tst_MultiGreeter.qml+15−15
@@ -56,9 +56,9 @@ Item {
5656 }
5757
5858 SignalSpy {
59− id: teaseSpy
59+ id: tappedSpy
6060 target: greeter
61− signalName: "tease"
61+ signalName: "tapped"
6262 }
6363
6464 UT.UnityTestCase {
@@ -323,18 +323,18 @@ Item {
323323 compare(greeter.model.data(index, LightDM.UserRoles.BackgroundPathRole), "")
324324 }
325325
326− function test_teasingArea_data() {
326+ function test_tappedSignal_data() {
327327 return [
328− {tag: "left", posX: units.gu(2), leftPressed: true, rightPressed: false},
329− {tag: "right", posX: greeter.width - units.gu(2), leftPressed: false, rightPressed: true}
328+ {tag: "left", posX: units.gu(2)},
329+ {tag: "right", posX: greeter.width - units.gu(2)}
330330 ]
331331 }
332332
333− function test_teasingArea(data) {
334− teaseSpy.clear()
335− mouseClick(greeter, data.posX, greeter.height - units.gu(1))
336− teaseSpy.wait()
337− tryCompare(teaseSpy, "count", 1)
333+ function test_tappedSignal(data) {
334+ select_user("no-password");
335+ tappedSpy.clear();
336+ tap(greeter, data.posX, greeter.height - units.gu(1))
337+ tryCompare(tappedSpy, "count", 1)
338338 }
339339
340340 function test_teaseLockedUnlocked_data() {
@@ -345,18 +345,18 @@ Item {
345345 }
346346
347347 function test_teaseLockedUnlocked(data) {
348− teaseSpy.clear()
348+ tappedSpy.clear()
349349 greeter.locked = data.locked;
350350
351− mouseClick(greeter, greeter.width - units.gu(5), greeter.height - units.gu(1));
351+ tap(greeter, greeter.width - units.gu(5), greeter.height - units.gu(1));
352352
353353 if (!data.locked || data.narrow) {
354− teaseSpy.wait()
355− tryCompare(teaseSpy, "count", 1);
354+ tappedSpy.wait()
355+ tryCompare(tappedSpy, "count", 1);
356356 } else {
357357 // waiting 100ms to make sure nothing happens
358358 wait(100);
359− compare(teaseSpy.count, 0, "Greeter teasing not disabled even though it's locked.");
359+ compare(tappedSpy.count, 0, "Greeter teasing not disabled even though it's locked.");
360360 }
361361
362362 // Reset value
tests/qmltests/Greeter/tst_SingleGreeter.qml+104−16
@@ -23,10 +23,20 @@ import LightDM 0.1 as LightDM
2323import Ubuntu.Components 0.1
2424import Unity.Test 0.1 as UT
2525
26−Item {
26+Rectangle {
27+ color: "darkblue"
2728 width: units.gu(60)
2829 height: units.gu(80)
2930
31+ Button {
32+ anchors.centerIn: parent
33+ text: "Show Greeter"
34+ onClicked: {
35+ if (greeterLoader.item)
36+ greeterLoader.item.show();
37+ }
38+ }
39+
3040 Loader {
3141 id: greeterLoader
3242 anchors.fill: parent
@@ -35,15 +45,8 @@ Item {
3545
3646 sourceComponent: Component {
3747 Greeter {
38− anchors.fill: greeterLoader
39−
40− property int minX: 0
41−
42− onXChanged: {
43− if (x < minX) {
44− minX = x;
45− }
46− }
48+ width: greeterLoader.width
49+ height: greeterLoader.height
4750
4851 Component.onDestruction: {
4952 greeterLoader.itemDestroyed = true;
@@ -64,9 +67,9 @@ Item {
6467 }
6568
6669 SignalSpy {
67− id: teaseSpy
70+ id: tappedSpy
6871 target: greeterLoader.item
69− signalName: "tease"
72+ signalName: "tapped"
7073 }
7174
7275 SignalSpy {
@@ -90,9 +93,11 @@ Item {
9093 tryCompare(greeterLoader, "itemDestroyed", true);
9194
9295 unlockSpy.clear();
93− teaseSpy.clear();
96+ tappedSpy.clear();
9497
9598 greeterLoader.active = true;
99+ tryCompare(greeterLoader, "status", Loader.Ready);
100+ removeTimeConstraintsFromDirectionalDragAreas(greeterLoader.item);
96101 }
97102
98103 function test_properties() {
@@ -108,10 +113,10 @@ Item {
108113 }
109114
110115 function test_teasingArea(data) {
111− teaseSpy.clear()
116+ tappedSpy.clear()
112117 tap(greeter, data.posX, greeter.height - units.gu(1))
113− teaseSpy.wait()
114− tryCompare(teaseSpy, "count", 1)
118+ tappedSpy.wait()
119+ tryCompare(tappedSpy, "count", 1)
115120 }
116121
117122 function test_statsWelcomeScreen() {
@@ -152,5 +157,88 @@ Item {
152157
153158 tryCompare(infographicDataChangedSpy, "count", 1);
154159 }
160+
161+ function test_movesBackIntoPlaceWhenNotDraggedFarEnough() {
162+
163+ var dragEvaluator = findInvisibleChild(greeter, "edgeDragEvaluator");
164+ verify(dragEvaluator);
165+
166+ // Make it easier to get a rejection/rollback. Otherwise would have to inject
167+ // a fake timer into dragEvaluator.
168+ // Afterall, we are testing if the Greeter indeed moves back on a
169+ // rollback decision, not the drag evaluation itself.
170+ dragEvaluator.minDragDistance = dragEvaluator.maxDragDistance / 2;
171+
172+ // it starts as fully shown
173+ compare(greeter.x, 0);
174+
175+ // then we drag it a bit
176+ var startX = greeter.width - 1;
177+ var touchY = greeter.height / 2;
178+ var dragXDelta = -(dragEvaluator.minDragDistance * 0.3);
179+ touchFlick(greeter,
180+ startX , touchY, // start pos
181+ startX + dragXDelta, touchY, // end pos
182+ true /* beginTouch */, false /* endTouch */);
183+
184+ // which should make it move a bit
185+ tryCompareFunction(function(){return greeter.x < 0;}, true);
186+
187+ // then we release it
188+ touchRelease(greeter, startX + dragXDelta, touchY);
189+
190+ // which should make it move back into its original position as it didn't move
191+ // far enough to have it hidden
192+ tryCompare(greeter, "x", 0);
193+ }
194+
195+ function test_dragToHide_data() {
196+ return [
197+ {tag: "left", startX: greeter.width * 0.95, endX: greeter.width * 0.1, hiddenX: -greeter.width},
198+ {tag: "right", startX: greeter.width * 0.1, endX: greeter.width * 0.95, hiddenX: greeter.width},
199+ ];
200+ }
201+ function test_dragToHide(data) {
202+ compare(greeter.x, 0);
203+ compare(greeter.visible, true);
204+ compare(greeter.shown, true);
205+ compare(greeter.showProgress, 1);
206+
207+ touchFlick(greeter,
208+ data.startX, greeter.height / 2, // start pos
209+ data.endX, greeter.height / 2); // end pos
210+
211+ tryCompare(greeter, "x", data.hiddenX);
212+ tryCompare(greeter, "visible", false);
213+ tryCompare(greeter, "shown", false);
214+ tryCompare(greeter, "showProgress", 0);
215+ }
216+
217+ function test_hiddenGreeterRemainsHiddenAfterResize_data() {
218+ return [
219+ {tag: "left", startX: greeter.width * 0.95, endX: greeter.width * 0.1},
220+ {tag: "right", startX: greeter.width * 0.1, endX: greeter.width * 0.95},
221+ ];
222+ }
223+ function test_hiddenGreeterRemainsHiddenAfterResize(data) {
224+ touchFlick(greeter,
225+ data.startX, greeter.height / 2, // start pos
226+ data.endX, greeter.height / 2); // end pos
227+
228+ tryCompare(greeter, "x", data.tag == "left" ? -greeter.width : greeter.width);
229+ tryCompare(greeter, "visible", false);
230+ tryCompare(greeter, "shown", false);
231+ tryCompare(greeter, "showProgress", 0);
232+
233+ // flip dimensions to simulate an orientation change
234+ greeter.width = greeterLoader.height;
235+ greeter.height = greeterLoader.width;
236+
237+ // All properties should remain consistent
238+ tryCompare(greeter, "x", data.tag == "left" ? -greeter.width : greeter.width);
239+ tryCompare(greeter, "visible", false);
240+ tryCompare(greeter, "shown", false);
241+ tryCompare(greeter, "showProgress", 0);
242+ }
155243 }
156244}
tests/qmltests/tst_TabletShell.qml+15−12
@@ -74,21 +74,24 @@ Row {
7474 Column {
7575 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
7676 spacing: units.gu(1)
77− Row {
78− anchors { left: parent.left; right: parent.right }
79− Button {
80− text: "Show Greeter"
81− onClicked: {
82− if (shellLoader.status !== Loader.Ready)
83− return
84−
85− var greeter = testCase.findChild(shellLoader.item, "greeter")
86− if (!greeter.shown) {
87− greeter.show()
88− }
77+ Button {
78+ text: "Show Greeter"
79+ onClicked: {
80+ if (shellLoader.status !== Loader.Ready)
81+ return
82+
83+ var greeter = testCase.findChild(shellLoader.item, "greeter")
84+ if (!greeter.shown) {
85+ greeter.show()
8986 }
9087 }
9188 }
89+ Button {
90+ text: "Demo edges"
91+ onClicked: {
92+ AccountsService.demoEdges = true
93+ }
94+ }
9295 }
9396 }
9497