2
* Copyright 2015 Canonical Ltd.
4
* This program is free software; you can redistribute it and/or modify
5
* it under the terms of the GNU Lesser General Public License as published by
6
* the Free Software Foundation; version 3.
8
* This program is distributed in the hope that it will be useful,
9
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
* GNU Lesser General Public License for more details.
13
* You should have received a copy of the GNU Lesser General Public License
14
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
import QtQuick.Layouts 1.1
19
import Ubuntu.Components 1.3
20
import "tree.js" as Tree
23
\qmltype AdaptivePageLayout
24
\inqmlmodule Ubuntu.Components 1.3
25
\since Ubuntu.Components 1.3
27
\brief View with multiple columns of Pages.
29
The component provides a flexible way of viewing a stack of pages in one or
30
more columns. Unlike in PageStack, there can be more than one Page active at
31
a time, depending on the number of the columns in the view.
33
AdaptivePageLayout stores pages added in a tree. Pages are added relative to a
34
given page, either as sibling (\l addPageToCurrentColumn) or as child
35
(\l addPageToNextColumn). This means that removing a non-leaf page from the Page
36
tree will remove all its children from the page tree.
38
The columns are populated from left to right. The column a page is added to is
39
detected based on the source page that is given to the functions adding the page.
40
The pages can be added either to the same column the source page resides or to
41
the column next to the source page. Giving a null value to the source page will
42
add the page to the leftmost column of the view.
44
The primary page, the very first page must be specified through the \l primaryPage
45
property. The property cannot be changed after component completion and can hold
46
a Page instance, a Component or a url to a document defining a Page. The page
47
cannot be removed from the view.
51
import Ubuntu.Components 1.3
65
text: "Add Page2 above " + page1.title
66
onClicked: page1.pageStack.addPageToCurrentColumn(page1, page2)
69
text: "Add Page3 next to " + page1.title
70
onClicked: page1.pageStack.addPageToNextColumn(page1, page3)
86
AdaptivePageLayout supports adaptive column handling. When the number of columns changes at
87
runtime the pages are automatically rearranged.
96
// AdaptivePageLayout has its own split headers, so
97
// disable the application header.
98
id: appHeaderControlPage
103
// title is set in attachPage() when the attached Page.column === 0
107
The property holds the first Page which will be added to the view. If the
108
view has more than one column, the page will be added to the leftmost column.
109
The property can hold either a Page instance, a component holding a Page
110
or a QML document defining the Page. The property cannot be changed after
111
component completion.
113
property Page primaryPage
116
\qmlmethod Item addPageToCurrentColumn(Item sourcePage, var page[, var properties])
117
Adds a \c page to the column the \c sourcePage resides in and removes all pages
118
from the higher columns. \c page can be a Component or a file.
119
\c properties is a JSON object containing properties
120
to be set when page is created. \c sourcePage must be active. Returns the
121
instance of the page created.
123
function addPageToCurrentColumn(sourcePage, page, properties) {
124
var nextColumn = d.columnForPage(sourcePage) + 1;
125
// Clear all following columns. If we do not do this, we have no way to
126
// determine which page needs to be on top when the view is resized
127
// to have a single column.
128
d.tree.prune(nextColumn);
129
for (var i = nextColumn; i < d.columns; i++) {
130
d.updatePageForColumn(i);
132
return d.addPageToColumn(nextColumn - 1, sourcePage, page, properties);
136
\qmlmethod Item addPageToNextColumn(Item sourcePage, var page[, var properties])
137
Remove all previous pages from the next column (relative to the column that
138
holds \c sourcePage) and all following columns, and then add \c page to the next column.
139
If \c sourcePage is located in the
140
rightmost column, the new page will be pushed to the same column as \c sourcePage.
142
function addPageToNextColumn(sourcePage, page, properties) {
143
var nextColumn = d.columnForPage(sourcePage) + 1;
144
d.tree.prune(nextColumn);
145
for (var i = nextColumn; i < d.columns; i++) {
146
d.updatePageForColumn(i);
148
return d.addPageToColumn(nextColumn, sourcePage, page, properties);
152
\qmlmethod void removePages(Item page)
153
The function removes and deletes all pages up to and including \c page
154
is reached. If the \a page is the same as the \l primaryPage, only its child
155
pages will be removed.
157
function removePages(page) {
158
var nodeToRemove = d.getWrapper(page);
159
var removedNodes = d.tree.chop(nodeToRemove, page != layout.primaryPage);
160
for (var i = removedNodes.length-1; i >= 0; i--) {
161
var node = removedNodes[i];
162
d.updatePageForColumn(node.column);
170
Component.onCompleted: {
174
var wrapper = d.createWrapper(primaryPage);
175
d.addWrappedPage(wrapper);
177
console.warn("No primary page set. No pages can be added without a primary page.");
180
onPrimaryPageChanged: {
182
console.warn("Cannot change primaryPage after completion.");
190
property bool completed: false
191
property var tree: new Tree.Tree()
193
property int columns: layout.width >= units.gu(80) ? 2 : 1
197
console.warn("There must be a minimum of one column set.");
202
property real defaultColumnWidth: units.gu(40)
203
onDefaultColumnWidthChanged: body.applyMetrics()
204
property list<ColumnMetrics> columnMetrics
206
function createWrapper(page, properties) {
207
var wrapperComponent = Qt.createComponent("PageWrapper.qml");
208
var wrapperObject = wrapperComponent.createObject(hiddenPages);
209
wrapperObject.pageStack = layout;
210
wrapperObject.properties = properties;
211
// set reference last because it will trigger creation of the object
212
// with specified properties.
213
wrapperObject.reference = page;
214
return wrapperObject;
217
function addWrappedPage(pageWrapper) {
218
pageWrapper.parentWrapper = d.getWrapper(pageWrapper.parentPage);
219
tree.add(pageWrapper.column, pageWrapper.parentWrapper, pageWrapper);
220
var targetColumn = MathUtils.clamp(pageWrapper.column, 0, d.columns - 1);
221
// replace page holder's child
222
var holder = body.children[targetColumn];
223
holder.detachCurrentPage();
224
holder.attachPage(pageWrapper)
227
function getWrapper(page) {
228
if (page && page.hasOwnProperty("parentNode")) {
229
var w = page.parentNode;
230
if (w && w.hasOwnProperty("object") && w.hasOwnProperty("reference")) {
231
if (w.object == page) {
234
print("Page is not wrapped by its parentNode. This should not happen!");
247
function columnForPage(page) {
248
var wrapper = d.getWrapper(page);
249
return wrapper ? wrapper.column : 0;
252
function addPageToColumn(column, sourcePage, page, properties) {
254
console.warn("Column must be >= 0.");
258
console.warn("No sourcePage specified. Page will not be added.");
261
var sourceWrapper = d.getWrapper(sourcePage);
262
if (d.tree.index(sourceWrapper) === -1) {
263
console.warn("sourcePage must be added to the view to add new page.");
267
// Check that the Page was not already added.
268
if (typeof page !== "string" && !page.createObject) {
269
// page is neither a url or a Component so it must be a Page object.
271
var oldWrapper = getWrapper(page);
272
if (oldWrapper && d.tree.index(oldWrapper) !== -1) {
273
console.warn("Cannot add a Page that was already added.");
278
var newWrapper = d.createWrapper(page, properties);
279
newWrapper.parentPage = sourcePage;
280
newWrapper.column = column;
281
d.addWrappedPage(newWrapper);
282
return newWrapper.object;
285
// update the page for the specified column
286
function updatePageForColumn(column) {
287
var effectiveColumn = MathUtils.clamp(column, 0, d.columns - 1);
288
var columnHolder = body.children[effectiveColumn];
289
var newWrapper = tree.top(effectiveColumn, effectiveColumn < d.columns - 1);
290
var oldWrapper = columnHolder.pageWrapper;
292
if (newWrapper != oldWrapper) {
293
columnHolder.detachCurrentPage();
294
oldWrapper.parent = null;
296
columnHolder.attachPage(newWrapper);
298
if (oldWrapper.canDestroy) {
299
oldWrapper.destroyObject();
304
// relayouts when column count changes
305
function relayout() {
306
if (body.children.length == d.columns) return;
307
if (body.children.length > d.columns) {
308
// need to remove few columns, the last ones
309
while (body.children.length > d.columns) {
310
var holder = body.children[body.children.length - 1];
311
holder.detachCurrentPage();
312
holder.parent = null;
316
var prevColumns = body.children.length;
319
for (var i = 0; i < d.columns - prevColumns; i++) {
320
pageHolderComponent.createObject(body);
326
function rearrangePages() {
327
for (var column = d.columns - 1; column >= 0; column--) {
328
var holder = body.children[column];
329
var pageWrapper = tree.top(column, column < (d.columns - 1));
333
if (!pageWrapper.parent) {
334
// this should never happen, so if it does, we have a bug!
335
console.error("Found a page which wasn't parented anywhere!", pageWrapper.object.title);
338
// detach current page from holder if differs
339
if (holder.pageWrapper != pageWrapper) {
340
holder.detachCurrentPage();
342
if (pageWrapper.parent == hiddenPages) {
343
// add the page to the column
344
holder.attachPage(pageWrapper);
345
} else if (pageWrapper.pageHolder != holder) {
346
// detach the pageWrapper from its holder
347
if (pageWrapper.pageHolder) {
348
pageWrapper.pageHolder.detachCurrentPage();
350
// then attach to this holder
351
holder.attachPage(pageWrapper);
361
fillWidth: column == d.columns
362
minimumWidth: d.defaultColumnWidth
366
// Page holder component, can have only one Page as child at a time, all stacked pages
367
// will be parented into hiddenPages
369
id: pageHolderComponent
370
// Page uses the height of the parentNode for its height, so make
371
// the holder a PageTreeNode that determines the Page height.
375
objectName: "ColumnHolder" + column
376
property PageWrapper pageWrapper
378
property alias config: subHeader.config
379
property ColumnMetrics metrics: setDefaultMetrics()
381
Layout.fillWidth: metrics.fillWidth
382
Layout.fillHeight: true
383
Layout.preferredWidth: metrics.maximumWidth > 0 ?
384
MathUtils.clamp(d.defaultColumnWidth, metrics.minimumWidth, metrics.maximumWidth) :
386
Layout.minimumWidth: metrics.minimumWidth
387
Layout.maximumWidth: metrics.maximumWidth
389
// prevent the pages from taking the app header height into account.
393
objectName: parent.objectName + "Body"
395
top: subHeader.bottom
396
bottom: parent.bottom
399
rightMargin: verticalDivider.width
401
// we need to clip because the header does not have a background
405
property alias head: subHeader
413
height: body.headerHeight
415
styleName: config ? "PageHeadStyle" : ""
416
theme.version: Ubuntu.toolkitVersion
417
objectName: "Header" + column
419
property real preferredHeight: subHeader.__styleInstance ?
420
subHeader.__styleInstance.implicitHeight :
422
onPreferredHeightChanged: {
423
body.updateHeaderHeight(preferredHeight);
426
property PageHeadConfiguration config: null
427
property Item contents: null
429
property color dividerColor: layout.__propagated.header.dividerColor
430
property color panelColor: layout.__propagated.header.panelColor
432
visible: holder.pageWrapper && holder.pageWrapper.active
434
// The multiColumn, page and showBackButton properties are used in
435
// PageHeadStyle to show/hide the back button.
436
property var multiColumn: layout
437
property var page: holder.pageWrapper ? holder.pageWrapper.object : null
438
property bool showBackButton: {
444
parentWrapper = d.tree.parent(holder.pageWrapper);
446
// Root node has no parent node.
449
var nextInColumn = d.tree.top(holder.column, holder.column < d.columns - 1, 1);
450
return parentWrapper === nextInColumn;
458
bottom: parent.bottom
461
width: (column == (d.columns - 1)) || !pageWrapper ? 0 : units.dp(1)
462
color: subHeader.dividerColor
465
function attachPage(page) {
467
pageWrapper.parent = holderBody;
468
pageWrapper.pageHolder = holder;
469
pageWrapper.active = true;
471
if (pageWrapper.object.hasOwnProperty("head")) {
472
subHeader.config = pageWrapper.object.head;
474
if (pageWrapper.column === 0) {
475
// set the application title
476
appHeaderControlPage.title = pageWrapper.object.title;
479
function detachCurrentPage() {
480
if (!pageWrapper) return undefined;
481
var wrapper = pageWrapper;
483
wrapper.active = false;
484
subHeader.config = null;
486
wrapper.parent = hiddenPages;
487
wrapper.pageHolder = null;
491
function setDefaultMetrics() {
492
var result = defaultMetrics.createObject(holder);
493
result.column = Qt.binding(function() { return holder.column + 1; });
500
// Pages declared as children will be placed directly into hiddenPages
501
default property alias data: hiddenPages.data
504
objectName: "HiddenPagePool"
506
// make sure nothing is shown eventually
510
// Holds the columns holding the pages visible. Each column has only one page
511
// as child, the invisible stacked ones are all stored in the hiddenPages
512
// component. The stack keeps the column index onto which those should be moved
513
// once they become visible.
520
property real headerHeight: 0
522
function updateHeaderHeight(newHeight) {
523
if (newHeight > body.headerHeight) {
524
body.headerHeight = newHeight;
528
for (var i = 0; i < children.length; i++) {
529
subHeight = children[i].head.preferredHeight;
530
if (subHeight > h) h = subHeight;
532
body.headerHeight = h;
537
// all children should have Layout.fillWidth false, except the last one
538
for (var i = 0; i < children.length; i++) {
539
children[i].column = i;
544
function applyMetrics() {
545
for (var i = 0; i < children.length; i++) {
546
var holder = children[i];
547
// search for the column metrics
549
for (var j = 0; j < d.columnMetrics.length; j++) {
550
if (d.columnMetrics[j].column == (i + 1)) {
551
metrics = d.columnMetrics[j];
556
metrics = holder.setDefaultMetrics();
558
holder.metrics = metrics;
559
updateHeaderHeight(0);