Merge lp:~zsombi/ubuntu-ui-toolkit/dynamic-tabs-reloaded into lp:ubuntu-ui-toolkit/staging
- dynamic-tabs-reloaded
- Merge into staging
Status: | Merged |
---|---|
Approved by: | Tim Peeters |
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Tim Peeters | Approve | ||
Cris Dywan | provided moveitembefore docs are fixed | 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
Description of the change
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:901
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal | # |
637 + * Copyright 2012 Canonical Ltd.
2013
Cris Dywan (kalikiana) wrote : Posted in a previous version of this proposal | # |
I'm seeing a number of errors while using the tabbar test:
modules/
coming from connectToRepeaters
modules/
coming from insertTab
modules/
coming from anchors.top on the Repeater, not sure what this is
Cris 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 | # |
FAILED: Continuous integration, rev:902
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:902
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
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 | # |
PASSED: Continuous integration, rev:903
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:904
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
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:
description doesn't exactly match the function specs.
* Moves a given item before the specified item in their parent's child list
?
Cris 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.
Cris Dywan (kalikiana) wrote : Posted in a previous version of this proposal | # |
CI@HOME OK http://
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.
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:906
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal | # |
> modules/
> defined
This is still broken. Please add the include in Tabs.qml.
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/
> 'indexOf' of undefined
> coming from connectToRepeaters
> modules/
> defined
> coming from insertTab
> modules/
> 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/
> > 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 | # |
FAILED: Continuous integration, rev:907
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:908
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:909
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal | # |
ok
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:911
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
ABORTED: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:911
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:912
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:913
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) : | # |
Preview Diff
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 | } |
FAILED: Continuous integration, rev:900 jenkins. qa.ubuntu. com/job/ ubuntu- ui-toolkit- ci/1483/ jenkins. qa.ubuntu. com/job/ generic- mediumtests- trusty/ 1856/console jenkins. qa.ubuntu. com/job/ generic- mediumtests- trusty- touch/1773/ console jenkins. qa.ubuntu. com/job/ ubuntu- ui-toolkit- trusty- amd64-ci/ 431/console jenkins. qa.ubuntu. com/job/ ubuntu- ui-toolkit- trusty- armhf-ci/ 431/console jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- trusty- amd64/1856/ console jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- trusty- armhf/1773/ console
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/ubuntu- ui-toolkit- ci/1483/ rebuild
http://