@@ -1,494 +0,0 @@
1−/*
2− * Copyright (C) 2014-2016
3− * Andrew Hayzen <ahayzen@gmail.com>
4− *
5− * This program is free software; you can redistribute it and/or modify
6− * it under the terms of the GNU General Public License as published by
7− * the Free Software Foundation; version 3.
8− *
9− * This program is distributed in the hope that it will be useful,
10− * but WITHOUT ANY WARRANTY; without even the implied warranty of
11− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12− * GNU General Public License for more details.
13− *
14− * You should have received a copy of the GNU General Public License
15− * along with this program. If not, see <http://www.gnu.org/licenses/>.
16− */
17−
18−import QtQuick 2.4
19−
20−Item {
21− id: columnFlow
22− property int columns: 1
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
33− property int contentHeight: 0
34− property int count: model === undefined ? 0 : model.count
35− property int delayRebuildIndex: -1
36− property var incubating: ({}) // incubating objects
37− property var items: ({})
38− property var itemToColumn: ({}) // cache of the columns of indexes
39− property int lastIndex: 0 // the furtherest index loaded
40− property bool removing: false
41− property bool restoring: false // is the view restoring?
42− property var restoreItems: ({}) // when rebuilding items are stored here temporarily
43−
44− onColumnWidthChanged: {
45− if (restoring) {
46− return;
47− } else if (columns != columnHeights.length && visible) {
48− // number of columns has changed so rebuild the columns
49− rebuildColumns()
50− } else { // column width has changed update visible items properties linked to columnWidth
51− for (var column=0; column < columnHeights.length; column++) {
52− for (var i in columnHeights[column]) {
53− if (columnHeights[column].hasOwnProperty(i) && items.hasOwnProperty(i)) {
54− items[i].width = columnWidth;
55− items[i].x = column * columnWidth;
56− }
57− }
58− }
59−
60− ensureItemsVisible()
61− }
62− }
63−
64− onVisibleChanged: {
65− if (visible && delayRebuildIndex !== -1) { // restore from count change
66− if (delayRebuildIndex === 0) {
67− reset()
68− } else {
69− removeIndex(delayRebuildIndex)
70− }
71−
72− delayRebuildIndex = -1
73− append(true)
74− }
75−
76− // number of columns has changed while invisible so reset if not already restoring
77− if (visible && !restoring && columns != columnHeights.length) {
78− rebuildColumns()
79− }
80− }
81−
82− ListModel { // fakemodel for connections to link to when there is no model
83− id: fakeModel
84− }
85−
86− Connections {
87− target: model === undefined ? fakeModel : model
88− onModelReset: {
89− if (!visible && lastIndex > 0) {
90− delayRebuildIndex = 0
91− } else {
92− reset()
93− append()
94− }
95− }
96− onRowsInserted: {
97− if (!visible && lastIndex > 0) {
98− setDelayRebuildIndex(first)
99− } else {
100− if (first <= lastIndex) {
101− if (first === 0) {
102− reset()
103− } else {
104− removeIndex(first) // remove earliest index and all items after
105− }
106− }
107−
108− // Supply last index if larger as count is not updated until after insertion
109− append(true, last > count ? last : count)
110− }
111− }
112− onRowsRemoved: {
113− if (!visible) {
114− setDelayRebuildIndex(first)
115− } else {
116− if (first <= lastIndex) {
117− if (first === 0) {
118− reset()
119− } else {
120− removeIndex(first) // remove earliest index and all items after
121− }
122−
123− // count is not updated until after removal, so send insertMax
124− // insertMax is count - removal region inclusive - 1 (lastIndex is 1 infront)
125−
126− append(true, count - (1 + last - first) - 1) // rebuild any items on screen or before
127− }
128− }
129− }
130− }
131−
132−
133− Connections {
134− target: flickable
135− onContentYChanged: {
136− append() // Append any new items (scrolling down)
137−
138− ensureItemsVisible()
139− }
140− }
141−
142− // Append a new row of items if possible
143− function append(loadBefore, insertMax)
144− {
145− // Do not allow append to run if incubating
146− if (isIncubating() || restoring || removing) {
147− return;
148− }
149−
150− // get the columns in order
151− var columnsByHeight = getColumnsByHeight();
152− var workDone = false;
153−
154− // check if a new item in each column is possible
155− for (var i=0; i < columnsByHeight.length; i++) {
156− var y = columnHeightsMax[columnsByHeight[i]];
157−
158− // build new object in column if possible
159− // if insertMax is undefined then allow if there is work todo (from the count in the model)
160− // otherwise use the insertMax as the count to compare with the lastIndex added to the columnFlow
161− // and
162− // allow if the y position is within the viewport
163− // or if loadBefore is true then allow if the y position is before the viewport
164− if (((count > 0 && lastIndex < count && insertMax === undefined) || (insertMax !== undefined && lastIndex <= insertMax)) && (inViewport(y, 0) || (loadBefore === true && beforeViewport(y)))) {
165− incubateObject(lastIndex++, columnsByHeight[i], getMaxInColumn(columnsByHeight[i]), append);
166− workDone = true
167− } else {
168− break;
169− }
170− }
171−
172− if (!workDone) { // last iteration over append so visible ensure items are correct
173− ensureItemsVisible();
174− }
175− }
176−
177− // Detect if a loaded object is before the viewport with a buffer
178− function beforeViewport(y)
179− {
180− return y <= flickable.contentY - buffer;
181− }
182−
183− // Cache the size of the columns for use later
184− function cacheColumnHeights()
185− {
186− columnHeightsMax = [];
187−
188− for (var i=0; i < columnHeights.length; i++) {
189− var sum = 0;
190−
191− for (var j in columnHeights[i]) {
192− sum += columnHeights[i][j];
193− }
194−
195− columnHeightsMax.push(sum);
196− }
197−
198− if (!restoring) { // when not restoring otherwise user will be pushed to the top of the view
199− // set the height of columnFlow to max column (for flickable contentHeight)
200− contentHeight = Math.max.apply(null, columnHeightsMax);
201− }
202− }
203−
204− // Recache the visible items heights (due to a change in their height)
205− function cacheVisibleItemsHeights()
206− {
207− for (var i in items) {
208− if (items.hasOwnProperty(i)) {
209− columnHeights[itemToColumn[i]][i] = items[i].height;
210− }
211− }
212−
213− cacheColumnHeights();
214− }
215−
216− // Ensures that the correct items are visible
217− function ensureItemsVisible()
218− {
219− for (var i in items) {
220− if (items.hasOwnProperty(i)) {
221− items[i].visible = inViewport(items[i].y, items[i].height)
222− }
223− }
224− }
225−
226− // Return if there are incubating objects
227− function isIncubating()
228− {
229− for (var i in incubating) {
230− if (incubating.hasOwnProperty(i)) {
231− return true;
232− }
233− }
234−
235− return false;
236− }
237−
238− // Run after incubation to store new column height and call any further append/restores
239− function finishIncubation(index, callback)
240− {
241− var obj = incubating[index].object;
242− delete incubating[index];
243−
244− obj.heightChanged.connect(cacheVisibleItemsHeights) // if the height changes recache
245−
246− // Ensure properties linked to columnWidth are correct (as width may still be changing)
247− obj.x = itemToColumn[index] * columnWidth;
248− obj.width = columnWidth;
249−
250− items[index] = obj;
251−
252− columnHeights[itemToColumn[index]][index] = obj.height; // ensure height is the latest
253−
254− if (!isIncubating()) {
255− cacheColumnHeights();
256−
257− // Check if there is any more work to be done (append or restore)
258− callback();
259− }
260− }
261−
262− // Force any incubation to finish
263− function forceIncubationCompletion()
264− {
265− for (var i in incubating) {
266− if (incubating.hasOwnProperty(i)) {
267− incubating[i].forceCompletion()
268− }
269− }
270− }
271−
272− // Get the column index in order of height
273− function getColumnsByHeight()
274− {
275− var columnsByHeight = [];
276−
277− for (var i=0; i < columnHeightsMax.length; i++) {
278− var min = undefined;
279− var index = -1;
280−
281− // Find the smallest column that has not been found yet
282− for (var j=0; j < columnHeightsMax.length; j++) {
283− if (columnsByHeight.indexOf(j) === -1 && (min === undefined || columnHeightsMax[j] < min)) {
284− min = columnHeightsMax[j];
285− index = j;
286− }
287− }
288−
289− columnsByHeight.push(index);
290− }
291−
292− return columnsByHeight;
293− }
294−
295− // Get the highest index for a column
296− function getMaxInColumn(column)
297− {
298− var max;
299−
300− for (var i in columnHeights[column]) {
301− if (columnHeights[column].hasOwnProperty(i)) {
302− i = parseInt(i);
303−
304− if (items.hasOwnProperty(i)) {
305− if (i > max || max === undefined) {
306− max = i;
307− }
308− }
309− }
310− }
311−
312− return max;
313− }
314−
315− // Incubate an object for creation
316− function incubateObject(index, column, anchorIndex, callback)
317− {
318− // Load parameters to send to the object on creation
319− var params = {
320− "anchors.top": anchorIndex === undefined ? parent.top : items[anchorIndex].bottom,
321− index: index,
322− model: getter(index),
323− width: columnWidth,
324− x: column * columnWidth
325− };
326−
327− // Start incubating and cache the column
328− incubating[index] = delegate.incubateObject(parent, params);
329− itemToColumn[index] = column;
330−
331− if (incubating[index].status != Component.Ready) {
332− incubating[index].onStatusChanged = function(status) {
333− if (status == Component.Ready) {
334− finishIncubation(index, callback)
335− }
336− }
337− } else {
338− finishIncubation(index, callback)
339− }
340− }
341−
342− // Detect if a loaded object is in the viewport with a buffer
343− function inViewport(y, height)
344− {
345− return flickable.contentY - buffer < y + height && y < flickable.contentY + flickable.height + buffer;
346− }
347−
348− // Number of columns has changed rebuild with live items
349− function rebuildColumns()
350− {
351− restoring = true;
352− var i;
353−
354− forceIncubationCompletion()
355−
356− columnHeights = []
357− columnHeightsMax = []
358−
359− for (i=0; i < columns; i++) {
360− columnHeights.push({});
361− columnHeightsMax.push(0);
362− }
363−
364− lastIndex = 0;
365−
366− restoreItems = items;
367− items = {};
368−
369− restoreExisting()
370−
371− restoring = false;
372−
373− cacheColumnHeights(); // rebuilds contentHeight
374−
375− // If the columns have changed while the view was locked rerun
376− if (columns != columnHeights.length && visible) {
377− rebuildColumns()
378− } else {
379− append() // check if any new items can be added
380− }
381− }
382−
383− // Remove an index from the model (invalidating anything after)
384− function removeIndex(index)
385− {
386− removing = true
387−
388− forceIncubationCompletion()
389−
390− for (var i in items) {
391− if (i >= index && items.hasOwnProperty(i)) {
392− delete columnHeights[itemToColumn[i]][i]
393− delete itemToColumn[i]
394−
395− items[i].destroy()
396− delete items[i]
397− }
398− }
399−
400− lastIndex = index
401− removing = false
402−
403− cacheColumnHeights()
404− }
405−
406− // Restores existing items into potentially new positions
407− function restoreExisting()
408− {
409− var i;
410−
411− // get the columns in order
412− var columnsByHeight = getColumnsByHeight();
413− var workDone = false;
414−
415− // check if a new item in each column is possible
416− for (i=0; i < columnsByHeight.length; i++) {
417− var column = columnsByHeight[i];
418−
419− // build new object in column if possible
420− if (count > 0 && lastIndex < count) {
421− if (restoreItems.hasOwnProperty(lastIndex)) {
422− var item = restoreItems[lastIndex];
423− var maxInColumn = getMaxInColumn(column); // get lowest item in column
424−
425− itemToColumn[lastIndex] = column;
426− columnHeights[column][lastIndex] = item.height; // ensure height is the latest
427−
428− // Rebuild item properties
429− item.anchors.bottom = undefined
430− item.anchors.top = maxInColumn === undefined ? parent.top : items[maxInColumn].bottom;
431− item.x = column * columnWidth;
432− item.visible = inViewport(item.y, item.height);
433−
434− // Migrate item from restoreItems to items
435− items[lastIndex] = item;
436− delete restoreItems[lastIndex];
437−
438− // set after restore as height will likely change causing cacheVisibleItemsHeights to be run
439− item.width = columnWidth;
440−
441− cacheColumnHeights(); // ensure column heights are up to date
442−
443− lastIndex++;
444− workDone = true;
445− }
446− } else {
447− break;
448− }
449− }
450−
451− if (workDone) {
452− restoreExisting() // if work done then check if any more is needed
453− } else {
454− restoreItems = {}; // ensure restoreItems is empty
455− }
456− }
457−
458− // Reset the column flow
459− function reset()
460− {
461− forceIncubationCompletion()
462−
463− // Destroy any old items
464− for (var j in items) {
465− if (items.hasOwnProperty(j)) {
466− items[j].destroy()
467− }
468− }
469−
470− // Reset and rebuild the variables
471− items = ({})
472− itemToColumn = ({})
473− lastIndex = 0
474−
475− columnHeights = []
476−
477− for (var k=0; k < columns; k++) {
478− columnHeights.push({})
479− }
480−
481− cacheColumnHeights()
482−
483− contentHeight = 0
484− }
485−
486− function setDelayRebuildIndex(index)
487− {
488− if (delayRebuildIndex === -1 || index < lastIndex) {
489− delayRebuildIndex = index
490− }
491− }
492−
493− Component.onCompleted: append(true)
494−}