Merge lp:~zsombi/ubuntu-ui-toolkit/dynamic-tabs-reloaded into lp:ubuntu-ui-toolkit

Proposed by Zsombor Egri
Status: Superseded
Proposed branch: lp:~zsombi/ubuntu-ui-toolkit/dynamic-tabs-reloaded
Merge into: lp:ubuntu-ui-toolkit
Diff against target: 1137 lines (+767/-49)
14 files modified
CHANGES (+1/-0)
components.api (+8/-1)
modules/Ubuntu/Components/Tab.qml (+0/-7)
modules/Ubuntu/Components/Tabs.qml (+292/-20)
modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml (+9/-2)
modules/Ubuntu/Components/plugin/quickutils.cpp (+15/-0)
modules/Ubuntu/Components/plugin/quickutils.h (+1/-0)
modules/Ubuntu/Test/UbuntuTestCase.qml (+69/-1)
modules/Ubuntu/Test/deployment.pri (+6/-1)
tests/resources/navigation/Tabs.qml (+90/-1)
tests/unit/runtest.sh (+1/-1)
tests/unit_x11/tst_components/ExternalTab.qml (+21/-0)
tests/unit_x11/tst_components/tst_tabs.qml (+151/-0)
tests/unit_x11/tst_test/tst_ubuntutestcase.qml (+103/-15)
To merge this branch: bzr merge lp:~zsombi/ubuntu-ui-toolkit/dynamic-tabs-reloaded
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Tim Peeters Approve
Cris Dywan provided moveitembefore docs are fixed Approve
Review via email: mp+199620@code.launchpad.net

This proposal has been superseded by a proposal from 2014-04-10.

Commit message

Extending Tabs with functions to dynamically add, move and remove tabs.

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: Approve (continuous-integration)
Revision history for this message
Tim Peeters (tpeeters) wrote :

637 + * Copyright 2012 Canonical Ltd.

2013

Revision history for this message
Cris Dywan (kalikiana) wrote :

I'm seeing a number of errors while using the tabbar test:

modules/Ubuntu/Components/Tabs.qml:598: TypeError: Cannot call method 'indexOf' of undefined
coming from connectToRepeaters
modules/Ubuntu/Components/Tabs.qml:360: ReferenceError: MathUtils is not defined
coming from insertTab
modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml:119: TypeError: Cannot read property of null
coming from anchors.top on the Repeater, not sure what this is

review: Needs Fixing
Revision history for this message
Cris Dywan (kalikiana) wrote :

I'm liking the documentation which is quite decent. I love that the API keeps the different types of tabs behind the scenes.

> // but move only if there are more than on eitems in the list
Typo.

// QQuickItem *parentItem = item->parentItem();
Please remove this one.

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
Zsombor Egri (zsombi) wrote :

> 637 + * Copyright 2012 Canonical Ltd.
>
> 2013

It's 2014 now :)

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

> I'm liking the documentation which is quite decent. I love that the API keeps
> the different types of tabs behind the scenes.
>
> > // but move only if there are more than on eitems in the list
> Typo.
>
> // QQuickItem *parentItem = item->parentItem();
> Please remove this one.

Fixed in revno 903

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
Tim Peeters (tpeeters) wrote :

+/*!
463 + * \internal
464 + * Moves a given item to the specified index in its parent's child list.
465 + */
466 +void QuickUtils::moveItemBefore(QQuickItem *item, QQuickItem *before)

description doesn't exactly match the function specs.
* Moves a given item before the specified item in their parent's child list
?

Revision history for this message
Cris Dywan (kalikiana) wrote :

That one escaped me; I'll give green light with the above sentence fixed.

review: Approve (provided moveitembefore docs are fixed)
Revision history for this message
Cris Dywan (kalikiana) wrote :
Revision history for this message
Tim Peeters (tpeeters) wrote :

note that you didn't fix the doc yet

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

I changed the status back to needs review, because we need to re-do the CI@home with qt52.

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

> note that you didn't fix the doc yet

Sorry, done now.

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

> modules/Ubuntu/Components/Tabs.qml:360: ReferenceError: MathUtils is not
> defined

This is still broken. Please add the include in Tabs.qml.

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

> I'm seeing a number of errors while using the tabbar test:
>
> modules/Ubuntu/Components/Tabs.qml:598: TypeError: Cannot call method
> 'indexOf' of undefined
> coming from connectToRepeaters
> modules/Ubuntu/Components/Tabs.qml:360: ReferenceError: MathUtils is not
> defined
> coming from insertTab
> modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml:119: TypeError:
> Cannot read property of null
> coming from anchors.top on the Repeater, not sure what this is

please double-check that all these are fixed

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

>
> > modules/Ubuntu/Components/Tabs.qml:360: ReferenceError: MathUtils is not
> > defined
>
> This is still broken. Please add the include in Tabs.qml.

I'm wondering why was it passing till now???

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
Tim Peeters (tpeeters) wrote :

ok

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: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
913. By Zsombor Egri

staging merge

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CHANGES'
2--- CHANGES 2014-02-24 22:03:55 +0000
3+++ CHANGES 2014-04-10 11:34:46 +0000
4@@ -9,6 +9,7 @@
5
6 API Changes
7 ***********
8+* DEPRECATED IN: Tabs: default property list<Item> tabChildren
9 * ADDED IN: PickerDelegate: readonly property Picker picker
10 * CHANGED IN: OptionSelector: readonly property bool currentlyExpanded TO property bool currentlyExpanded
11 * CHANGED IN: ItemSelector: readonly property bool currentlyExpanded TO property bool currentlyExpanded
12
13=== modified file 'components.api'
14--- components.api 2014-04-01 12:57:27 +0000
15+++ components.api 2014-04-10 11:34:46 +0000
16@@ -417,9 +417,14 @@
17 readonly property Tab selectedTab
18 readonly property Item currentPage
19 property TabBar tabBar
20- default property list<Item> tabChildren
21+ property list<Item> tabChildren
22 readonly property int count
23 signal modelChanged()
24+ function addTab(title, tab)
25+ function insertTab(index, title, tab)
26+ function getTab(index)
27+ function moveTab(from, to)
28+ function removeTab(index)
29 modules/Ubuntu/Components/TextArea.qml
30 StyledItem
31 property bool highlighted
32@@ -593,6 +598,8 @@
33 function findChild(obj,objectName)
34 function findInvisibleChild(obj,objectName)
35 function mouseMoveSlowly(item,x,y,dx,dy,steps,stepdelay)
36+ function flick(item, x, y, dx, dy, pressTimeout, steps, button, modifiers, delay)
37+ function mouseLongPress(item, x, y, button, modifiers, delay)
38 function tryCompareFunction(func, expectedResult, timeout)
39 plugins.qmltypes
40 name: "InverseMouseAreaType"
41
42=== modified file 'modules/Ubuntu/Components/Tab.qml'
43--- modules/Ubuntu/Components/Tab.qml 2013-11-07 07:26:01 +0000
44+++ modules/Ubuntu/Components/Tab.qml 2014-04-10 11:34:46 +0000
45@@ -105,12 +105,5 @@
46 Tab is destroyed upon removal.
47 */
48 property bool dynamic: false
49-
50- /*
51- This flag is used by the Tabs to determine whether the pre-declared Tab was removed
52- from the Tabs model or not. The flag guards adding back pre-declared tabs upon Tabs
53- component stack (children) change.
54- */
55- property bool removedFromTabs: false
56 }
57 }
58
59=== modified file 'modules/Ubuntu/Components/Tabs.qml'
60--- modules/Ubuntu/Components/Tabs.qml 2014-04-08 12:38:35 +0000
61+++ modules/Ubuntu/Components/Tabs.qml 2014-04-10 11:34:46 +0000
62@@ -15,6 +15,7 @@
63 */
64
65 import QtQuick 2.0
66+import "mathUtils.js" as MathUtils
67
68 /*!
69 \qmltype Tabs
70@@ -149,6 +150,110 @@
71 }
72 }
73 \endqml
74+
75+ \section2 Dynamic tabs
76+ So far all Tab elements were pre-declared, but there can be situations when
77+ tabs need to be added dynamically. There are two ways to solve this, depending
78+ on the output needed.
79+
80+ \section3 Using Repeaters
81+ A Repeater can be used to create the necessary tabs depending on a given model.
82+ In this way the number of tabs will be driven by the model itself.
83+ An example of such a dynamic tab:
84+ \qml
85+ // DynamicTab.qml
86+ import QtQuick 2.0
87+ import Ubuntu.Components 0.1
88+
89+ Tabs {
90+ property alias model: tabRepeater.model
91+ Repeater {
92+ id: tabRepeater
93+ model: 5
94+ Tab {
95+ title: "Tab #" + index
96+ page: Page {
97+ // [...]
98+ }
99+ }
100+ }
101+ }
102+ \endqml
103+ Note that in the example above the Tabs will be re-created each time the model
104+ changes. This will cause state losing of each Tab, which depending on the
105+ content type can be solved at some extent using StateSaver. Using a Loader
106+ or specifying the Tab instance/component in the model the state can be preserved,
107+ however may increase code complexity.
108+
109+ \section3 Dynamic tabs
110+ Tabs provides functions to add Tab elements dynamically on runtime, without
111+ destroying the state of the existing tabs. You can add, move and remove any
112+ kind of Tab element, pre-declared or dynamically created ones. When removing
113+ pre-declared tabs, those will all be held back and hidden by Tabs, and can be
114+ added back any time either to the same or to a different position.
115+
116+ \qml
117+ import QtQuick 2.0
118+ import Ubuntu.Components 0.1
119+
120+ MainView {
121+ width: units.gu(40)
122+ height: units.gu(71)
123+
124+ Component {
125+ id: dynamicTab
126+ Tab {
127+ page: Page {
128+ Label {
129+ text: title
130+ anchors.centerIn: parent
131+ }
132+ }
133+ }
134+ }
135+ Tabs {
136+ id: tabs
137+ Tab {
138+ title: "Main tab"
139+ page: Page {
140+ toolbar: ToolbarItems {
141+ ToolbarButton {
142+ text: "remove predeclared"
143+ onTriggered: tabs.removeTab(preDeclared.index)
144+ }
145+ ToolbarButton {
146+ text: "add new"
147+ onTriggered: tabs.addTab("New tab", dynamicTab)
148+ }
149+ ToolbarButton {
150+ text: "insert predeclared"
151+ onTriggered: tabs.insertTab("", 0)
152+ }
153+ }
154+ }
155+ }
156+ Tab {
157+ id: preDeclared
158+ title: "Pre-declared tab"
159+ page: Page {
160+ Label {
161+ text: "This is a predeclared tab at index #" + index
162+ anchors.centerIn: parent
163+ }
164+ }
165+ }
166+ }
167+ }
168+ \endqml
169+
170+ \section3 Using Repeater and functions together
171+ Repeaters re-create their delegates as many times the model changes. Tabs added
172+ or moved in between the tabs maintained by the Repeater, as well as reordered
173+ through the Tabs functions will be re-arranged once the Repeater's model changes.
174+ This should be taken into account when designing the application, and the use
175+ of Repeater and functions toghether should be avoided if possible, or at least
176+ Repeater should always add tabs to te tail of the tab stack, and no tab insertion
177+ happens in that area.
178 */
179 PageTreeNode {
180 id: tabs
181@@ -186,10 +291,14 @@
182 }
183
184 /*!
185- Children are placed in a separate item that has functionality to extract the Tab items.
186+ \deprecated
187+ Children are placed in a separate item that has functionality to extract
188+ the Tab items.
189+ Note: this property is deprecated. Tab components are directly parented
190+ to Tabs' data property.
191 \qmlproperty list<Item> tabChildren
192 */
193- default property alias tabChildren: tabStack.data
194+ property alias tabChildren: tabs.data
195
196 /*!
197 \qmlproperty int count
198@@ -206,36 +315,180 @@
199 signal modelChanged()
200
201 /*!
202+ Appends a Tab dynamically to the list of tabs. The \a title specifies the
203+ title of the Tab. The \a tab can be either a Component, a URL to a Tab
204+ component to be loaded or an instance of a pre-declared tab that has been
205+ previously removed. The Tab's title will be replaced with the given \a title,
206+ unless the value is an empty string or undefined.
207+ Returns the instance of the added Tab.
208+ */
209+ function addTab(title, tab) {
210+ return insertTab(count, title, tab);
211+ }
212+
213+ /*!
214+ Inserts a Tab at the given index. If the \a index is less or equal than 0,
215+ the Tab will be added to the front, and to the end of the tab stack in case
216+ the \a index is greater than \l count. \a title and \a tab are used in the
217+ same way as with \l addTab().
218+ Returns the instance of the inserted Tab.
219+ */
220+ function insertTab(index, title, tab) {
221+ // check if the given component is a Tab instance
222+ var tabObject = null;
223+
224+ if (typeof tab === "string") {
225+ // we have a URL
226+ var tabComponent = Qt.createComponent(tab);
227+ if (tabComponent.status === Component.Error) {
228+ console.error(tabComponent.errorString());
229+ return null;
230+ }
231+ tabObject = tabComponent.createObject();
232+ tabObject.__protected.dynamic = true;
233+ } else if (tab.hasOwnProperty("createObject")) {
234+ // we have a Component
235+ tabObject = tab.createObject();
236+ tabObject.__protected.dynamic = true;
237+ } else if (tab.hasOwnProperty("parent") && tab.parent === trashedTabs) {
238+ // we have a pre-declared tab that has been removed
239+ tabObject = tab;
240+ } else {
241+ console.error(i18n.tr("The object is not a URL, Component or a removed Tab: ") + tab);
242+ return null;
243+ }
244+
245+ // fix title
246+ if (title !== undefined && title !== "") {
247+ tabObject.title = title;
248+ }
249+
250+ // insert the created tab into the model
251+ index = MathUtils.clamp(index, 0, count);
252+ tabObject.__protected.inserted = true;
253+ tabObject.__protected.index = index;
254+ tabsModel.insertTab(tabObject, index);
255+ if (tabs.selectedTabIndex >= index) {
256+ // move the selected index to the next index
257+ tabs.selectedTabIndex += 1;
258+ } else {
259+ internal.sync();
260+ }
261+ return tabObject;
262+ }
263+
264+ /*!
265+ The function returns the Tab from the given \a index, or null if the \a index
266+ is invalid (less than \c 0 and greater than \l count).
267+ */
268+ function getTab(index) {
269+ return (index >=0) && (index < count) ? tabsModel.get(index).tab : null;
270+ }
271+
272+ /*!
273+ Moves the tab from the given \a from position to the position given in \a to.
274+ Returns true if the indexes were in 0..\l count - 1 boundary and if the operation
275+ succeeds, and false otherwise. The \l selectedTabIndex is updated if it is
276+ affected by the move (it is equal with \a from or falls between \a from and
277+ \a to indexes).
278+ */
279+ function moveTab(from, to) {
280+ if (from < 0 || from >= count || to < 0 || to >= count || from === to) return false;
281+ var tabFrom = tabsModel.get(from).tab;
282+ var tabTo = tabsModel.get(to).tab;
283+
284+ // move tab
285+ QuickUtils.moveItemBefore(tabFrom, tabTo);
286+ tabsModel.updateTabList(tabs.children);
287+
288+ // fix selected tab
289+ if (selectedTabIndex === from) {
290+ selectedTabIndex = to;
291+ } else if (selectedTabIndex >= Math.min(from, to) && selectedTabIndex <= Math.max(from, to)) {
292+ selectedTabIndex--;
293+ } else {
294+ internal.sync();
295+ }
296+
297+ return true;
298+ }
299+
300+ /*!
301+ Removes the Tab from the given \a index. Returns true if the \a index falls
302+ into 0..\l count - 1 boundary and the operation succeeds, and false on error.
303+ The function removes also the pre-declared tabs. These can be added back using
304+ \l addTab or \l insertTab by specifying the instance of the Tab to be added as
305+ component. The \l selectedTabIndex is updated if is affected by the removal
306+ (it is identical or greater than the tab index to be removed).
307+ */
308+ function removeTab(index) {
309+ if (index < 0 || index >= count) return false;
310+ var tab = tabsModel.get(index).tab;
311+ var activeIndex = (selectedTabIndex >= index) ? MathUtils.clamp(selectedTabIndex, 0, count - 2) : -1;
312+
313+ // remove from Tabs; Tabs children change will remove the tab from the model
314+ tab.parent = null;
315+ if (tab.__protected.dynamic) {
316+ tab.destroy();
317+ } else {
318+ // pre-declared tab, mark it as removed, so we don't update it next time
319+ // the tabs stack children is updated
320+ tab.parent = trashedTabs;
321+ }
322+
323+ // move active tab if needed
324+ if (activeIndex >= 0 && activeIndex !== selectedTabIndex) {
325+ selectedTabIndex = activeIndex;
326+ } else {
327+ internal.sync();
328+ }
329+
330+ return true;
331+ }
332+
333+ /*! \internal */
334+ onChildrenChanged: {
335+ internal.connectToRepeaters(tabs.children);
336+ tabsModel.updateTabList(tabs.children);
337+ }
338+
339+ /*!
340 \internal
341 required by TabsStyle
342 */
343 ListModel {
344 id: tabsModel
345
346+ property bool updateDisabled: false
347+
348 function listModel(tab) {
349 return {"title": tab.title, "tab": tab};
350 }
351
352 function updateTabList(tabsList) {
353+ if (updateDisabled) return;
354 var offset = 0;
355- var tabIndex;
356+ var tabIndex = -1;
357 for (var i in tabsList) {
358 var tab = tabsList[i];
359 if (internal.isTab(tab)) {
360 tabIndex = i - offset;
361 // make sure we have the right parent
362- tab.parent = tabStack;
363+ tab.parent = tabs;
364
365 if (!tab.__protected.inserted) {
366 tab.__protected.index = tabIndex;
367 tab.__protected.inserted = true;
368 insert(tabIndex, listModel(tab));
369- } else if (!tab.__protected.removedFromTabs && tabsModel.count > tab.index) {
370+ } else {
371 get(tab.index).title = tab.title;
372 }
373
374 // always makes sure that tabsModel has the same order as tabsList
375- move(tab.__protected.index, tabIndex, 1);
376+ // but move only if there is more than one item in the list
377+ if (count > 1) {
378+ move(tab.__protected.index, tabIndex, 1);
379+ }
380 reindex();
381 } else {
382 // keep track of children that are not tabs so that we compute
383@@ -243,6 +496,10 @@
384 offset += 1;
385 }
386 }
387+ // remove deleted tabs, those should be at the end of the list by now
388+ if ((tabIndex >= 0) && (tabIndex + 1) < count) {
389+ remove(tabIndex + 1, count - tabIndex - 1);
390+ }
391 internal.sync();
392 }
393
394@@ -257,18 +514,31 @@
395 tab.__protected.index = i;
396 }
397 }
398+
399+ function insertTab(tab, index) {
400+ // fix index
401+ if (index < 0) {
402+ index = 0;
403+ }
404+ // get the tab before which the item will be inserted
405+ var itemAtIndex = ((index >= 0) && (index < count)) ? get(index).tab : null;
406+ // disable update only if we insert, append can keep the logic rolling
407+ updateDisabled = (itemAtIndex !== null);
408+ insert(index, listModel(tab));
409+ tab.parent = tabs;
410+ updateDisabled = false;
411+ if (itemAtIndex) {
412+ QuickUtils.moveItemBefore(tab, itemAtIndex);
413+ updateTabList(tabs.children);
414+ }
415+ }
416 }
417
418- // FIXME: this component is not really needed, as it doesn't really bring any
419- // value; should be removed in a later MR
420+ // invisible component stacking removed pre-declared components
421 Item {
422- anchors.fill: parent
423- id: tabStack
424-
425- onChildrenChanged: {
426- internal.connectToRepeaters(tabStack.children);
427- tabsModel.updateTabList(tabStack.children);
428- }
429+ id: trashedTabs
430+ visible: false
431+ opacity: 0.0
432 }
433
434 /*
435@@ -283,7 +553,7 @@
436 interval: 1
437 running: false
438 onTriggered: {
439- tabsModel.updateTabList(tabStack.children);
440+ tabsModel.updateTabList(tabs.children);
441 internal.sync();
442 }
443 }
444@@ -295,8 +565,8 @@
445 Binding {
446 target: tabBar
447 property: "animate"
448- when: internal.header && internal.header.hasOwnProperty("animate")
449- value: internal.header.animate
450+ when: (internal.header !== null) && internal.header.hasOwnProperty("animate")
451+ value: internal.header ? internal.header.animate : "false"
452 }
453
454 /*
455@@ -332,7 +602,9 @@
456 function connectToRepeaters(children) {
457 for (var i = 0; i < children.length; i++) {
458 var child = children[i];
459- if (internal.isRepeater(child) && (internal.repeaters.indexOf(child) < 0)) {
460+ if (internal.isRepeater(child) &&
461+ (internal.repeaters !== undefined) &&
462+ (internal.repeaters.indexOf(child) < 0)) {
463 internal.connectRepeater(child);
464 }
465 }
466@@ -355,7 +627,7 @@
467 https://bugreports.qt-project.org/browse/QTBUG-32438
468 */
469 function updateTabsModel() {
470- tabsModel.updateTabList(tabStack.children);
471+ tabsModel.updateTabList(tabs.children);
472 }
473
474 /*
475
476=== modified file 'modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml'
477--- modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml 2014-04-08 12:38:35 +0000
478+++ modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml 2014-04-10 11:34:46 +0000
479@@ -116,8 +116,8 @@
480 AbstractButton {
481 id: button
482 anchors {
483- top: parent.top
484- bottom: parent.bottom
485+ top: parent ? parent.top : undefined
486+ bottom: parent ? parent.bottom: undefined
487 }
488 width: text.paintedWidth + text.anchors.leftMargin + text.anchors.rightMargin
489
490@@ -150,6 +150,13 @@
491 return false;
492 }
493
494+ // update the offset of the buttonRow
495+ onOffsetChanged: {
496+ if (selected) {
497+ buttonView.updateOffset(button.offset);
498+ }
499+ }
500+
501 Behavior on opacity {
502 NumberAnimation {
503 duration: headerTextFadeDuration
504
505=== modified file 'modules/Ubuntu/Components/plugin/quickutils.cpp'
506--- modules/Ubuntu/Components/plugin/quickutils.cpp 2014-03-20 15:46:28 +0000
507+++ modules/Ubuntu/Components/plugin/quickutils.cpp 2014-04-10 11:34:46 +0000
508@@ -119,6 +119,21 @@
509 return result.left(result.indexOf("_QML"));
510 }
511
512+/*!
513+ * \internal
514+ * Moves a given \a item before the \a other one in the object stack. Both \a item
515+ * and \a other must have the same parent item.
516+ */
517+void QuickUtils::moveItemBefore(QQuickItem *item, QQuickItem *other)
518+{
519+ Q_ASSERT(item);
520+ Q_ASSERT(item->parentItem());
521+ if (other) {
522+ Q_ASSERT(other->parentItem() == item->parentItem());
523+ item->stackBefore(other);
524+ }
525+}
526+
527
528 /*!
529 * \internal
530
531=== modified file 'modules/Ubuntu/Components/plugin/quickutils.h'
532--- modules/Ubuntu/Components/plugin/quickutils.h 2014-03-20 15:46:28 +0000
533+++ modules/Ubuntu/Components/plugin/quickutils.h 2014-04-10 11:34:46 +0000
534@@ -42,6 +42,7 @@
535 QString inputMethodProvider() const;
536
537 Q_INVOKABLE static QString className(QObject *item);
538+ Q_INVOKABLE void moveItemBefore(QQuickItem *item, QQuickItem *before);
539 QObject* createQmlObject(const QUrl &url, QQmlEngine *engine);
540
541 Q_SIGNALS:
542
543=== modified file 'modules/Ubuntu/Test/UbuntuTestCase.qml'
544--- modules/Ubuntu/Test/UbuntuTestCase.qml 2014-02-25 12:36:27 +0000
545+++ modules/Ubuntu/Test/UbuntuTestCase.qml 2014-04-10 11:34:46 +0000
546@@ -87,7 +87,75 @@
547 }
548 }
549
550- /*!
551+ /*!
552+ \qmlmethod UbuntuTestCase::flick(item, x, y, dx, dy, pressTimeout = -1, steps = -1, button = Qt.LeftButton, modifiers = Qt.NoModifiers, delay = -1)
553+
554+ The function produces a flick event when executed on Flickables. When used
555+ on other components it provides the same functionality as \l mouseDrag()
556+ function. The optional \a pressTimeout parameter can be used to introduce
557+ a small delay between the mouse press and the first mouse move. Setting a
558+ negative or zero value will disable the timeout.
559+
560+ The default flick velocity is built up using 5 move points. This can be altered
561+ by setting a positive value to \a steps parameter. The bigger the number the
562+ longer the flick will be. When a negative or zero value is given, the default
563+ of 5 move points will be used.
564+
565+ \note The function can be used to select a text in a TextField or TextArea by
566+ specifying at least 400 millisecods to \a pressTimeout.
567+ */
568+ function flick(item, x, y, dx, dy, pressTimeout, steps, button, modifiers, delay) {
569+ if (item === undefined || item.x === undefined || item.y === undefined)
570+ return
571+ if (button === undefined)
572+ button = Qt.LeftButton
573+ if (modifiers === undefined)
574+ modifiers = Qt.NoModifier
575+ if (steps === undefined || steps <= 0)
576+ steps = 4;
577+ // make sure we have at least two move steps so the flick will be sensed
578+ steps += 1;
579+ if (delay === undefined)
580+ delay = -1;
581+
582+ var ddx = dx / steps;
583+ var ddy = dy / steps;
584+
585+ mousePress(item, x, y, button, modifiers, delay);
586+ if (pressTimeout !== undefined && pressTimeout > 0) {
587+ wait(pressTimeout);
588+ }
589+ for (var i = 1; i <= steps; i++) {
590+ // mouse moves are all processed immediately, without delay in between events
591+ mouseMove(item, x + i * ddx, y + i * ddy, -1, button);
592+ }
593+ mouseRelease(item, x + dx, y + dy, button, modifiers, delay);
594+ // empty event buffer
595+ wait(200);
596+ }
597+
598+ /*!
599+ \qmlmethod UbuntuTestCase::mouseLongPress(item, x, y, button = Qt.LeftButton, modifiers = Qt.NoModifiers, delay = -1)
600+
601+ Simulates a long press on a mouse \a button with an optional \a modifier
602+ on an \a item. The position is defined by \a x and \a y. If \a delay is
603+ specified, the test will wait the specified amount of milliseconds before
604+ the press.
605+
606+ The position given by \a x and \a y is transformed from the co-ordinate
607+ system of \a item into window co-ordinates and then delivered.
608+ If \a item is obscured by another item, or a child of \a item occupies
609+ that position, then the event will be delivered to the other item instead.
610+
611+ \sa mouseRelease(), mouseClick(), mouseDoubleClick(), mouseMove(), mouseDrag(), mouseWheel()
612+ */
613+ function mouseLongPress(item, x, y, button, modifiers, delay) {
614+ mousePress(item, x, y, button, modifiers, delay);
615+ // the delay is taken from QQuickMouseArea
616+ wait(800);
617+ }
618+
619+ /*!
620 Keeps executing a given parameter-less function until it returns the given
621 expected result or the timemout is reached (in which case a test failure
622 is generated)
623
624=== modified file 'modules/Ubuntu/Test/deployment.pri'
625--- modules/Ubuntu/Test/deployment.pri 2014-01-17 12:30:05 +0000
626+++ modules/Ubuntu/Test/deployment.pri 2014-04-10 11:34:46 +0000
627@@ -7,9 +7,14 @@
628 # make found deployables visible in Qt Creator
629 OTHER_FILES += $$QMLDIR_FILE
630
631+QML_FILES = $$system(ls *.qml)
632+JS_FILES = $$system(ls *.js)
633+
634 # define deployment for found deployables
635 qmldir_file.path = $$installPath
636 qmldir_file.files = $$QMLDIR_FILE
637+qml_files.path = $$installPath
638+qml_files.files = $$QML_FILES
639 js_files.path = $$installPath
640 js_files.files = $$JS_FILES
641
642@@ -20,4 +25,4 @@
643 # https://bugreports.qt-project.org/browse/QTBUG-36243
644 plugins_qmltypes.extra = $$[QT_INSTALL_BINS]/qmlplugindump -notrelocatable Ubuntu.Test 0.1 ../../ 2>/dev/null > $(INSTALL_ROOT)/$$installPath/plugins.qmltypes
645
646-INSTALLS += qmldir_file plugins_qmltypes
647+INSTALLS += qmldir_file plugins_qmltypes qml_files js_files
648
649=== modified file 'tests/resources/navigation/Tabs.qml'
650--- tests/resources/navigation/Tabs.qml 2014-04-07 10:03:39 +0000
651+++ tests/resources/navigation/Tabs.qml 2014-04-10 11:34:46 +0000
652@@ -19,9 +19,47 @@
653 import Ubuntu.Components.ListItems 0.1 as ListItem
654
655 MainView {
656+ id: root
657 width: 800
658 height: 600
659
660+ property var repeaterModel: 3
661+
662+ Component {
663+ id: dynamicTab
664+ Tab {
665+ page: Page {
666+ Label {
667+ text: title + " at index " + index
668+ anchors.centerIn: parent
669+ }
670+ tools: ToolbarItems {
671+ ToolbarButton {
672+ text: "move @1"
673+ onTriggered: {
674+ print("MOVE TAB TO #1")
675+ tabs.moveTab(index, 1)
676+ }
677+ }
678+ ToolbarButton {
679+ text: "remove me"
680+ onTriggered: {
681+ print("REMOVE CURENT TAB")
682+ tabs.removeTab(index)
683+ }
684+ }
685+ ToolbarButton {
686+ text: "remove first"
687+ onTriggered: {
688+ print("REMOVE TAB AT #0")
689+ tabs.removeTab(0)
690+ }
691+ }
692+ }
693+ }
694+ }
695+ }
696+
697 Tabs {
698 id: tabs
699 selectedTabIndex: 0
700@@ -31,6 +69,7 @@
701
702 Tab {
703 id: simpleTab
704+ objectName: title
705 title: i18n.tr("Simple page #" + index)
706 page: Page {
707 Row {
708@@ -55,13 +94,60 @@
709 iconSource: "call_icon.png"
710 onTriggered: print("action triggered")
711 }
712+ ToolbarButton {
713+ text: "append"
714+ onTriggered: {
715+ print("APPEND TAB")
716+ tabs.addTab("Appended tab", dynamicTab)
717+ }
718+ }
719+ ToolbarButton {
720+ text: "insert@1"
721+ onTriggered: {
722+ print("INSERT TAB TO #1")
723+ tabs.insertTab(1, "Inserted tab", dynamicTab)
724+ }
725+ }
726+ ToolbarButton {
727+ text: "insert@2"
728+ onTriggered: {
729+ print("INSERT BETWEEN REPEATERS #1")
730+ tabs.insertTab(2, "Between repeaters", dynamicTab)
731+ }
732+ }
733+ ToolbarButton {
734+ text: "insert@here"
735+ onTriggered: {
736+ print("INSERT AFTER ME")
737+ tabs.insertTab(simpleTab.index, "Inserted tab", dynamicTab)
738+ }
739+ }
740+ ToolbarButton {
741+ text: "incRep"
742+ onTriggered: {
743+ print("INCREASE REPEATER MODEL")
744+ root.repeaterModel += 1
745+ }
746+ }
747+ ToolbarButton {
748+ text: "remove last"
749+ onTriggered: {
750+ print("REMOVE LAST TAB")
751+ tabs.removeTab(tabs.count - 1)
752+ }
753+ }
754+ ToolbarButton {
755+ text: "append predec"
756+ onTriggered: tabs.addTab("Re-added ListView", listViewTab)
757+ }
758 }
759 }
760 }
761 Repeater {
762- model: 3
763+ model: root.repeaterModel
764 Tab {
765 id: tab
766+ objectName: title
767 title: "Extra #" + tab.index
768 page: Page {
769 Column {
770@@ -88,6 +174,7 @@
771 }
772 Tab {
773 id: externalTab
774+ objectName: title
775 title: i18n.tr("External #" + index)
776 page: Loader {
777 parent: externalTab
778@@ -96,6 +183,8 @@
779 }
780 }
781 Tab {
782+ id: listViewTab
783+ objectName: title
784 title: i18n.tr("List view #" + index)
785 page: Page {
786 ListView {
787
788=== modified file 'tests/unit/runtest.sh'
789--- tests/unit/runtest.sh 2014-03-31 18:26:46 +0000
790+++ tests/unit/runtest.sh 2014-04-10 11:34:46 +0000
791@@ -33,7 +33,7 @@
792 if [ $_TARGET != $_TESTFILE ]; then
793 _CMD="$_CMD -input $_TESTFILE"
794 fi
795- _CMD="$_CMD -maxwarnings 4"
796+ _CMD="$_CMD -maxwarnings 40"
797 }
798
799 function execute_test_cmd {
800
801=== added file 'tests/unit_x11/tst_components/ExternalTab.qml'
802--- tests/unit_x11/tst_components/ExternalTab.qml 1970-01-01 00:00:00 +0000
803+++ tests/unit_x11/tst_components/ExternalTab.qml 2014-04-10 11:34:46 +0000
804@@ -0,0 +1,21 @@
805+/*
806+ * Copyright 2014 Canonical Ltd.
807+ *
808+ * This program is free software; you can redistribute it and/or modify
809+ * it under the terms of the GNU Lesser General Public License as published by
810+ * the Free Software Foundation; version 3.
811+ *
812+ * This program is distributed in the hope that it will be useful,
813+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
814+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
815+ * GNU Lesser General Public License for more details.
816+ *
817+ * You should have received a copy of the GNU Lesser General Public License
818+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
819+ */
820+
821+import QtQuick 2.0
822+import Ubuntu.Components 0.1
823+
824+Tab {
825+}
826
827=== modified file 'tests/unit_x11/tst_components/tst_tabs.qml'
828--- tests/unit_x11/tst_components/tst_tabs.qml 2014-01-13 12:43:12 +0000
829+++ tests/unit_x11/tst_components/tst_tabs.qml 2014-04-10 11:34:46 +0000
830@@ -27,6 +27,13 @@
831 id: emptyTabs
832 }
833
834+ Component {
835+ id: dynamicTab
836+ Tab{
837+ title: "OriginalTitle"
838+ }
839+ }
840+
841 MainView {
842 id: mainView
843 anchors.fill: parent
844@@ -424,5 +431,149 @@
845 mouseRelease(tabs.tabBar, tabs.tabBar.width/2, tabs.tabBar.height/2);
846 compare(tabs.tabBar.pressed, false, "After releasing, pressed is false");
847 }
848+
849+
850+
851+ // these tests should not be mixed with Repeaters
852+ function test_z_addTab() {
853+ var newTab = tabs.addTab("Dynamic Tab", dynamicTab);
854+ compare((newTab !== null), true, "tab added");
855+ compare(newTab.active, false, "the inserted tab is inactive");
856+ compare(newTab.index, tabs.count - 1, "the tab is the last one");
857+ }
858+
859+ function test_z_addExternalTab() {
860+ var newTab = tabs.addTab("External Tab", Qt.resolvedUrl("ExternalTab.qml"));
861+ compare((newTab !== null), true, "tab added");
862+ compare(newTab.active, false, "the inserted tab is inactive");
863+ compare(newTab.index, tabs.count - 1, "the tab is the last one");
864+ }
865+
866+ function test_z_addTabWithDefaultTitle() {
867+ var newTab = tabs.addTab("", dynamicTab);
868+ compare((newTab !== null), true, "tab added");
869+ compare(newTab.title, "OriginalTitle", "tab created with original title");
870+ }
871+
872+ function test_z_insertTab() {
873+ var tabIndex = Math.ceil(tabs.count / 2);
874+ var newTab = tabs.insertTab(tabIndex, "Inserted tab", dynamicTab);
875+ compare((newTab !== null), true, "tab inserted");
876+ compare(newTab.index, tabIndex, "this is the first tab");
877+ compare(tabs.selectedTab !== newTab, true, "the new tab is not the active one");
878+ }
879+
880+ function test_z_insertExternalTab() {
881+ var tabIndex = Math.ceil(tabs.count / 2);
882+ var newTab = tabs.insertTab(tabIndex, "Inserted External tab", Qt.resolvedUrl("ExternalTab.qml"));
883+ compare((newTab !== null), true, "tab inserted");
884+ compare(newTab.index, tabIndex, "this is the first tab");
885+ compare(tabs.selectedTab !== newTab, true, "the new tab is not the active one");
886+ }
887+
888+ function test_z_insertTabAtSelectedIndex() {
889+ tabs.selectedTabIndex = 1;
890+ var tabIndex = tabs.selectedTabIndex - 1;
891+ var newTab = tabs.insertTab(tabIndex, "InsertedAtSelected tab", dynamicTab);
892+ compare((newTab !== null), true, "tab inserted");
893+ compare(newTab.index, tabIndex, "inserted at selected tab");
894+ compare(tabs.selectedTabIndex != (tabIndex + 1), true, "it is not the selected tab");
895+ }
896+
897+ function test_z_insertTabFront() {
898+ var newTab = tabs.insertTab(-1, "PreTab", dynamicTab);
899+ compare(newTab !== null, true, "pre-tab inserted");
900+ compare(newTab.index, 0, "this is the new first tab");
901+ compare(tabs.selectedTab !== newTab, true, "the new tab is not the active one");
902+ }
903+
904+ function test_z_insertTabEnd() {
905+ var newTab = tabs.insertTab(tabs.count, "PostTab", dynamicTab);
906+ compare(newTab !== null, true, "post-tab inserted");
907+ compare(newTab.index, tabs.count - 1, "thsi is the new last tab");
908+ compare(tabs.selectedTab !== newTab, true, "the new tab is not the active one");
909+ }
910+
911+ function test_z_insertTabAndActivate() {
912+ var newTab = tabs.addTab("Inserted tab", dynamicTab);
913+ compare((newTab !== null), true, "tab inserted");
914+ compare(newTab.index, tabs.count - 1, "the tab is the last one");
915+ tabs.selectedTabIndex = newTab.index;
916+ compare(tabs.selectedTab, newTab, "the inserted tab is selected");
917+ compare(newTab.active, true, "the new tab is active");
918+ }
919+
920+ function test_z_moveTab() {
921+ var selectedIndex = tabs.count - 1;
922+ tabs.selectedTabIndex = selectedIndex;
923+ compare(tabs.moveTab(0, selectedIndex), true, "first tab moved to last");
924+ compare(tabs.selectedTabIndex, selectedIndex - 1, "the selected index moved backwards");
925+ tabs.selectedTabIndex = selectedIndex = 0;
926+ compare(tabs.moveTab(selectedIndex, selectedIndex + 1), true, "selected tab moved as next");
927+ compare(tabs.selectedTabIndex, selectedIndex + 1, "the selected index moved forewards");
928+ }
929+
930+ function test_z_moveSelectedTab() {
931+ tabs.selectedTabIndex = 0;
932+ tabs.moveTab(0, 1);
933+ compare(tabs.selectedTabIndex, 1, "selected tab moved");
934+ }
935+
936+ function test_z_moveTabFail() {
937+ compare(tabs.moveTab(-1, tabs.count - 1), false, "from-parameter out of range");
938+ compare(tabs.moveTab(0, tabs.count), false, "to-parameter out of range");
939+ }
940+
941+ function test_z_removeTab() {
942+ compare(tabs.removeTab(tabs.count - 1), true, "last tab removed");
943+ tabs.selectedTabIndex = 0;
944+ compare(tabs.removeTab(0), true, "active tab removed");
945+ compare(tabs.selectedTabIndex, 0, "the next tab is selected")
946+ }
947+
948+ function test_z_removeActiveTab() {
949+ tabs.selectedTabIndex = 1;
950+ compare(tabs.removeTab(1), true, "selected tab removed");
951+ compare(tabs.selectedTabIndex, 1, "selected tab is next");
952+
953+ tabs.selectedTabIndex = tabs.count - 1;
954+ compare(tabs.removeTab(tabs.count - 1), true, "last tab removed");
955+ compare(tabs.selectedTabIndex, tabs.count - 1, "selected tab moved to last item");
956+ }
957+
958+ function test_z_removeTabAfterActiveTab() {
959+ var activeTab = tabs.count - 2;
960+ tabs.selectedTabIndex = activeTab;
961+ compare(tabs.removeTab(tabs.count - 1), true, "last tab removed");
962+ compare(tabs.selectedTabIndex, activeTab, "the selected tab wasn't moved");
963+ }
964+
965+ function test_z_removeTabBeforeActiveTab() {
966+ var activeTab = tabs.count - 1;
967+ tabs.selectedTabIndex = activeTab;
968+ compare(tabs.removeTab(0), true, "first tab removed");
969+ compare(tabs.selectedTabIndex, activeTab - 1, "the selected tab index decreased");
970+ }
971+
972+ function test_zz_addTabAfterCleaningUpTabs() {
973+ while (tabs.count > 1) {
974+ tabs.removeTab(tabs.count - 1);
975+ }
976+ compare(tabs.selectedTabIndex, 0, "the only tab is the selected one");
977+ // add a new tab anc check the count (default added tas should not be added anymore
978+ tabs.addTab("Second tab", dynamicTab);
979+ compare(tabs.count, 2, "we have two tabs only");
980+ }
981+
982+ function test_zz_addPredeclaredTab() {
983+ tabs.removeTab(tab1.index);
984+
985+ // add a predeclared tab back with original title
986+ compare(tabs.addTab("", tab1), tab1, "tab1 was not added back");
987+ compare(tab1.title, "tab 1", "the original title differs");
988+
989+ // add a predeclared tab which was added already
990+ compare(tabs.addTab("", tab1), null, "tab1 is already in tabs");
991+ }
992 }
993 }
994
995=== modified file 'tests/unit_x11/tst_test/tst_ubuntutestcase.qml'
996--- tests/unit_x11/tst_test/tst_ubuntutestcase.qml 2014-02-13 10:27:14 +0000
997+++ tests/unit_x11/tst_test/tst_ubuntutestcase.qml 2014-04-10 11:34:46 +0000
998@@ -23,30 +23,57 @@
999 width: 800
1000 height: 600
1001
1002- MouseArea {
1003- id: mouseArea
1004- objectName: "myMouseArea"
1005- anchors.fill: parent
1006- hoverEnabled: true
1007- property int testX : 0
1008- property int testY : 0
1009- property int steps : 0
1010+ Column {
1011+ anchors.fill: parent
1012+ MouseArea {
1013+ id: mouseArea
1014+ objectName: "myMouseArea"
1015+ width: parent.width
1016+ height: 300
1017+ hoverEnabled: true
1018+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
1019+ property int testX : 0
1020+ property int testY : 0
1021+ property int steps : 0
1022
1023- onPositionChanged: {
1024- testX = mouseX;
1025- testY = mouseY;
1026- steps++;
1027- }
1028+ onPositionChanged: {
1029+ testX = mouseX;
1030+ testY = mouseY;
1031+ steps++;
1032+ }
1033+ }
1034+ Flickable {
1035+ id: flicker
1036+ width: parent.width
1037+ height: 400
1038+ contentWidth: rect.width
1039+ contentHeight: rect.height
1040+ clip: true
1041+ Rectangle {
1042+ id: rect
1043+ color: "blue"
1044+ width: 1000
1045+ height: 1000
1046+ }
1047+ }
1048 }
1049
1050 UbuntuTestCase {
1051 name: "TestTheUbuntuTestCase"
1052 when: windowShown
1053
1054+ function init() {
1055+ mouseArea.steps = 0;
1056+ }
1057+ function cleanup() {
1058+ movementSpy.clear();
1059+ longPressSpy.clear();
1060+ }
1061+
1062 function test_mouseMoveSlowly() {
1063- mouseMoveSlowly(root,0,0,800,600,10,100);
1064+ mouseMoveSlowly(root,0,0,800,300,10,100);
1065 compare(mouseArea.testX,800);
1066- compare(mouseArea.testY,600);
1067+ compare(mouseArea.testY,300);
1068 compare(mouseArea.steps,10);
1069 }
1070
1071@@ -58,5 +85,66 @@
1072 child = findChild(root,"NoSuchChildHere");
1073 compare(child===null,true,"When there is no child, function should return null");
1074 }
1075+
1076+ SignalSpy {
1077+ id: longPressSpy
1078+ target: mouseArea
1079+ signalName: "onPressAndHold"
1080+ }
1081+
1082+ function test_longPress_left() {
1083+ longPressSpy.clear();
1084+ mouseLongPress(mouseArea, mouseArea.width / 2, mouseArea.height / 2);
1085+ longPressSpy.wait();
1086+ // cleanup
1087+ mouseRelease(mouseArea, mouseArea.width / 2, mouseArea.height / 2);
1088+ }
1089+
1090+ function test_longPress_right() {
1091+ longPressSpy.clear();
1092+ mouseLongPress(mouseArea, mouseArea.width / 2, mouseArea.height / 2, Qt.RightButton);
1093+ longPressSpy.wait();
1094+ // cleanup
1095+ mouseRelease(mouseArea, mouseArea.width / 2, mouseArea.height / 2, Qt.RightButton);
1096+ }
1097+
1098+ function test_longPress_middle() {
1099+ longPressSpy.clear();
1100+ mouseLongPress(mouseArea, mouseArea.width / 2, mouseArea.height / 2, Qt.MiddleButton);
1101+ longPressSpy.wait();
1102+ // cleanup
1103+ mouseRelease(mouseArea, mouseArea.width / 2, mouseArea.height / 2, Qt.MiddleButton);
1104+ }
1105+
1106+ SignalSpy {
1107+ id: movementSpy
1108+ target: flicker
1109+ signalName: "onMovementEnded"
1110+ }
1111+
1112+ function test_flick_default() {
1113+ flick(flicker, 0, 0, flicker.width, flicker.height);
1114+ movementSpy.wait();
1115+ }
1116+ function test_flick_long() {
1117+ flick(flicker, 0, 0, flicker.width, flicker.height, -1, 10);
1118+ movementSpy.wait();
1119+ }
1120+ function test_flick_short() {
1121+ flick(flicker, 0, 0, flicker.width, flicker.height, -1, 1);
1122+ movementSpy.wait();
1123+ }
1124+ function test_flick_pressTimeout() {
1125+ flick(flicker, 0, 0, flicker.width, flicker.height, 400);
1126+ movementSpy.wait();
1127+ }
1128+ function test_flick_pressTimeout_short() {
1129+ flick(flicker, flicker.width, flicker.height, -flicker.width, -flicker.height, 400, 1);
1130+ movementSpy.wait();
1131+ }
1132+ function test_flick_pressTimeout_long() {
1133+ flick(flicker, flicker.width, flicker.height, -flicker.width, -flicker.height, 400, 100);
1134+ movementSpy.wait();
1135+ }
1136 }
1137 }

Subscribers

People subscribed via source and target branches

to status/vote changes: