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

Proposed by Zsombor Egri on 2014-04-10
Status: Merged
Approved by: Tim Peeters on 2014-04-10
Approved revision: 913
Merged at revision: 1003
Proposed branch: lp:~zsombi/ubuntu-ui-toolkit/dynamic-tabs-reloaded
Merge into: lp:ubuntu-ui-toolkit/staging
Diff against target: 865 lines (+586/-31)
10 files modified
CHANGES (+1/-0)
components.api (+6/-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)
tests/resources/navigation/Tabs.qml (+90/-1)
tests/unit_x11/tst_components/ExternalTab.qml (+21/-0)
tests/unit_x11/tst_components/tst_tabs.qml (+151/-0)
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 2014-04-10 Approve on 2014-04-10
Tim Peeters 2014-04-10 Approve on 2014-04-10
Christian Dywan provided moveitembefore docs are fixed 2014-04-10 Pending
Review via email: mp+215156@code.launchpad.net

This proposal supersedes a proposal from 2013-12-19.

Commit message

Adding dynamic tab handling functionality to Tabs

To post a comment you must log in.
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

637 + * Copyright 2012 Canonical Ltd.

2013

Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal

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
Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal

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.

PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Zsombor Egri (zsombi) wrote : Posted in a previous version of this proposal

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

It's 2014 now :)

Zsombor Egri (zsombi) wrote : Posted in a previous version of this proposal

> 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

PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

+/*!
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
?

Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal

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

review: Approve (provided moveitembefore docs are fixed)
Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

note that you didn't fix the doc yet

Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

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

Zsombor Egri (zsombi) wrote : Posted in a previous version of this proposal

> note that you didn't fix the doc yet

Sorry, done now.

Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

> 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
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

> 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

Zsombor Egri (zsombi) wrote : Posted in a previous version of this proposal

>
> > 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???

PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

ok

review: Approve
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Tim Peeters (tpeeters) wrote :

still good

review: Approve
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:913
http://jenkins.qa.ubuntu.com/job/ubuntu-sdk-team-ubuntu-ui-toolkit-staging-ci/11/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-trusty-touch/176
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-trusty/4741
    SUCCESS: http://jenkins.qa.ubuntu.com/job/ubuntu-sdk-team-ubuntu-ui-toolkit-staging-trusty-amd64-ci/11
    SUCCESS: http://jenkins.qa.ubuntu.com/job/ubuntu-sdk-team-ubuntu-ui-toolkit-staging-trusty-armhf-ci/11
        deb: http://jenkins.qa.ubuntu.com/job/ubuntu-sdk-team-ubuntu-ui-toolkit-staging-trusty-armhf-ci/11/artifact/work/output/*zip*/output.zip
    SUCCESS: http://jenkins.qa.ubuntu.com/job/ubuntu-sdk-team-ubuntu-ui-toolkit-staging-trusty-i386-ci/11
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-runner-mako/169
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-trusty-armhf/4324
        deb: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-trusty-armhf/4324/artifact/work/output/*zip*/output.zip
    SUCCESS: http://s-jenkins.ubuntu-ci:8080/job/touch-flash-device/5896
    SUCCESS: http://jenkins.qa.ubuntu.com/job/autopilot-testrunner-otto-trusty/4093
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-trusty-amd64/4870
        deb: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-trusty-amd64/4870/artifact/work/output/*zip*/output.zip

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/ubuntu-sdk-team-ubuntu-ui-toolkit-staging-ci/11/rebuild

review: Approve (continuous-integration)
review: Approve (continuous-integration)

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:35:42 +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-10 05:14:42 +0000
15+++ components.api 2014-04-10 11:35:42 +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
33=== modified file 'modules/Ubuntu/Components/Tab.qml'
34--- modules/Ubuntu/Components/Tab.qml 2013-11-07 07:26:01 +0000
35+++ modules/Ubuntu/Components/Tab.qml 2014-04-10 11:35:42 +0000
36@@ -105,12 +105,5 @@
37 Tab is destroyed upon removal.
38 */
39 property bool dynamic: false
40-
41- /*
42- This flag is used by the Tabs to determine whether the pre-declared Tab was removed
43- from the Tabs model or not. The flag guards adding back pre-declared tabs upon Tabs
44- component stack (children) change.
45- */
46- property bool removedFromTabs: false
47 }
48 }
49
50=== modified file 'modules/Ubuntu/Components/Tabs.qml'
51--- modules/Ubuntu/Components/Tabs.qml 2014-04-08 12:38:35 +0000
52+++ modules/Ubuntu/Components/Tabs.qml 2014-04-10 11:35:42 +0000
53@@ -15,6 +15,7 @@
54 */
55
56 import QtQuick 2.0
57+import "mathUtils.js" as MathUtils
58
59 /*!
60 \qmltype Tabs
61@@ -149,6 +150,110 @@
62 }
63 }
64 \endqml
65+
66+ \section2 Dynamic tabs
67+ So far all Tab elements were pre-declared, but there can be situations when
68+ tabs need to be added dynamically. There are two ways to solve this, depending
69+ on the output needed.
70+
71+ \section3 Using Repeaters
72+ A Repeater can be used to create the necessary tabs depending on a given model.
73+ In this way the number of tabs will be driven by the model itself.
74+ An example of such a dynamic tab:
75+ \qml
76+ // DynamicTab.qml
77+ import QtQuick 2.0
78+ import Ubuntu.Components 0.1
79+
80+ Tabs {
81+ property alias model: tabRepeater.model
82+ Repeater {
83+ id: tabRepeater
84+ model: 5
85+ Tab {
86+ title: "Tab #" + index
87+ page: Page {
88+ // [...]
89+ }
90+ }
91+ }
92+ }
93+ \endqml
94+ Note that in the example above the Tabs will be re-created each time the model
95+ changes. This will cause state losing of each Tab, which depending on the
96+ content type can be solved at some extent using StateSaver. Using a Loader
97+ or specifying the Tab instance/component in the model the state can be preserved,
98+ however may increase code complexity.
99+
100+ \section3 Dynamic tabs
101+ Tabs provides functions to add Tab elements dynamically on runtime, without
102+ destroying the state of the existing tabs. You can add, move and remove any
103+ kind of Tab element, pre-declared or dynamically created ones. When removing
104+ pre-declared tabs, those will all be held back and hidden by Tabs, and can be
105+ added back any time either to the same or to a different position.
106+
107+ \qml
108+ import QtQuick 2.0
109+ import Ubuntu.Components 0.1
110+
111+ MainView {
112+ width: units.gu(40)
113+ height: units.gu(71)
114+
115+ Component {
116+ id: dynamicTab
117+ Tab {
118+ page: Page {
119+ Label {
120+ text: title
121+ anchors.centerIn: parent
122+ }
123+ }
124+ }
125+ }
126+ Tabs {
127+ id: tabs
128+ Tab {
129+ title: "Main tab"
130+ page: Page {
131+ toolbar: ToolbarItems {
132+ ToolbarButton {
133+ text: "remove predeclared"
134+ onTriggered: tabs.removeTab(preDeclared.index)
135+ }
136+ ToolbarButton {
137+ text: "add new"
138+ onTriggered: tabs.addTab("New tab", dynamicTab)
139+ }
140+ ToolbarButton {
141+ text: "insert predeclared"
142+ onTriggered: tabs.insertTab("", 0)
143+ }
144+ }
145+ }
146+ }
147+ Tab {
148+ id: preDeclared
149+ title: "Pre-declared tab"
150+ page: Page {
151+ Label {
152+ text: "This is a predeclared tab at index #" + index
153+ anchors.centerIn: parent
154+ }
155+ }
156+ }
157+ }
158+ }
159+ \endqml
160+
161+ \section3 Using Repeater and functions together
162+ Repeaters re-create their delegates as many times the model changes. Tabs added
163+ or moved in between the tabs maintained by the Repeater, as well as reordered
164+ through the Tabs functions will be re-arranged once the Repeater's model changes.
165+ This should be taken into account when designing the application, and the use
166+ of Repeater and functions toghether should be avoided if possible, or at least
167+ Repeater should always add tabs to te tail of the tab stack, and no tab insertion
168+ happens in that area.
169 */
170 PageTreeNode {
171 id: tabs
172@@ -186,10 +291,14 @@
173 }
174
175 /*!
176- Children are placed in a separate item that has functionality to extract the Tab items.
177+ \deprecated
178+ Children are placed in a separate item that has functionality to extract
179+ the Tab items.
180+ Note: this property is deprecated. Tab components are directly parented
181+ to Tabs' data property.
182 \qmlproperty list<Item> tabChildren
183 */
184- default property alias tabChildren: tabStack.data
185+ property alias tabChildren: tabs.data
186
187 /*!
188 \qmlproperty int count
189@@ -206,36 +315,180 @@
190 signal modelChanged()
191
192 /*!
193+ Appends a Tab dynamically to the list of tabs. The \a title specifies the
194+ title of the Tab. The \a tab can be either a Component, a URL to a Tab
195+ component to be loaded or an instance of a pre-declared tab that has been
196+ previously removed. The Tab's title will be replaced with the given \a title,
197+ unless the value is an empty string or undefined.
198+ Returns the instance of the added Tab.
199+ */
200+ function addTab(title, tab) {
201+ return insertTab(count, title, tab);
202+ }
203+
204+ /*!
205+ Inserts a Tab at the given index. If the \a index is less or equal than 0,
206+ the Tab will be added to the front, and to the end of the tab stack in case
207+ the \a index is greater than \l count. \a title and \a tab are used in the
208+ same way as with \l addTab().
209+ Returns the instance of the inserted Tab.
210+ */
211+ function insertTab(index, title, tab) {
212+ // check if the given component is a Tab instance
213+ var tabObject = null;
214+
215+ if (typeof tab === "string") {
216+ // we have a URL
217+ var tabComponent = Qt.createComponent(tab);
218+ if (tabComponent.status === Component.Error) {
219+ console.error(tabComponent.errorString());
220+ return null;
221+ }
222+ tabObject = tabComponent.createObject();
223+ tabObject.__protected.dynamic = true;
224+ } else if (tab.hasOwnProperty("createObject")) {
225+ // we have a Component
226+ tabObject = tab.createObject();
227+ tabObject.__protected.dynamic = true;
228+ } else if (tab.hasOwnProperty("parent") && tab.parent === trashedTabs) {
229+ // we have a pre-declared tab that has been removed
230+ tabObject = tab;
231+ } else {
232+ console.error(i18n.tr("The object is not a URL, Component or a removed Tab: ") + tab);
233+ return null;
234+ }
235+
236+ // fix title
237+ if (title !== undefined && title !== "") {
238+ tabObject.title = title;
239+ }
240+
241+ // insert the created tab into the model
242+ index = MathUtils.clamp(index, 0, count);
243+ tabObject.__protected.inserted = true;
244+ tabObject.__protected.index = index;
245+ tabsModel.insertTab(tabObject, index);
246+ if (tabs.selectedTabIndex >= index) {
247+ // move the selected index to the next index
248+ tabs.selectedTabIndex += 1;
249+ } else {
250+ internal.sync();
251+ }
252+ return tabObject;
253+ }
254+
255+ /*!
256+ The function returns the Tab from the given \a index, or null if the \a index
257+ is invalid (less than \c 0 and greater than \l count).
258+ */
259+ function getTab(index) {
260+ return (index >=0) && (index < count) ? tabsModel.get(index).tab : null;
261+ }
262+
263+ /*!
264+ Moves the tab from the given \a from position to the position given in \a to.
265+ Returns true if the indexes were in 0..\l count - 1 boundary and if the operation
266+ succeeds, and false otherwise. The \l selectedTabIndex is updated if it is
267+ affected by the move (it is equal with \a from or falls between \a from and
268+ \a to indexes).
269+ */
270+ function moveTab(from, to) {
271+ if (from < 0 || from >= count || to < 0 || to >= count || from === to) return false;
272+ var tabFrom = tabsModel.get(from).tab;
273+ var tabTo = tabsModel.get(to).tab;
274+
275+ // move tab
276+ QuickUtils.moveItemBefore(tabFrom, tabTo);
277+ tabsModel.updateTabList(tabs.children);
278+
279+ // fix selected tab
280+ if (selectedTabIndex === from) {
281+ selectedTabIndex = to;
282+ } else if (selectedTabIndex >= Math.min(from, to) && selectedTabIndex <= Math.max(from, to)) {
283+ selectedTabIndex--;
284+ } else {
285+ internal.sync();
286+ }
287+
288+ return true;
289+ }
290+
291+ /*!
292+ Removes the Tab from the given \a index. Returns true if the \a index falls
293+ into 0..\l count - 1 boundary and the operation succeeds, and false on error.
294+ The function removes also the pre-declared tabs. These can be added back using
295+ \l addTab or \l insertTab by specifying the instance of the Tab to be added as
296+ component. The \l selectedTabIndex is updated if is affected by the removal
297+ (it is identical or greater than the tab index to be removed).
298+ */
299+ function removeTab(index) {
300+ if (index < 0 || index >= count) return false;
301+ var tab = tabsModel.get(index).tab;
302+ var activeIndex = (selectedTabIndex >= index) ? MathUtils.clamp(selectedTabIndex, 0, count - 2) : -1;
303+
304+ // remove from Tabs; Tabs children change will remove the tab from the model
305+ tab.parent = null;
306+ if (tab.__protected.dynamic) {
307+ tab.destroy();
308+ } else {
309+ // pre-declared tab, mark it as removed, so we don't update it next time
310+ // the tabs stack children is updated
311+ tab.parent = trashedTabs;
312+ }
313+
314+ // move active tab if needed
315+ if (activeIndex >= 0 && activeIndex !== selectedTabIndex) {
316+ selectedTabIndex = activeIndex;
317+ } else {
318+ internal.sync();
319+ }
320+
321+ return true;
322+ }
323+
324+ /*! \internal */
325+ onChildrenChanged: {
326+ internal.connectToRepeaters(tabs.children);
327+ tabsModel.updateTabList(tabs.children);
328+ }
329+
330+ /*!
331 \internal
332 required by TabsStyle
333 */
334 ListModel {
335 id: tabsModel
336
337+ property bool updateDisabled: false
338+
339 function listModel(tab) {
340 return {"title": tab.title, "tab": tab};
341 }
342
343 function updateTabList(tabsList) {
344+ if (updateDisabled) return;
345 var offset = 0;
346- var tabIndex;
347+ var tabIndex = -1;
348 for (var i in tabsList) {
349 var tab = tabsList[i];
350 if (internal.isTab(tab)) {
351 tabIndex = i - offset;
352 // make sure we have the right parent
353- tab.parent = tabStack;
354+ tab.parent = tabs;
355
356 if (!tab.__protected.inserted) {
357 tab.__protected.index = tabIndex;
358 tab.__protected.inserted = true;
359 insert(tabIndex, listModel(tab));
360- } else if (!tab.__protected.removedFromTabs && tabsModel.count > tab.index) {
361+ } else {
362 get(tab.index).title = tab.title;
363 }
364
365 // always makes sure that tabsModel has the same order as tabsList
366- move(tab.__protected.index, tabIndex, 1);
367+ // but move only if there is more than one item in the list
368+ if (count > 1) {
369+ move(tab.__protected.index, tabIndex, 1);
370+ }
371 reindex();
372 } else {
373 // keep track of children that are not tabs so that we compute
374@@ -243,6 +496,10 @@
375 offset += 1;
376 }
377 }
378+ // remove deleted tabs, those should be at the end of the list by now
379+ if ((tabIndex >= 0) && (tabIndex + 1) < count) {
380+ remove(tabIndex + 1, count - tabIndex - 1);
381+ }
382 internal.sync();
383 }
384
385@@ -257,18 +514,31 @@
386 tab.__protected.index = i;
387 }
388 }
389+
390+ function insertTab(tab, index) {
391+ // fix index
392+ if (index < 0) {
393+ index = 0;
394+ }
395+ // get the tab before which the item will be inserted
396+ var itemAtIndex = ((index >= 0) && (index < count)) ? get(index).tab : null;
397+ // disable update only if we insert, append can keep the logic rolling
398+ updateDisabled = (itemAtIndex !== null);
399+ insert(index, listModel(tab));
400+ tab.parent = tabs;
401+ updateDisabled = false;
402+ if (itemAtIndex) {
403+ QuickUtils.moveItemBefore(tab, itemAtIndex);
404+ updateTabList(tabs.children);
405+ }
406+ }
407 }
408
409- // FIXME: this component is not really needed, as it doesn't really bring any
410- // value; should be removed in a later MR
411+ // invisible component stacking removed pre-declared components
412 Item {
413- anchors.fill: parent
414- id: tabStack
415-
416- onChildrenChanged: {
417- internal.connectToRepeaters(tabStack.children);
418- tabsModel.updateTabList(tabStack.children);
419- }
420+ id: trashedTabs
421+ visible: false
422+ opacity: 0.0
423 }
424
425 /*
426@@ -283,7 +553,7 @@
427 interval: 1
428 running: false
429 onTriggered: {
430- tabsModel.updateTabList(tabStack.children);
431+ tabsModel.updateTabList(tabs.children);
432 internal.sync();
433 }
434 }
435@@ -295,8 +565,8 @@
436 Binding {
437 target: tabBar
438 property: "animate"
439- when: internal.header && internal.header.hasOwnProperty("animate")
440- value: internal.header.animate
441+ when: (internal.header !== null) && internal.header.hasOwnProperty("animate")
442+ value: internal.header ? internal.header.animate : "false"
443 }
444
445 /*
446@@ -332,7 +602,9 @@
447 function connectToRepeaters(children) {
448 for (var i = 0; i < children.length; i++) {
449 var child = children[i];
450- if (internal.isRepeater(child) && (internal.repeaters.indexOf(child) < 0)) {
451+ if (internal.isRepeater(child) &&
452+ (internal.repeaters !== undefined) &&
453+ (internal.repeaters.indexOf(child) < 0)) {
454 internal.connectRepeater(child);
455 }
456 }
457@@ -355,7 +627,7 @@
458 https://bugreports.qt-project.org/browse/QTBUG-32438
459 */
460 function updateTabsModel() {
461- tabsModel.updateTabList(tabStack.children);
462+ tabsModel.updateTabList(tabs.children);
463 }
464
465 /*
466
467=== modified file 'modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml'
468--- modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml 2014-04-08 12:38:35 +0000
469+++ modules/Ubuntu/Components/Themes/Ambiance/TabBarStyle.qml 2014-04-10 11:35:42 +0000
470@@ -116,8 +116,8 @@
471 AbstractButton {
472 id: button
473 anchors {
474- top: parent.top
475- bottom: parent.bottom
476+ top: parent ? parent.top : undefined
477+ bottom: parent ? parent.bottom: undefined
478 }
479 width: text.paintedWidth + text.anchors.leftMargin + text.anchors.rightMargin
480
481@@ -150,6 +150,13 @@
482 return false;
483 }
484
485+ // update the offset of the buttonRow
486+ onOffsetChanged: {
487+ if (selected) {
488+ buttonView.updateOffset(button.offset);
489+ }
490+ }
491+
492 Behavior on opacity {
493 NumberAnimation {
494 duration: headerTextFadeDuration
495
496=== modified file 'modules/Ubuntu/Components/plugin/quickutils.cpp'
497--- modules/Ubuntu/Components/plugin/quickutils.cpp 2014-03-20 15:46:28 +0000
498+++ modules/Ubuntu/Components/plugin/quickutils.cpp 2014-04-10 11:35:42 +0000
499@@ -119,6 +119,21 @@
500 return result.left(result.indexOf("_QML"));
501 }
502
503+/*!
504+ * \internal
505+ * Moves a given \a item before the \a other one in the object stack. Both \a item
506+ * and \a other must have the same parent item.
507+ */
508+void QuickUtils::moveItemBefore(QQuickItem *item, QQuickItem *other)
509+{
510+ Q_ASSERT(item);
511+ Q_ASSERT(item->parentItem());
512+ if (other) {
513+ Q_ASSERT(other->parentItem() == item->parentItem());
514+ item->stackBefore(other);
515+ }
516+}
517+
518
519 /*!
520 * \internal
521
522=== modified file 'modules/Ubuntu/Components/plugin/quickutils.h'
523--- modules/Ubuntu/Components/plugin/quickutils.h 2014-03-20 15:46:28 +0000
524+++ modules/Ubuntu/Components/plugin/quickutils.h 2014-04-10 11:35:42 +0000
525@@ -42,6 +42,7 @@
526 QString inputMethodProvider() const;
527
528 Q_INVOKABLE static QString className(QObject *item);
529+ Q_INVOKABLE void moveItemBefore(QQuickItem *item, QQuickItem *before);
530 QObject* createQmlObject(const QUrl &url, QQmlEngine *engine);
531
532 Q_SIGNALS:
533
534=== modified file 'tests/resources/navigation/Tabs.qml'
535--- tests/resources/navigation/Tabs.qml 2014-04-07 10:03:39 +0000
536+++ tests/resources/navigation/Tabs.qml 2014-04-10 11:35:42 +0000
537@@ -19,9 +19,47 @@
538 import Ubuntu.Components.ListItems 0.1 as ListItem
539
540 MainView {
541+ id: root
542 width: 800
543 height: 600
544
545+ property var repeaterModel: 3
546+
547+ Component {
548+ id: dynamicTab
549+ Tab {
550+ page: Page {
551+ Label {
552+ text: title + " at index " + index
553+ anchors.centerIn: parent
554+ }
555+ tools: ToolbarItems {
556+ ToolbarButton {
557+ text: "move @1"
558+ onTriggered: {
559+ print("MOVE TAB TO #1")
560+ tabs.moveTab(index, 1)
561+ }
562+ }
563+ ToolbarButton {
564+ text: "remove me"
565+ onTriggered: {
566+ print("REMOVE CURENT TAB")
567+ tabs.removeTab(index)
568+ }
569+ }
570+ ToolbarButton {
571+ text: "remove first"
572+ onTriggered: {
573+ print("REMOVE TAB AT #0")
574+ tabs.removeTab(0)
575+ }
576+ }
577+ }
578+ }
579+ }
580+ }
581+
582 Tabs {
583 id: tabs
584 selectedTabIndex: 0
585@@ -31,6 +69,7 @@
586
587 Tab {
588 id: simpleTab
589+ objectName: title
590 title: i18n.tr("Simple page #" + index)
591 page: Page {
592 Row {
593@@ -55,13 +94,60 @@
594 iconSource: "call_icon.png"
595 onTriggered: print("action triggered")
596 }
597+ ToolbarButton {
598+ text: "append"
599+ onTriggered: {
600+ print("APPEND TAB")
601+ tabs.addTab("Appended tab", dynamicTab)
602+ }
603+ }
604+ ToolbarButton {
605+ text: "insert@1"
606+ onTriggered: {
607+ print("INSERT TAB TO #1")
608+ tabs.insertTab(1, "Inserted tab", dynamicTab)
609+ }
610+ }
611+ ToolbarButton {
612+ text: "insert@2"
613+ onTriggered: {
614+ print("INSERT BETWEEN REPEATERS #1")
615+ tabs.insertTab(2, "Between repeaters", dynamicTab)
616+ }
617+ }
618+ ToolbarButton {
619+ text: "insert@here"
620+ onTriggered: {
621+ print("INSERT AFTER ME")
622+ tabs.insertTab(simpleTab.index, "Inserted tab", dynamicTab)
623+ }
624+ }
625+ ToolbarButton {
626+ text: "incRep"
627+ onTriggered: {
628+ print("INCREASE REPEATER MODEL")
629+ root.repeaterModel += 1
630+ }
631+ }
632+ ToolbarButton {
633+ text: "remove last"
634+ onTriggered: {
635+ print("REMOVE LAST TAB")
636+ tabs.removeTab(tabs.count - 1)
637+ }
638+ }
639+ ToolbarButton {
640+ text: "append predec"
641+ onTriggered: tabs.addTab("Re-added ListView", listViewTab)
642+ }
643 }
644 }
645 }
646 Repeater {
647- model: 3
648+ model: root.repeaterModel
649 Tab {
650 id: tab
651+ objectName: title
652 title: "Extra #" + tab.index
653 page: Page {
654 Column {
655@@ -88,6 +174,7 @@
656 }
657 Tab {
658 id: externalTab
659+ objectName: title
660 title: i18n.tr("External #" + index)
661 page: Loader {
662 parent: externalTab
663@@ -96,6 +183,8 @@
664 }
665 }
666 Tab {
667+ id: listViewTab
668+ objectName: title
669 title: i18n.tr("List view #" + index)
670 page: Page {
671 ListView {
672
673=== added file 'tests/unit_x11/tst_components/ExternalTab.qml'
674--- tests/unit_x11/tst_components/ExternalTab.qml 1970-01-01 00:00:00 +0000
675+++ tests/unit_x11/tst_components/ExternalTab.qml 2014-04-10 11:35:42 +0000
676@@ -0,0 +1,21 @@
677+/*
678+ * Copyright 2014 Canonical Ltd.
679+ *
680+ * This program is free software; you can redistribute it and/or modify
681+ * it under the terms of the GNU Lesser General Public License as published by
682+ * the Free Software Foundation; version 3.
683+ *
684+ * This program is distributed in the hope that it will be useful,
685+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
686+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
687+ * GNU Lesser General Public License for more details.
688+ *
689+ * You should have received a copy of the GNU Lesser General Public License
690+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
691+ */
692+
693+import QtQuick 2.0
694+import Ubuntu.Components 0.1
695+
696+Tab {
697+}
698
699=== modified file 'tests/unit_x11/tst_components/tst_tabs.qml'
700--- tests/unit_x11/tst_components/tst_tabs.qml 2014-01-13 12:43:12 +0000
701+++ tests/unit_x11/tst_components/tst_tabs.qml 2014-04-10 11:35:42 +0000
702@@ -27,6 +27,13 @@
703 id: emptyTabs
704 }
705
706+ Component {
707+ id: dynamicTab
708+ Tab{
709+ title: "OriginalTitle"
710+ }
711+ }
712+
713 MainView {
714 id: mainView
715 anchors.fill: parent
716@@ -424,5 +431,149 @@
717 mouseRelease(tabs.tabBar, tabs.tabBar.width/2, tabs.tabBar.height/2);
718 compare(tabs.tabBar.pressed, false, "After releasing, pressed is false");
719 }
720+
721+
722+
723+ // these tests should not be mixed with Repeaters
724+ function test_z_addTab() {
725+ var newTab = tabs.addTab("Dynamic Tab", dynamicTab);
726+ compare((newTab !== null), true, "tab added");
727+ compare(newTab.active, false, "the inserted tab is inactive");
728+ compare(newTab.index, tabs.count - 1, "the tab is the last one");
729+ }
730+
731+ function test_z_addExternalTab() {
732+ var newTab = tabs.addTab("External Tab", Qt.resolvedUrl("ExternalTab.qml"));
733+ compare((newTab !== null), true, "tab added");
734+ compare(newTab.active, false, "the inserted tab is inactive");
735+ compare(newTab.index, tabs.count - 1, "the tab is the last one");
736+ }
737+
738+ function test_z_addTabWithDefaultTitle() {
739+ var newTab = tabs.addTab("", dynamicTab);
740+ compare((newTab !== null), true, "tab added");
741+ compare(newTab.title, "OriginalTitle", "tab created with original title");
742+ }
743+
744+ function test_z_insertTab() {
745+ var tabIndex = Math.ceil(tabs.count / 2);
746+ var newTab = tabs.insertTab(tabIndex, "Inserted tab", dynamicTab);
747+ compare((newTab !== null), true, "tab inserted");
748+ compare(newTab.index, tabIndex, "this is the first tab");
749+ compare(tabs.selectedTab !== newTab, true, "the new tab is not the active one");
750+ }
751+
752+ function test_z_insertExternalTab() {
753+ var tabIndex = Math.ceil(tabs.count / 2);
754+ var newTab = tabs.insertTab(tabIndex, "Inserted External tab", Qt.resolvedUrl("ExternalTab.qml"));
755+ compare((newTab !== null), true, "tab inserted");
756+ compare(newTab.index, tabIndex, "this is the first tab");
757+ compare(tabs.selectedTab !== newTab, true, "the new tab is not the active one");
758+ }
759+
760+ function test_z_insertTabAtSelectedIndex() {
761+ tabs.selectedTabIndex = 1;
762+ var tabIndex = tabs.selectedTabIndex - 1;
763+ var newTab = tabs.insertTab(tabIndex, "InsertedAtSelected tab", dynamicTab);
764+ compare((newTab !== null), true, "tab inserted");
765+ compare(newTab.index, tabIndex, "inserted at selected tab");
766+ compare(tabs.selectedTabIndex != (tabIndex + 1), true, "it is not the selected tab");
767+ }
768+
769+ function test_z_insertTabFront() {
770+ var newTab = tabs.insertTab(-1, "PreTab", dynamicTab);
771+ compare(newTab !== null, true, "pre-tab inserted");
772+ compare(newTab.index, 0, "this is the new first tab");
773+ compare(tabs.selectedTab !== newTab, true, "the new tab is not the active one");
774+ }
775+
776+ function test_z_insertTabEnd() {
777+ var newTab = tabs.insertTab(tabs.count, "PostTab", dynamicTab);
778+ compare(newTab !== null, true, "post-tab inserted");
779+ compare(newTab.index, tabs.count - 1, "thsi is the new last tab");
780+ compare(tabs.selectedTab !== newTab, true, "the new tab is not the active one");
781+ }
782+
783+ function test_z_insertTabAndActivate() {
784+ var newTab = tabs.addTab("Inserted tab", dynamicTab);
785+ compare((newTab !== null), true, "tab inserted");
786+ compare(newTab.index, tabs.count - 1, "the tab is the last one");
787+ tabs.selectedTabIndex = newTab.index;
788+ compare(tabs.selectedTab, newTab, "the inserted tab is selected");
789+ compare(newTab.active, true, "the new tab is active");
790+ }
791+
792+ function test_z_moveTab() {
793+ var selectedIndex = tabs.count - 1;
794+ tabs.selectedTabIndex = selectedIndex;
795+ compare(tabs.moveTab(0, selectedIndex), true, "first tab moved to last");
796+ compare(tabs.selectedTabIndex, selectedIndex - 1, "the selected index moved backwards");
797+ tabs.selectedTabIndex = selectedIndex = 0;
798+ compare(tabs.moveTab(selectedIndex, selectedIndex + 1), true, "selected tab moved as next");
799+ compare(tabs.selectedTabIndex, selectedIndex + 1, "the selected index moved forewards");
800+ }
801+
802+ function test_z_moveSelectedTab() {
803+ tabs.selectedTabIndex = 0;
804+ tabs.moveTab(0, 1);
805+ compare(tabs.selectedTabIndex, 1, "selected tab moved");
806+ }
807+
808+ function test_z_moveTabFail() {
809+ compare(tabs.moveTab(-1, tabs.count - 1), false, "from-parameter out of range");
810+ compare(tabs.moveTab(0, tabs.count), false, "to-parameter out of range");
811+ }
812+
813+ function test_z_removeTab() {
814+ compare(tabs.removeTab(tabs.count - 1), true, "last tab removed");
815+ tabs.selectedTabIndex = 0;
816+ compare(tabs.removeTab(0), true, "active tab removed");
817+ compare(tabs.selectedTabIndex, 0, "the next tab is selected")
818+ }
819+
820+ function test_z_removeActiveTab() {
821+ tabs.selectedTabIndex = 1;
822+ compare(tabs.removeTab(1), true, "selected tab removed");
823+ compare(tabs.selectedTabIndex, 1, "selected tab is next");
824+
825+ tabs.selectedTabIndex = tabs.count - 1;
826+ compare(tabs.removeTab(tabs.count - 1), true, "last tab removed");
827+ compare(tabs.selectedTabIndex, tabs.count - 1, "selected tab moved to last item");
828+ }
829+
830+ function test_z_removeTabAfterActiveTab() {
831+ var activeTab = tabs.count - 2;
832+ tabs.selectedTabIndex = activeTab;
833+ compare(tabs.removeTab(tabs.count - 1), true, "last tab removed");
834+ compare(tabs.selectedTabIndex, activeTab, "the selected tab wasn't moved");
835+ }
836+
837+ function test_z_removeTabBeforeActiveTab() {
838+ var activeTab = tabs.count - 1;
839+ tabs.selectedTabIndex = activeTab;
840+ compare(tabs.removeTab(0), true, "first tab removed");
841+ compare(tabs.selectedTabIndex, activeTab - 1, "the selected tab index decreased");
842+ }
843+
844+ function test_zz_addTabAfterCleaningUpTabs() {
845+ while (tabs.count > 1) {
846+ tabs.removeTab(tabs.count - 1);
847+ }
848+ compare(tabs.selectedTabIndex, 0, "the only tab is the selected one");
849+ // add a new tab anc check the count (default added tas should not be added anymore
850+ tabs.addTab("Second tab", dynamicTab);
851+ compare(tabs.count, 2, "we have two tabs only");
852+ }
853+
854+ function test_zz_addPredeclaredTab() {
855+ tabs.removeTab(tab1.index);
856+
857+ // add a predeclared tab back with original title
858+ compare(tabs.addTab("", tab1), tab1, "tab1 was not added back");
859+ compare(tab1.title, "tab 1", "the original title differs");
860+
861+ // add a predeclared tab which was added already
862+ compare(tabs.addTab("", tab1), null, "tab1 is already in tabs");
863+ }
864 }
865 }

Subscribers

People subscribed via source and target branches