~gg-lp/music-app/music-app

« back to all changes in this revision

Viewing changes to common/ColumnFlow.qml

  • Committer: Andrew Hayzen
  • Date: 2014-11-10 22:20:24 UTC
  • mto: This revision was merged to the branch mainline in revision 735.
  • Revision ID: ahayzen@gmail.com-20141110222024-kjfoirzwefw4lhwk
* Rewrite the ColumnFlow.qml to use dynamic creation/deletion of cards

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
/*
2
2
 * Copyright (C) 2014
3
3
 *      Andrew Hayzen <ahayzen@gmail.com>
4
 
 *      Michael Spencer <sonrisesoftware@gmail.com>
5
4
 *
6
5
 * This program is free software; you can redistribute it and/or modify
7
6
 * it under the terms of the GNU General Public License as published by
14
13
 *
15
14
 * You should have received a copy of the GNU General Public License
16
15
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 
 *
18
 
 * Upstream location:
19
 
 * https://github.com/iBeliever/ubuntu-ui-extras/blob/master/ColumnFlow.qml
20
16
 */
21
17
 
22
18
import QtQuick 2.3
23
19
 
24
 
 
25
20
Item {
26
21
    id: columnFlow
27
22
    property int columns: 1
28
 
    property bool repeaterCompleted: false
29
 
    property alias count: repeater.count
30
 
    property alias model: repeater.model
31
 
    property alias delegate: repeater.delegate
 
23
    property Flickable flickable
 
24
    property var model
 
25
    property Component delegate
 
26
 
 
27
    property var getter: function (i) { return model.get(i); }  // optional getter override (useful for music-app ms2 models)
 
28
 
 
29
    property int buffer: units.gu(20)
 
30
    property var columnHeights: []
 
31
    property var columnHeightsMax: []
 
32
    property int columnWidth: parent.width / columns
32
33
    property int contentHeight: 0
33
 
 
34
 
    onColumnsChanged: reEvalColumns()
35
 
    onModelChanged: reEvalColumns()
36
 
    onWidthChanged: updateWidths()
37
 
 
38
 
    function updateWidths() {
39
 
        if (repeaterCompleted) {
40
 
            var count = 0
41
 
 
42
 
            //add the first <column> elements
43
 
            for (var i = 0; count < columns && i < columnFlow.children.length; i++) {
44
 
                //print(i, count)
45
 
                if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0)
46
 
                        //|| !columnFlow.children[i].visible)  // CUSTOM - view is invisible at start
47
 
                    continue
48
 
 
49
 
                columnFlow.children[i].width = width / columns
50
 
 
51
 
                count++
52
 
            }
53
 
        }
54
 
    }
55
 
 
56
 
    function reEvalColumns() {
57
 
        if (columnFlow.repeaterCompleted === false)
58
 
            return
59
 
 
60
 
        if (columns === 0) {
61
 
            contentHeight = 0
62
 
            return
63
 
        }
64
 
 
65
 
        var i, j
66
 
        var columnHeights = new Array(columns);
67
 
        var lastItem = new Array(columns)
68
 
        var lastI = -1
69
 
        var count = 0
70
 
 
71
 
        //add the first <column> elements
72
 
        for (i = 0; count < columns && i < columnFlow.children.length; i++) {
73
 
            // CUSTOM - ignore if has just been removed
74
 
            if (i === repeater.removeHintIndex && columnFlow.children[i] === repeater.removeHintItem) {
75
 
                continue
76
 
            }
77
 
 
78
 
            if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0)
79
 
                    //|| !columnFlow.children[i].visible)  // CUSTOM - view is invisible at start
80
 
                continue
81
 
 
82
 
            lastItem[count] = i
83
 
 
84
 
            columnHeights[count] = columnFlow.children[i].height
85
 
            columnFlow.children[i].anchors.top = columnFlow.top
86
 
            columnFlow.children[i].anchors.left = (lastI === -1 ? columnFlow.left : columnFlow.children[lastI].right)
87
 
            columnFlow.children[i].anchors.right = undefined
88
 
            columnFlow.children[i].width = columnFlow.width / columns
89
 
 
90
 
            lastI = i
91
 
            count++
92
 
        }
93
 
 
94
 
        //add the other elements
95
 
        for (i = i; i < columnFlow.children.length; i++) {
96
 
            var highestHeight = Number.MAX_VALUE
97
 
            var newColumn = 0
98
 
 
99
 
            // CUSTOM - ignore if has just been removed
100
 
            if (i === repeater.removeHintIndex && columnFlow.children[i] === repeater.removeHintItem) {
101
 
                continue
102
 
            }
103
 
 
104
 
            if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0)
105
 
                    //|| !columnFlow.children[i].visible)  // CUSTOM - view is invisible at start
106
 
                continue
107
 
 
108
 
            // find the shortest column
109
 
            for (j = 0; j < columns; j++) {
110
 
                if (columnHeights[j] !== null && columnHeights[j] < highestHeight) {
111
 
                    newColumn = j
112
 
                    highestHeight = columnHeights[j]
113
 
                }
114
 
            }
115
 
 
116
 
            // add the element to the shortest column
117
 
            columnFlow.children[i].anchors.top = columnFlow.children[lastItem[newColumn]].bottom
118
 
            columnFlow.children[i].anchors.left = columnFlow.children[lastItem[newColumn]].left
119
 
            columnFlow.children[i].anchors.right = columnFlow.children[lastItem[newColumn]].right
120
 
 
121
 
            lastItem[newColumn] = i
122
 
            columnHeights[newColumn] += columnFlow.children[i].height
123
 
        }
124
 
 
125
 
        var cHeight = 0
126
 
 
127
 
        for (i = 0; i < columnHeights.length; i++) {
128
 
            if (columnHeights[i])
129
 
                cHeight = Math.max(cHeight, columnHeights[i])
130
 
        }
131
 
 
132
 
        contentHeight = cHeight
133
 
        updateWidths()
134
 
    }
135
 
 
136
 
    Repeater {
137
 
        id: repeater
138
 
        model: columnFlow.model
139
 
        Component.onCompleted: {
140
 
            columnFlow.repeaterCompleted = true
141
 
            columnFlow.reEvalColumns()
142
 
        }
143
 
 
144
 
        // Provide a hint of the removed item
145
 
        property int removeHintIndex: -1  // CUSTOM
146
 
        property var removeHintItem  // CUSTOM
147
 
 
148
 
        onItemAdded: columnFlow.reEvalColumns()  // CUSTOM - ms2 models are live
149
 
        onItemRemoved: {
150
 
            removeHintIndex = index
151
 
            removeHintItem = item
152
 
 
153
 
            columnFlow.reEvalColumns()  // CUSTOM - ms2 models are live
154
 
 
155
 
            // Set back to null to allow freeing of memory
156
 
            removeHintIndex = -1
157
 
            removeHintItem = undefined
 
34
    property int count: model === undefined ? 0 : model.count
 
35
    property var incubating: ({})  // incubating objects
 
36
    property var items: ({})
 
37
    property var itemToColumn: ({})  // cache of the columns of indexes
 
38
    property int lastIndex: 0  // the furtherest index loaded
 
39
 
 
40
    onColumnWidthChanged: {
 
41
        if (columns != columnHeights.length) {  // number of columns has changed so reset
 
42
            reset()
 
43
            append()
 
44
        } else {  // column width has changed update visible items properties linked to columnWidth
 
45
            for (var column=0; column < columnHeights.length; column++) {
 
46
                for (var i in columnHeights[column]) {
 
47
                    if (columnHeights[column].hasOwnProperty(i) && items.hasOwnProperty(i)) {
 
48
                        items[i].width = columnWidth;
 
49
                        items[i].x = column * columnWidth;
 
50
                    }
 
51
                }
 
52
            }
 
53
        }
 
54
    }
 
55
 
 
56
    onCountChanged: {
 
57
        if (count === 0) {  // likely the model is been reset so reset the view
 
58
            reset()
 
59
        } else {  // likely new items in the model check if any can be shown
 
60
            append()
 
61
        }
 
62
    }
 
63
 
 
64
    // Append a new row of items if possible
 
65
    function append()
 
66
    {
 
67
        // Do not allow append to run if incubating
 
68
        if (isIncubating() === true) {
 
69
            return;
 
70
        }
 
71
 
 
72
        // get the columns in order
 
73
        var columnsByHeight = getColumnsByHeight();
 
74
 
 
75
        // check if a new item in each column is possible
 
76
        for (var i=0; i < columnsByHeight.length; i++) {
 
77
            var y = columnHeightsMax[columnsByHeight[i]];
 
78
 
 
79
            // build new object in column if possible
 
80
            if (count > 0 && lastIndex < count && inViewport(y, 0)) {
 
81
                incubateObject(lastIndex++, columnsByHeight[i], getMaxVisibleInColumn(columnsByHeight[i]), append);
 
82
            } else {
 
83
                break;
 
84
            }
 
85
        }
 
86
    }
 
87
 
 
88
    // Cache the size of the columns for use later
 
89
    function cacheColumnHeights()
 
90
    {
 
91
        columnHeightsMax = [];
 
92
 
 
93
        for (var i=0; i < columnHeights.length; i++) {
 
94
            var sum = 0;
 
95
 
 
96
            for (var j in columnHeights[i]) {
 
97
                sum += columnHeights[i][j];
 
98
            }
 
99
 
 
100
            columnHeightsMax.push(sum);
 
101
        }
 
102
 
 
103
        // set the height of columnFlow to max column (for flickable contentHeight)
 
104
        contentHeight = Math.max.apply(null, columnHeightsMax);
 
105
    }
 
106
 
 
107
    // Recache the visible items heights (due to a change in their height)
 
108
    function cacheVisibleItemsHeights()
 
109
    {
 
110
        for (var i in items) {
 
111
            if (items.hasOwnProperty(i)) {
 
112
                columnHeights[itemToColumn[i]][i] = items[i].height;
 
113
            }
 
114
        }
 
115
 
 
116
        cacheColumnHeights();
 
117
    }
 
118
 
 
119
    // Return if there are incubating objects
 
120
    function isIncubating()
 
121
    {
 
122
        for (var i in incubating) {
 
123
            if (incubating.hasOwnProperty(i)) {
 
124
                return true;
 
125
            }
 
126
        }
 
127
 
 
128
        return false;
 
129
    }
 
130
 
 
131
    // Run after incubation to store new column height and call any further append/restores
 
132
    function finishIncubation(index, callback)
 
133
    {
 
134
        var obj = incubating[index].object;
 
135
        delete incubating[index];
 
136
 
 
137
        obj.heightChanged.connect(cacheVisibleItemsHeights)  // if the height changes recache
 
138
 
 
139
        // Ensure properties linked to columnWidth are correct (as width may still be changing)
 
140
        obj.x = itemToColumn[index] * columnWidth;
 
141
        obj.width = columnWidth;
 
142
 
 
143
        items[index] = obj;
 
144
 
 
145
        columnHeights[itemToColumn[index]][index] = obj.height;  // ensure height is the latest
 
146
 
 
147
        if (isIncubating() === false) {
 
148
            cacheColumnHeights();
 
149
 
 
150
            // Check if there is any more work to be done (append or restore)
 
151
            callback();
 
152
        }
 
153
    }
 
154
 
 
155
    // Get the column index in order of height
 
156
    function getColumnsByHeight()
 
157
    {
 
158
        var columnsByHeight = [];
 
159
 
 
160
        for (var i=0; i < columnHeightsMax.length; i++) {
 
161
            var min = undefined;
 
162
            var index = -1;
 
163
 
 
164
            // Find the smallest column that has not been found yet
 
165
            for (var j=0; j < columnHeightsMax.length; j++) {
 
166
                if (columnsByHeight.indexOf(j) === -1 && (min === undefined || columnHeightsMax[j] < min)) {
 
167
                    min = columnHeightsMax[j];
 
168
                    index = j;
 
169
                }
 
170
            }
 
171
 
 
172
            columnsByHeight.push(index);
 
173
        }
 
174
 
 
175
        return columnsByHeight;
 
176
    }
 
177
 
 
178
    // Get the min value in a column after the limit
 
179
    function getMinIndexInColumnAfter(column, limit)
 
180
    {
 
181
        for (var i=limit + 1; i <= lastIndex; i++) {
 
182
            if (columnHeights[column].hasOwnProperty(i)) {
 
183
                return i;
 
184
            }
 
185
        }
 
186
    }
 
187
 
 
188
    // Get the lowest visible index for a column
 
189
    function getMinVisibleInColumn(column)
 
190
    {
 
191
        var min;
 
192
 
 
193
        for (var i in columnHeights[column]) {
 
194
            if (columnHeights[column].hasOwnProperty(i)) {
 
195
                i = parseInt(i);
 
196
 
 
197
                if (items.hasOwnProperty(i)) {
 
198
                    if (i < min || min === undefined) {
 
199
                        min = i;
 
200
                    }
 
201
                }
 
202
            }
 
203
        }
 
204
 
 
205
        return min;
 
206
    }
 
207
 
 
208
    // Get the max value in a column before the limit
 
209
    function getMaxIndexInColumnBefore(column, limit)
 
210
    {
 
211
        for (var i=--limit; i >= 0; i--) {
 
212
            if (columnHeights[column].hasOwnProperty(i)) {
 
213
                return i;
 
214
            }
 
215
        }
 
216
    }
 
217
 
 
218
    // Get the highest visible index for a column
 
219
    function getMaxVisibleInColumn(column)
 
220
    {
 
221
        var max;
 
222
 
 
223
        for (var i in columnHeights[column]) {
 
224
            if (columnHeights[column].hasOwnProperty(i)) {
 
225
                i = parseInt(i);
 
226
 
 
227
                if (items.hasOwnProperty(i)) {
 
228
                    if (i > max || max === undefined) {
 
229
                        max = i;
 
230
                    }
 
231
                }
 
232
            }
 
233
        }
 
234
 
 
235
        return max;
 
236
    }
 
237
 
 
238
    // Incubate an object for creation
 
239
    function incubateObject(index, column, anchorIndex, callback)
 
240
    {
 
241
        // Load parameters to send to the object on creation
 
242
        var params = {
 
243
            index: index,
 
244
            model: getter(index),
 
245
            width: columnWidth,
 
246
            x: column * columnWidth
 
247
        };
 
248
 
 
249
        if (anchorIndex === undefined) {
 
250
            params["anchors.top"] = parent.top;
 
251
        } else if (anchorIndex < 0) {
 
252
            params["anchors.bottom"] = items[-(anchorIndex + 1)].top;
 
253
        } else {
 
254
            params["anchors.top"] = items[anchorIndex].bottom;
 
255
        }
 
256
 
 
257
        // Start incubating and cache the column
 
258
        incubating[index] = delegate.incubateObject(parent, params);
 
259
        itemToColumn[index] = column;
 
260
 
 
261
        if (incubating[index].status != Component.Ready) {
 
262
            incubating[index].onStatusChanged = function(status) {
 
263
                if (status == Component.Ready) {
 
264
                    finishIncubation(index, callback)
 
265
                }
 
266
            }
 
267
        } else {
 
268
            finishIncubation(index, callback)
 
269
        }
 
270
    }
 
271
 
 
272
    // Detect if a loaded object is in the viewport with double buffer before and single after
 
273
    function inViewport(y, height)
 
274
    {
 
275
        return flickable.contentY - buffer - buffer < y + height && y < flickable.contentY + flickable.height + buffer;
 
276
    }
 
277
 
 
278
    // Reset the column flow
 
279
    function reset()
 
280
    {
 
281
        // Force and incubation to finish
 
282
        for (var i in incubating) {
 
283
            if (incubating.hasOwnProperty(i)) {
 
284
                incubating[i].forceCompletion()
 
285
            }
 
286
        }
 
287
 
 
288
        // Destroy any old items
 
289
        for (var j in items) {
 
290
            if (items.hasOwnProperty(j)) {
 
291
                items[j].destroy()
 
292
            }
 
293
        }
 
294
 
 
295
        // Reset and rebuild the variables
 
296
        items = ({})
 
297
        lastIndex = 0
 
298
 
 
299
        columnHeights = []
 
300
 
 
301
        for (var k=0; k < columns; k++) {
 
302
            columnHeights.push({})
 
303
        }
 
304
 
 
305
        cacheColumnHeights()
 
306
 
 
307
        contentHeight = 0
 
308
    }
 
309
 
 
310
    // Restore any objects that are now in the viewport
 
311
    function restore()
 
312
    {
 
313
        // Do not allow restore to run if incubating
 
314
        if (isIncubating() === true) {
 
315
            return;
 
316
        }
 
317
 
 
318
        for (var column in columnHeights) {
 
319
            var index;
 
320
 
 
321
            // Rebuild anything before the lowest visible index
 
322
            var minVisible = getMinVisibleInColumn(column);
 
323
 
 
324
            if (minVisible !== undefined) {
 
325
                // get the next lowest index for this column to add before
 
326
                index = getMaxIndexInColumnBefore(column, minVisible)
 
327
 
 
328
                if (index !== undefined) {
 
329
                    // Check that the object will be in the viewport
 
330
                    if (inViewport(items[minVisible].y - columnHeights[column][index], columnHeights[column][index])) {
 
331
                        incubateObject(index, column, (-minVisible) - 1, restore)  // add the new object
 
332
                    }
 
333
                }
 
334
            }
 
335
 
 
336
            // Rebuild anything after the highest visible index
 
337
            var maxVisible = getMaxVisibleInColumn(column);
 
338
 
 
339
            if (maxVisible !== undefined) {
 
340
                // get the next highest index for this column to add after
 
341
                index = getMinIndexInColumnAfter(column, maxVisible);
 
342
 
 
343
                if (index !== undefined) {
 
344
                    // Check that the object will be in the viewport
 
345
                    if (inViewport(items[maxVisible].y + columnHeights[column][maxVisible], columnHeights[column][index])) {
 
346
                        incubateObject(index, column, maxVisible, restore)  // add the new object
 
347
                    }
 
348
                }
 
349
            }
 
350
        }
 
351
    }
 
352
 
 
353
    Connections {
 
354
        target: flickable
 
355
        onContentYChanged: {
 
356
            restore()  // Restore old items (scrolling up/down)
 
357
 
 
358
            append()  // Append any new items (scrolling down)
 
359
 
 
360
            // skip if at the start of end of the flickable (prevents overscroll issue)
 
361
            if (!flickable.atYBeginning && !flickable.atYEnd) {
 
362
                // Destroy any old items
 
363
                for (var i in items) {
 
364
                    if (items.hasOwnProperty(i)) {
 
365
                        if (!inViewport(items[i].y, items[i].height)) {
 
366
                            // Ensure height is at its latest value
 
367
                            columnHeights[itemToColumn[i]][items[i].index] = items[i].height;
 
368
 
 
369
                            // Destroy the object
 
370
                            items[i].destroy()
 
371
                            delete items[i];
 
372
                        }
 
373
                    }
 
374
                }
 
375
            }
158
376
        }
159
377
    }
160
378
}