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/>.
19
* https://github.com/iBeliever/ubuntu-ui-extras/blob/master/ColumnFlow.qml
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
25
property Component delegate
27
property var getter: function (i) { return model.get(i); } // optional getter override (useful for music-app ms2 models)
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
34
onColumnsChanged: reEvalColumns()
35
onModelChanged: reEvalColumns()
36
onWidthChanged: updateWidths()
38
function updateWidths() {
39
if (repeaterCompleted) {
42
//add the first <column> elements
43
for (var i = 0; count < columns && i < columnFlow.children.length; i++) {
45
if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0)
46
//|| !columnFlow.children[i].visible) // CUSTOM - view is invisible at start
49
columnFlow.children[i].width = width / columns
56
function reEvalColumns() {
57
if (columnFlow.repeaterCompleted === false)
66
var columnHeights = new Array(columns);
67
var lastItem = new Array(columns)
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) {
78
if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0)
79
//|| !columnFlow.children[i].visible) // CUSTOM - view is invisible at start
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
94
//add the other elements
95
for (i = i; i < columnFlow.children.length; i++) {
96
var highestHeight = Number.MAX_VALUE
99
// CUSTOM - ignore if has just been removed
100
if (i === repeater.removeHintIndex && columnFlow.children[i] === repeater.removeHintItem) {
104
if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0)
105
//|| !columnFlow.children[i].visible) // CUSTOM - view is invisible at start
108
// find the shortest column
109
for (j = 0; j < columns; j++) {
110
if (columnHeights[j] !== null && columnHeights[j] < highestHeight) {
112
highestHeight = columnHeights[j]
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
121
lastItem[newColumn] = i
122
columnHeights[newColumn] += columnFlow.children[i].height
127
for (i = 0; i < columnHeights.length; i++) {
128
if (columnHeights[i])
129
cHeight = Math.max(cHeight, columnHeights[i])
132
contentHeight = cHeight
138
model: columnFlow.model
139
Component.onCompleted: {
140
columnFlow.repeaterCompleted = true
141
columnFlow.reEvalColumns()
144
// Provide a hint of the removed item
145
property int removeHintIndex: -1 // CUSTOM
146
property var removeHintItem // CUSTOM
148
onItemAdded: columnFlow.reEvalColumns() // CUSTOM - ms2 models are live
150
removeHintIndex = index
151
removeHintItem = item
153
columnFlow.reEvalColumns() // CUSTOM - ms2 models are live
155
// Set back to null to allow freeing of memory
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
40
onColumnWidthChanged: {
41
if (columns != columnHeights.length) { // number of columns has changed so reset
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;
57
if (count === 0) { // likely the model is been reset so reset the view
59
} else { // likely new items in the model check if any can be shown
64
// Append a new row of items if possible
67
// Do not allow append to run if incubating
68
if (isIncubating() === true) {
72
// get the columns in order
73
var columnsByHeight = getColumnsByHeight();
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]];
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);
88
// Cache the size of the columns for use later
89
function cacheColumnHeights()
91
columnHeightsMax = [];
93
for (var i=0; i < columnHeights.length; i++) {
96
for (var j in columnHeights[i]) {
97
sum += columnHeights[i][j];
100
columnHeightsMax.push(sum);
103
// set the height of columnFlow to max column (for flickable contentHeight)
104
contentHeight = Math.max.apply(null, columnHeightsMax);
107
// Recache the visible items heights (due to a change in their height)
108
function cacheVisibleItemsHeights()
110
for (var i in items) {
111
if (items.hasOwnProperty(i)) {
112
columnHeights[itemToColumn[i]][i] = items[i].height;
116
cacheColumnHeights();
119
// Return if there are incubating objects
120
function isIncubating()
122
for (var i in incubating) {
123
if (incubating.hasOwnProperty(i)) {
131
// Run after incubation to store new column height and call any further append/restores
132
function finishIncubation(index, callback)
134
var obj = incubating[index].object;
135
delete incubating[index];
137
obj.heightChanged.connect(cacheVisibleItemsHeights) // if the height changes recache
139
// Ensure properties linked to columnWidth are correct (as width may still be changing)
140
obj.x = itemToColumn[index] * columnWidth;
141
obj.width = columnWidth;
145
columnHeights[itemToColumn[index]][index] = obj.height; // ensure height is the latest
147
if (isIncubating() === false) {
148
cacheColumnHeights();
150
// Check if there is any more work to be done (append or restore)
155
// Get the column index in order of height
156
function getColumnsByHeight()
158
var columnsByHeight = [];
160
for (var i=0; i < columnHeightsMax.length; i++) {
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];
172
columnsByHeight.push(index);
175
return columnsByHeight;
178
// Get the min value in a column after the limit
179
function getMinIndexInColumnAfter(column, limit)
181
for (var i=limit + 1; i <= lastIndex; i++) {
182
if (columnHeights[column].hasOwnProperty(i)) {
188
// Get the lowest visible index for a column
189
function getMinVisibleInColumn(column)
193
for (var i in columnHeights[column]) {
194
if (columnHeights[column].hasOwnProperty(i)) {
197
if (items.hasOwnProperty(i)) {
198
if (i < min || min === undefined) {
208
// Get the max value in a column before the limit
209
function getMaxIndexInColumnBefore(column, limit)
211
for (var i=--limit; i >= 0; i--) {
212
if (columnHeights[column].hasOwnProperty(i)) {
218
// Get the highest visible index for a column
219
function getMaxVisibleInColumn(column)
223
for (var i in columnHeights[column]) {
224
if (columnHeights[column].hasOwnProperty(i)) {
227
if (items.hasOwnProperty(i)) {
228
if (i > max || max === undefined) {
238
// Incubate an object for creation
239
function incubateObject(index, column, anchorIndex, callback)
241
// Load parameters to send to the object on creation
244
model: getter(index),
246
x: column * columnWidth
249
if (anchorIndex === undefined) {
250
params["anchors.top"] = parent.top;
251
} else if (anchorIndex < 0) {
252
params["anchors.bottom"] = items[-(anchorIndex + 1)].top;
254
params["anchors.top"] = items[anchorIndex].bottom;
257
// Start incubating and cache the column
258
incubating[index] = delegate.incubateObject(parent, params);
259
itemToColumn[index] = column;
261
if (incubating[index].status != Component.Ready) {
262
incubating[index].onStatusChanged = function(status) {
263
if (status == Component.Ready) {
264
finishIncubation(index, callback)
268
finishIncubation(index, callback)
272
// Detect if a loaded object is in the viewport with double buffer before and single after
273
function inViewport(y, height)
275
return flickable.contentY - buffer - buffer < y + height && y < flickable.contentY + flickable.height + buffer;
278
// Reset the column flow
281
// Force and incubation to finish
282
for (var i in incubating) {
283
if (incubating.hasOwnProperty(i)) {
284
incubating[i].forceCompletion()
288
// Destroy any old items
289
for (var j in items) {
290
if (items.hasOwnProperty(j)) {
295
// Reset and rebuild the variables
301
for (var k=0; k < columns; k++) {
302
columnHeights.push({})
310
// Restore any objects that are now in the viewport
313
// Do not allow restore to run if incubating
314
if (isIncubating() === true) {
318
for (var column in columnHeights) {
321
// Rebuild anything before the lowest visible index
322
var minVisible = getMinVisibleInColumn(column);
324
if (minVisible !== undefined) {
325
// get the next lowest index for this column to add before
326
index = getMaxIndexInColumnBefore(column, minVisible)
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
336
// Rebuild anything after the highest visible index
337
var maxVisible = getMaxVisibleInColumn(column);
339
if (maxVisible !== undefined) {
340
// get the next highest index for this column to add after
341
index = getMinIndexInColumnAfter(column, maxVisible);
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
356
restore() // Restore old items (scrolling up/down)
358
append() // Append any new items (scrolling down)
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;
369
// Destroy the object