~loic.molinari/ubuntu-ui-toolkit/ubuntu-ui-toolkit-ubuntu-shape-icon

« back to all changes in this revision

Viewing changes to src/Ubuntu/Components/1.3/AdaptivePageLayout.qml

  • Committer: Loïc Molinari
  • Date: 2015-08-06 13:16:24 UTC
  • mfrom: (1568.1.24 staging)
  • Revision ID: loic.molinari@canonical.com-20150806131624-vbtvrvpu5z1bapd8
Merged lp:ubuntu-ui-toolkit/staging.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright 2015 Canonical Ltd.
 
3
 *
 
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.
 
7
 *
 
8
 * This program is distributed in the hope that it will be useful,
 
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
 * GNU Lesser General Public License for more details.
 
12
 *
 
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/>.
 
15
 */
 
16
 
 
17
import QtQuick 2.4
 
18
import QtQuick.Layouts 1.1
 
19
import Ubuntu.Components 1.3
 
20
import "tree.js" as Tree
 
21
 
 
22
/*!
 
23
  \qmltype AdaptivePageLayout
 
24
  \inqmlmodule Ubuntu.Components 1.3
 
25
  \since Ubuntu.Components 1.3
 
26
  \ingroup ubuntu
 
27
  \brief View with multiple columns of Pages.
 
28
 
 
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.
 
32
 
 
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.
 
37
 
 
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.
 
43
 
 
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.
 
48
 
 
49
  \qml
 
50
    import QtQuick 2.4
 
51
    import Ubuntu.Components 1.3
 
52
 
 
53
    MainView {
 
54
        width: units.gu(100)
 
55
        height: units.gu(60)
 
56
 
 
57
        AdaptivePageLayout {
 
58
            anchors.fill: parent
 
59
            primaryPage: page1
 
60
            Page {
 
61
                id: page1
 
62
                title: "Main page"
 
63
                Column {
 
64
                    Button {
 
65
                        text: "Add Page2 above " + page1.title
 
66
                        onClicked: page1.pageStack.addPageToCurrentColumn(page1, page2)
 
67
                    }
 
68
                    Button {
 
69
                        text: "Add Page3 next to " + page1.title
 
70
                        onClicked: page1.pageStack.addPageToNextColumn(page1, page3)
 
71
                    }
 
72
                }
 
73
            }
 
74
            Page {
 
75
                id: page2
 
76
                title: "Page #2"
 
77
            }
 
78
            Page {
 
79
                id: page3
 
80
                title: "Page #3"
 
81
            }
 
82
        }
 
83
    }
 
84
  \endqml
 
85
 
 
86
  AdaptivePageLayout supports adaptive column handling. When the number of columns changes at
 
87
  runtime the pages are automatically rearranged.
 
88
 
 
89
  \sa PageStack
 
90
*/
 
91
 
 
92
PageTreeNode {
 
93
    id: layout
 
94
 
 
95
    Page {
 
96
        // AdaptivePageLayout has its own split headers, so
 
97
        //  disable the application header.
 
98
        id: appHeaderControlPage
 
99
        head {
 
100
            locked: true
 
101
            visible: false
 
102
        }
 
103
        // title is set in attachPage() when the attached Page.column === 0
 
104
    }
 
105
 
 
106
    /*!
 
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.
 
112
      */
 
113
    property Page primaryPage
 
114
 
 
115
    /*!
 
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.
 
122
      */
 
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);
 
131
        }
 
132
        return d.addPageToColumn(nextColumn - 1, sourcePage, page, properties);
 
133
    }
 
134
 
 
135
    /*!
 
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.
 
141
      */
 
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);
 
147
        }
 
148
        return d.addPageToColumn(nextColumn, sourcePage, page, properties);
 
149
    }
 
150
 
 
151
    /*!
 
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.
 
156
      */
 
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);
 
163
        }
 
164
    }
 
165
 
 
166
    /*
 
167
      internals
 
168
      */
 
169
 
 
170
    Component.onCompleted: {
 
171
        d.relayout();
 
172
        d.completed = true;
 
173
        if (primaryPage) {
 
174
            var wrapper = d.createWrapper(primaryPage);
 
175
            d.addWrappedPage(wrapper);
 
176
        } else {
 
177
            console.warn("No primary page set. No pages can be added without a primary page.");
 
178
        }
 
179
    }
 
180
    onPrimaryPageChanged: {
 
181
        if (d.completed) {
 
182
            console.warn("Cannot change primaryPage after completion.");
 
183
            return;
 
184
        }
 
185
    }
 
186
 
 
187
    QtObject {
 
188
        id: d
 
189
 
 
190
        property bool completed: false
 
191
        property var tree: new Tree.Tree()
 
192
 
 
193
        property int columns: layout.width >= units.gu(80) ? 2 : 1
 
194
        /*! internal */
 
195
        onColumnsChanged: {
 
196
            if (columns <= 0) {
 
197
                console.warn("There must be a minimum of one column set.");
 
198
                columns = 1;
 
199
            }
 
200
            d.relayout();
 
201
        }
 
202
        property real defaultColumnWidth: units.gu(40)
 
203
        onDefaultColumnWidthChanged: body.applyMetrics()
 
204
        property list<ColumnMetrics> columnMetrics
 
205
 
 
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;
 
215
        }
 
216
 
 
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)
 
225
        }
 
226
 
 
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) {
 
232
                        return w;
 
233
                    } else {
 
234
                        print("Page is not wrapped by its parentNode. This should not happen!");
 
235
                        return null;
 
236
                    }
 
237
                } else {
 
238
                    // invalid wrapper
 
239
                    return null;
 
240
                }
 
241
            } else {
 
242
                // invalid page
 
243
                return null;
 
244
            }
 
245
        }
 
246
 
 
247
        function columnForPage(page) {
 
248
            var wrapper = d.getWrapper(page);
 
249
            return wrapper ? wrapper.column : 0;
 
250
        }
 
251
 
 
252
        function addPageToColumn(column, sourcePage, page, properties) {
 
253
            if (column < 0) {
 
254
                console.warn("Column must be >= 0.");
 
255
                return;
 
256
            }
 
257
            if (!sourcePage) {
 
258
                console.warn("No sourcePage specified. Page will not be added.");
 
259
                return;
 
260
            }
 
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.");
 
264
                return;
 
265
            }
 
266
 
 
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.
 
270
 
 
271
                var oldWrapper = getWrapper(page);
 
272
                if (oldWrapper && d.tree.index(oldWrapper) !== -1) {
 
273
                    console.warn("Cannot add a Page that was already added.");
 
274
                    return null;
 
275
                }
 
276
            }
 
277
 
 
278
            var newWrapper = d.createWrapper(page, properties);
 
279
            newWrapper.parentPage = sourcePage;
 
280
            newWrapper.column = column;
 
281
            d.addWrappedPage(newWrapper);
 
282
            return newWrapper.object;
 
283
        }
 
284
 
 
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;
 
291
 
 
292
            if (newWrapper != oldWrapper) {
 
293
                columnHolder.detachCurrentPage();
 
294
                oldWrapper.parent = null;
 
295
                if (newWrapper) {
 
296
                    columnHolder.attachPage(newWrapper);
 
297
                }
 
298
                if (oldWrapper.canDestroy) {
 
299
                    oldWrapper.destroyObject();
 
300
                }
 
301
            }
 
302
        }
 
303
 
 
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;
 
313
                    holder.destroy();
 
314
                }
 
315
            } else {
 
316
                var prevColumns = body.children.length;
 
317
 
 
318
                // add columns
 
319
                for (var i = 0; i < d.columns - prevColumns; i++) {
 
320
                    pageHolderComponent.createObject(body);
 
321
                }
 
322
            }
 
323
            rearrangePages();
 
324
        }
 
325
 
 
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));
 
330
                if (!pageWrapper) {
 
331
                    continue;
 
332
                }
 
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);
 
336
                    continue;
 
337
                }
 
338
                // detach current page from holder if differs
 
339
                if (holder.pageWrapper != pageWrapper) {
 
340
                    holder.detachCurrentPage();
 
341
                }
 
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();
 
349
                    }
 
350
                    // then attach to this holder
 
351
                    holder.attachPage(pageWrapper);
 
352
                }
 
353
            }
 
354
        }
 
355
    }
 
356
 
 
357
    // default metrics
 
358
    Component {
 
359
        id: defaultMetrics
 
360
        ColumnMetrics {
 
361
            fillWidth: column == d.columns
 
362
            minimumWidth: d.defaultColumnWidth
 
363
        }
 
364
    }
 
365
 
 
366
    // Page holder component, can have only one Page as child at a time, all stacked pages
 
367
    // will be parented into hiddenPages
 
368
    Component {
 
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.
 
372
        PageTreeNode {
 
373
            id: holder
 
374
            active: false
 
375
            objectName: "ColumnHolder" + column
 
376
            property PageWrapper pageWrapper
 
377
            property int column
 
378
            property alias config: subHeader.config
 
379
            property ColumnMetrics metrics: setDefaultMetrics()
 
380
 
 
381
            Layout.fillWidth: metrics.fillWidth
 
382
            Layout.fillHeight: true
 
383
            Layout.preferredWidth: metrics.maximumWidth > 0 ?
 
384
                                       MathUtils.clamp(d.defaultColumnWidth, metrics.minimumWidth, metrics.maximumWidth) :
 
385
                                       d.defaultColumnWidth
 
386
            Layout.minimumWidth: metrics.minimumWidth
 
387
            Layout.maximumWidth: metrics.maximumWidth
 
388
 
 
389
            // prevent the pages from taking the app header height into account.
 
390
            __propagated: null
 
391
            Item {
 
392
                id: holderBody
 
393
                objectName: parent.objectName + "Body"
 
394
                anchors {
 
395
                    top: subHeader.bottom
 
396
                    bottom: parent.bottom
 
397
                    left: parent.left
 
398
                    right: parent.right
 
399
                    rightMargin: verticalDivider.width
 
400
                }
 
401
                // we need to clip because the header does not have a background
 
402
                clip: true
 
403
            }
 
404
 
 
405
            property alias head: subHeader
 
406
            StyledItem {
 
407
                id: subHeader
 
408
                anchors {
 
409
                    left: parent.left
 
410
                    top: parent.top
 
411
                    right: parent.right
 
412
                }
 
413
                height: body.headerHeight
 
414
 
 
415
                styleName: config ? "PageHeadStyle" : ""
 
416
                theme.version: Ubuntu.toolkitVersion
 
417
                objectName: "Header" + column
 
418
 
 
419
                property real preferredHeight: subHeader.__styleInstance ?
 
420
                                                   subHeader.__styleInstance.implicitHeight :
 
421
                                                   0
 
422
                onPreferredHeightChanged: {
 
423
                    body.updateHeaderHeight(preferredHeight);
 
424
                }
 
425
 
 
426
                property PageHeadConfiguration config: null
 
427
                property Item contents: null
 
428
 
 
429
                property color dividerColor: layout.__propagated.header.dividerColor
 
430
                property color panelColor: layout.__propagated.header.panelColor
 
431
 
 
432
                visible: holder.pageWrapper && holder.pageWrapper.active
 
433
 
 
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: {
 
439
                    if (!page) {
 
440
                        return false;
 
441
                    }
 
442
                    var parentWrapper;
 
443
                    try {
 
444
                        parentWrapper = d.tree.parent(holder.pageWrapper);
 
445
                    } catch(err) {
 
446
                        // Root node has no parent node.
 
447
                        return false;
 
448
                    }
 
449
                    var nextInColumn = d.tree.top(holder.column, holder.column < d.columns - 1, 1);
 
450
                    return parentWrapper === nextInColumn;
 
451
                }
 
452
            }
 
453
 
 
454
            Rectangle {
 
455
                id: verticalDivider
 
456
                anchors {
 
457
                    top: parent.top
 
458
                    bottom: parent.bottom
 
459
                    right: parent.right
 
460
                }
 
461
                width: (column == (d.columns - 1)) || !pageWrapper ? 0 : units.dp(1)
 
462
                color: subHeader.dividerColor
 
463
            }
 
464
 
 
465
            function attachPage(page) {
 
466
                pageWrapper = page;
 
467
                pageWrapper.parent = holderBody;
 
468
                pageWrapper.pageHolder = holder;
 
469
                pageWrapper.active = true;
 
470
 
 
471
                if (pageWrapper.object.hasOwnProperty("head")) {
 
472
                    subHeader.config = pageWrapper.object.head;
 
473
                }
 
474
                if (pageWrapper.column === 0) {
 
475
                    // set the application title
 
476
                    appHeaderControlPage.title = pageWrapper.object.title;
 
477
                }
 
478
            }
 
479
            function detachCurrentPage() {
 
480
                if (!pageWrapper) return undefined;
 
481
                var wrapper = pageWrapper;
 
482
                // remove header
 
483
                wrapper.active = false;
 
484
                subHeader.config = null;
 
485
                pageWrapper = null;
 
486
                wrapper.parent = hiddenPages;
 
487
                wrapper.pageHolder = null;
 
488
                return wrapper;
 
489
            }
 
490
 
 
491
            function setDefaultMetrics() {
 
492
                var result = defaultMetrics.createObject(holder);
 
493
                result.column = Qt.binding(function() { return holder.column + 1; });
 
494
                return result;
 
495
            }
 
496
        }
 
497
    }
 
498
 
 
499
    /*! \internal */
 
500
    // Pages declared as children will be placed directly into hiddenPages
 
501
    default property alias data: hiddenPages.data
 
502
    Item {
 
503
        id: hiddenPages
 
504
        objectName: "HiddenPagePool"
 
505
        visible: false
 
506
        // make sure nothing is shown eventually
 
507
        clip: true
 
508
    }
 
509
 
 
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.
 
514
    RowLayout {
 
515
        id: body
 
516
        objectName: "body"
 
517
        anchors.fill: parent
 
518
        spacing: 0
 
519
 
 
520
        property real headerHeight: 0
 
521
 
 
522
        function updateHeaderHeight(newHeight) {
 
523
            if (newHeight > body.headerHeight) {
 
524
                body.headerHeight = newHeight;
 
525
            } else {
 
526
                var h = 0;
 
527
                var subHeight = 0;
 
528
                for (var i = 0; i < children.length; i++) {
 
529
                    subHeight = children[i].head.preferredHeight;
 
530
                    if (subHeight > h) h = subHeight;
 
531
                }
 
532
                body.headerHeight = h;
 
533
            }
 
534
        }
 
535
 
 
536
        onChildrenChanged: {
 
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;
 
540
            }
 
541
            applyMetrics();
 
542
        }
 
543
 
 
544
        function applyMetrics() {
 
545
            for (var i = 0; i < children.length; i++) {
 
546
                var holder = children[i];
 
547
                // search for the column metrics
 
548
                var metrics = null;
 
549
                for (var j = 0; j < d.columnMetrics.length; j++) {
 
550
                    if (d.columnMetrics[j].column == (i + 1)) {
 
551
                        metrics = d.columnMetrics[j];
 
552
                        break;
 
553
                    }
 
554
                }
 
555
                if (!metrics) {
 
556
                    metrics = holder.setDefaultMetrics();
 
557
                }
 
558
                holder.metrics = metrics;
 
559
                updateHeaderHeight(0);
 
560
            }
 
561
        }
 
562
    }
 
563
}