Dekko
Loading...
Searching...
No Matches
StackView.qml
1import QtQuick 2.4
2import Dekko.Controls 1.0
3
4FocusScope {
5 id: root
6
7 QtObject {
8 id: jsArray
9 property var stackView: [];
10
11 function push(p)
12 {
13 if (!p)
14 return
15 stackView.push(p)
16 __depth++
17 return p
18 }
19
20 function pop()
21 {
22 if (stackView.length === 0)
23 return null
24 var p = stackView.pop()
25 __depth--
26 return p
27 }
28
29 function current()
30 {
31 if (stackView.length === 0)
32 return null
33 return stackView[stackView.length-1]
34 }
35 }
36
41 readonly property alias depth: root.__depth
42
47 readonly property alias currentItem: root.__currentItem
48
63 property var initialItem: null
64
67 readonly property bool busy: __currentTransition !== null
68
72 property StackViewDelegate delegate: StackSlideDelegate {}
73
121 function push(item) {
122 // Note: we support two different APIs in this function; The old meego API, and
123 // the new "property list" API. Hence the reason for hiding the fact that you
124 // can pass more arguments than shown in the signature:
125 if (__recursionGuard(true))
126 return
127 var properties = arguments[1]
128 var immediate = arguments[2]
129 var replace = arguments[3]
130 var arrayPushed = (item instanceof Array)
131 var firstItem = arrayPushed ? item[0] : item
132 immediate = (immediate || jsArray.stackView.length === 0)
133
134 if (firstItem && firstItem.item && firstItem.hasOwnProperty("x") === false) {
135 // Property list API used:
136 immediate = immediate || firstItem.immediate
137 replace = replace || firstItem.replace
138 }
139
140 // Create, and push, a new javascript object, called "element", onto the stack.
141 // This element contains all the information necessary to construct the item, and
142 // will, after loaded, also contain the loaded item:
143 if (arrayPushed) {
144 if (item.length === 0)
145 return
146 var outElement = replace ? jsArray.pop() : jsArray.current()
147 for (var i=0; i<item.length; ++i)
148 jsArray.push({itemComponent:item[i], loaded: false, index: __depth, properties: properties});
149 } else {
150 outElement = replace ? jsArray.pop() : jsArray.current()
151 jsArray.push({itemComponent:item, loaded: false, index: __depth, properties: properties})
152 }
153
154 var currentElement = jsArray.current()
155 var transition = {
156 inElement: currentElement,
157 outElement: outElement,
158 immediate: immediate,
159 replace: replace,
160 push: true
161 }
162 __performTransition(transition)
163 __recursionGuard(false)
164 return __currentItem
165 }
166
194 function pop(item) {
195 if (__depth <= 1)
196 return null
197 if (item && item.hasOwnProperty("x") === false) {
198 // Property list API used:
199 var immediate = (item.immediate === true)
200 item = item.item
201 } else {
202 immediate = (arguments[1] === true)
203 }
204
205 if (item === __currentItem)
206 return
207
208 if (__recursionGuard(true))
209 return
210
211 var outElement = jsArray.pop()
212 var inElement = jsArray.current()
213
214 if (__depth > 1 && item !== undefined && item !== inElement.item) {
215 // Pop from the top until we find 'item', and return the corresponding
216 // element. Skip all non-loaded items (except the first), since no one
217 // has any references to such items anyway:
218 while (__depth > 1 && !jsArray.current().loaded)
219 jsArray.pop()
220 inElement = jsArray.current()
221 while (__depth > 1 && item !== inElement.item) {
222 jsArray.pop()
223 __cleanup(inElement)
224 while (__depth > 1 && !jsArray.current().loaded)
225 jsArray.pop()
226 inElement = jsArray.current()
227 }
228 }
229
230 var transition = {
231 inElement: inElement,
232 outElement: outElement,
233 immediate: immediate,
234 replace: false,
235 push: false
236 }
237 __performTransition(transition)
238 __recursionGuard(false)
239 return outElement.item;
240 }
241
244 function clear() {
245 if (__recursionGuard(true))
246 return
247 if (__currentTransition)
248 __currentTransition.animation.complete()
249 __currentItem = null
250 var count = __depth
251 for (var i=0; i<count; ++i) {
252 var element = jsArray.pop()
253 if (element.item)
254 __cleanup(element);
255 }
256 __recursionGuard(false)
257 }
258
267 function find(func, onlySearchLoadedItems) {
268 for (var i=__depth-1; i>=0; --i) {
269 var element = jsArray.stackView[i];
270 if (onlySearchLoadedItems !== true)
271 __loadElement(element)
272 else if (!element.item)
273 continue
274 if (func(element.item))
275 return element.item
276 }
277 return null;
278 }
279
285 function get(index, dontLoad)
286 {
287 if (index < 0 || index >= jsArray.stackView.length)
288 return null
289 var element = jsArray.stackView[index]
290 if (dontLoad !== true) {
291 __loadElement(element)
292 return element.item
293 } else if (element.item) {
294 return element.item
295 } else {
296 return null
297 }
298 }
299
304 function completeTransition()
305 {
306 if (__recursionGuard(true))
307 return
308 if (__currentTransition)
309 __currentTransition.animation.complete()
310 __recursionGuard(false)
311 }
312
313 /********* DEPRECATED API *********/
314
317 function replace(item, properties, immediate) {
318 push(item, properties, immediate, true)
319 }
320
321 /********* PRIVATE API *********/
322
324 property Item __currentItem: null
326 property int __depth: 0
328 property var __currentTransition: null
330 property bool __guard: false
331
332 Component.onCompleted: {
333 if (initialItem)
334 push(initialItem)
335 }
336
337 Component.onDestruction: {
338 if (__currentTransition)
339 __currentTransition.animation.complete()
340 __currentItem = null
341 }
342
344 function __recursionGuard(use)
345 {
346 if (use && __guard) {
347 console.warn("Warning: StackView: You cannot push/pop recursively!")
348 console.trace()
349 return true
350 }
351 __guard = use
352 }
353
355 function __loadElement(element)
356 {
357 if (element.loaded) {
358 if (!element.item) {
359 element.item = invalidItemReplacement.createObject(root)
360 element.item.text = "\nError: The item has been deleted outside StackView!"
361 }
362 return
363 }
364 if (!element.itemComponent) {
365 element.item = invalidItemReplacement.createObject(root)
366 element.item.text = "\nError: Invalid item (item was 'null'). "
367 + "This might indicate that the item was deleted outside StackView!"
368 return
369 }
370
371 var comp = __resolveComponent(element.itemComponent, element)
372
373 // Assign properties to item:
374 if (!element.properties)
375 element.properties = {}
376
377 if (comp.hasOwnProperty("createObject")) {
378 if (comp.status === Component.Error) {
379 element.item = invalidItemReplacement.createObject(root)
380 element.item.text = "\nError: Could not load: " + comp.errorString()
381 } else {
382 element.item = comp.createObject(root, element.properties)
383 // Destroy items we create unless the user specified something else:
384 if (!element.hasOwnProperty("destroyOnPop"))
385 element.destroyOnPop = true
386 }
387 } else {
388 // comp is already an Item, so just re-parent it into the StackView:
389 element.item = comp
390 element.originalParent = parent
391 element.item.parent = root
392 for (var prop in element.properties) {
393 if (element.item.hasOwnProperty(prop))
394 element.item[prop] = element.properties[prop];
395 }
396 // Do not destroy items we didn't create, unless the user specified something else:
397 if (!element.hasOwnProperty("destroyOnPop"))
398 element.destroyOnPop = false
399 }
400
401 element.item.Stack.__index = element.index
402 element.item.Stack.__view = root
403 // Let item fill all available space by default:
404 element.item.width = Qt.binding(function() { return root.width })
405 element.item.height = Qt.binding(function() { return root.height })
406 element.loaded = true
407 }
408
410 function __resolveComponent(unknownObjectType, element)
411 {
412 // We need this extra resolve function since we don't really
413 // know what kind of object the user pushed. So we try to
414 // figure it out by inspecting the object:
415 if (unknownObjectType.hasOwnProperty("createObject")) {
416 return unknownObjectType
417 } else if (typeof unknownObjectType == "string") {
418 return Qt.createComponent(unknownObjectType)
419 } else if (unknownObjectType.hasOwnProperty("x")) {
420 return unknownObjectType
421 } else if (unknownObjectType.hasOwnProperty("item")) {
422 // INVARIANT: user pushed a JS-object
423 element.properties = unknownObjectType.properties
424 if (!unknownObjectType.item)
425 unknownObjectType.item = invalidItemReplacement
426 if (unknownObjectType.hasOwnProperty("destroyOnPop"))
427 element.destroyOnPop = unknownObjectType.destroyOnPop
428 return __resolveComponent(unknownObjectType.item, element)
429 } else {
430 // We cannot determine the type, so assume its a URL:
431 return Qt.createComponent(unknownObjectType)
432 }
433 }
434
436 function __cleanup(element) {
437 // INVARIANT: element has been removed from jsArray. Destroy its
438 // item, or re-parent it back to the parent it had before it was pushed:
439 var item = element.item
440 if (element.destroyOnPop) {
441 console.log("Destroying item: ", item)
442 item.destroy()
443 gc()
444 } else {
445 // Mark the item as no longer part of the StackView. It
446 // might reenter on pop if pushed several times:
447 item.visible = false
448 __setStatus(item, Stack.Inactive)
449 item.Stack.__view = null
450 item.Stack.__index = -1
451 if (element.originalParent)
452 item.parent = element.originalParent
453 }
454 }
455
457 function __setStatus(item, status) {
458 item.Stack.__status = status
459 }
460
462 function __performTransition(transition)
463 {
464 // Animate item in "outElement" out, and item in "inElement" in. Set a guard to protect
465 // the user from pushing new items on signals that will fire while preparing for the transition
466 // (e.g Stack.onCompleted, Stack.onStatusChanged, Stack.onIndexChanged etc). Otherwise, we will enter
467 // this function several times, which causes the items to be updated half-way.
468 if (__currentTransition)
469 __currentTransition.animation.complete()
470 __loadElement(transition.inElement)
471
472 transition.name = transition.replace ? "replaceTransition" : (transition.push ? "pushTransition" : "popTransition")
473 var enterItem = transition.inElement.item
474 transition.enterItem = enterItem
475
476 // Since an item can be pushed several times, we need to update its properties:
477 enterItem.parent = root
478 enterItem.Stack.__view = root
479 enterItem.Stack.__index = transition.inElement.index
480 __currentItem = enterItem
481
482 if (!transition.outElement) {
483 // A transition consists of two items, but we got just one. So just show the item:
484 enterItem.visible = true
485 __setStatus(enterItem, Stack.Activating)
486 __setStatus(enterItem, Stack.Active)
487 return
488 }
489
490 var exitItem = transition.outElement.item
491 transition.exitItem = exitItem
492 if (enterItem === exitItem)
493 return
494
495 if (root.delegate) {
496 transition.properties = {
497 "name":transition.name,
498 "enterItem":transition.enterItem,
499 "exitItem":transition.exitItem,
500 "immediate":transition.immediate }
501 var anim = root.delegate.getTransition(transition.properties)
502 if (anim.createObject) {
503 anim = anim.createObject(null, transition.properties)
504 anim.runningChanged.connect(function(){ if (anim.running === false) anim.destroy() })
505 }
506 transition.animation = anim
507 }
508
509 if (!transition.animation) {
510 console.warn("Warning: StackView: no", transition.name, "found!")
511 return
512 }
513 if (enterItem.anchors.fill || exitItem.anchors.fill)
514 console.warn("Warning: StackView: cannot transition an item that is anchored!")
515
516 __currentTransition = transition
517 __setStatus(exitItem, Stack.Deactivating)
518 enterItem.visible = true
519 __setStatus(enterItem, Stack.Activating)
520 transition.animation.runningChanged.connect(animationFinished)
521 transition.animation.start()
522 // NB! For empty animations, "animationFinished" is already
523 // executed at this point, leaving __animation === null:
524 if (transition.immediate === true && transition.animation)
525 transition.animation.complete()
526 }
527
529 function animationFinished()
530 {
531 if (!__currentTransition || __currentTransition.animation.running)
532 return
533
534 __currentTransition.animation.runningChanged.disconnect(animationFinished)
535 __currentTransition.exitItem.visible = false
536 __setStatus(__currentTransition.exitItem, Stack.Inactive);
537 __setStatus(__currentTransition.enterItem, Stack.Active);
538 __currentTransition.properties.animation = __currentTransition.animation
539 root.delegate.transitionFinished(__currentTransition.properties)
540
541 if (!__currentTransition.push || __currentTransition.replace)
542 __cleanup(__currentTransition.outElement)
543
544 __currentTransition = null
545 }
546
548 property Component invalidItemReplacement: Component {
549 Text {
550 width: parent.width
551 height: parent.height
552 wrapMode: Text.WrapAtWordBoundaryOrAnywhere
553 }
554 }
555}
556