Unity 8
GenericScopeView.qml
1 /*
2  * Copyright (C) 2013-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 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 General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.0
18 import Ubuntu.Components 1.1
19 import Utils 0.1
20 import Unity 0.2
21 import Dash 0.1
22 import "../Components"
23 import "../Components/ListItems" as ListItems
24 
25 FocusScope {
26  id: scopeView
27 
28  readonly property bool navigationDisableParentInteractive: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].disableParentInteractive : false
29  property bool forceNonInteractive: false
30  property var scope: null
31  property UnitySortFilterProxyModel categories: categoryFilter
32  property bool isCurrent: false
33  property alias moving: categoryView.moving
34  property bool hasBackAction: false
35  property bool enableHeightBehaviorOnNextCreation: false
36  property var categoryView: categoryView
37  property bool showPageHeader: true
38  readonly property alias subPageShown: subPageLoader.subPageShown
39  property int paginationCount: 0
40  property int paginationIndex: 0
41  property bool visibleToParent: false
42  property alias pageHeaderTotallyVisible: categoryView.pageHeaderTotallyVisible
43  property var holdingList: null
44  property bool wasCurrentOnMoveStart: false
45 
46  property var scopeStyle: ScopeStyle {
47  style: scope ? scope.customizations : {}
48  }
49 
50  readonly property bool processing: scope ? scope.searchInProgress || subPageLoader.processing : false
51 
52  signal backClicked()
53 
54  onScopeChanged: {
55  floatingSeeLess.companionBase = null;
56  }
57 
58  function positionAtBeginning() {
59  categoryView.positionAtBeginning()
60  }
61 
62  function showHeader() {
63  categoryView.showHeader()
64  }
65 
66  function closePreview() {
67  subPageLoader.closeSubPage()
68  }
69 
70  function resetSearch() {
71  if(pageHeaderLoader.item && showPageHeader)
72  pageHeaderLoader.item.resetSearch()
73  }
74 
75  function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount, categoryId) {
76  if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope" || (scope.id === "videoaggregator" && categoryId === "myvideos-getstarted")) {
77  // TODO Technically it is possible that calling activate() will make the scope emit
78  // previewRequested so that we show a preview but there's no scope that does that yet
79  // so it's not implemented
80  scope.activate(result)
81  } else {
82  if (scope.preview(result)) {
83  openPreview(index, resultsModel, limitedCategoryItemCount);
84  }
85  }
86  }
87 
88  function itemPressedAndHeld(index, result, itemModel, resultsModel, limitedCategoryItemCount, categoryId) {
89  if (itemModel.uri.indexOf("scope://") !== 0 && !(scope.id === "videoaggregator" && categoryId === "myvideos-getstarted")) {
90  if (scope.preview(result)) {
91  openPreview(index, resultsModel, limitedCategoryItemCount);
92  }
93  }
94  }
95 
96  function openPreview(index, resultsModel, limitedCategoryItemCount) {
97  if (limitedCategoryItemCount > 0) {
98  previewLimitModel.model = resultsModel;
99  previewLimitModel.limit = limitedCategoryItemCount;
100  subPageLoader.model = previewLimitModel;
101  } else {
102  subPageLoader.model = resultsModel;
103  }
104  subPageLoader.initialIndex = -1;
105  subPageLoader.initialIndex = index;
106  subPageLoader.openSubPage("preview");
107  }
108 
109  Binding {
110  target: scope
111  property: "isActive"
112  value: isCurrent && !subPageLoader.open && (Qt.application.state == Qt.ApplicationActive)
113  }
114 
115  UnitySortFilterProxyModel {
116  id: categoryFilter
117  model: scope ? scope.categories : null
118  dynamicSortFilter: true
119  filterRole: Categories.RoleCount
120  filterRegExp: /^0$/
121  invertMatch: true
122  }
123 
124  onIsCurrentChanged: {
125  if (!holdingList || !holdingList.moving) {
126  wasCurrentOnMoveStart = scopeView.isCurrent;
127  }
128  if (pageHeaderLoader.item && showPageHeader) {
129  pageHeaderLoader.item.resetSearch();
130  }
131  subPageLoader.closeSubPage();
132  }
133 
134  Binding {
135  target: scopeView.scope
136  property: "searchQuery"
137  value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
138  when: isCurrent && showPageHeader
139  }
140 
141  Binding {
142  target: pageHeaderLoader.item
143  property: "searchQuery"
144  value: scopeView.scope ? scopeView.scope.searchQuery : ""
145  when: isCurrent && showPageHeader
146  }
147 
148  Connections {
149  target: scopeView.scope
150  onShowDash: subPageLoader.closeSubPage()
151  onHideDash: subPageLoader.closeSubPage()
152  }
153 
154  Connections {
155  target: holdingList
156  onMovingChanged: {
157  if (!moving) {
158  wasCurrentOnMoveStart = scopeView.isCurrent;
159  }
160  }
161  }
162 
163  Rectangle {
164  anchors.fill: parent
165  color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
166  visible: color != "transparent"
167  }
168 
169  ScopeListView {
170  id: categoryView
171  objectName: "categoryListView"
172  interactive: !forceNonInteractive
173 
174  x: subPageLoader.open ? -width : 0
175  visible: x != -width
176  Behavior on x { UbuntuNumberAnimation { } }
177  width: parent.width
178  height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
179  : parent.height
180  clip: height != parent.height
181 
182  model: scopeView.categories
183  forceNoClip: subPageLoader.open
184  pixelAligned: true
185 
186  property string expandedCategoryId: ""
187  property int runMaximizeAfterSizeChanges: 0
188 
189  readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
190  ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
191 
192  onExpandedCategoryIdChanged: {
193  var firstCreated = firstCreatedIndex();
194  var shrinkingAny = false;
195  var shrinkHeightDifference = 0;
196  for (var i = 0; i < createdItemCount(); ++i) {
197  var baseItem = item(firstCreated + i);
198  if (baseItem.expandable) {
199  var shouldExpand = baseItem.category === expandedCategoryId;
200  if (shouldExpand != baseItem.expanded) {
201  var animate = false;
202  if (!subPageLoader.open) {
203  var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
204  var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
205  animate = shrinkingAny || animateShrinking || animateGrowing;
206  }
207 
208  if (!shouldExpand) {
209  shrinkingAny = true;
210  shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
211  }
212 
213  if (shouldExpand && !subPageLoader.open) {
214  if (!shrinkingAny) {
215  categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
216  } else {
217  // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
218  // after the shrink/grow animation ends
219  var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
220  if (growHeightDifference > shrinkHeightDifference) {
221  runMaximizeAfterSizeChanges = 2;
222  } else {
223  runMaximizeAfterSizeChanges = 0;
224  }
225  }
226  }
227 
228  baseItem.expand(shouldExpand, animate);
229  }
230  }
231  }
232  }
233 
234  delegate: DashCategoryBase {
235  id: baseItem
236  objectName: "dashCategory" + category
237 
238  property Item seeAllButton: seeAll
239 
240  readonly property bool expandable: {
241  if (categoryView.model.count === 1) return false;
242  if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
243  if (item && item.expandedHeight > item.collapsedHeight) return true;
244  return false;
245  }
246  property bool expanded: false
247  readonly property string category: categoryId
248  readonly property string headerLink: model.headerLink
249  readonly property var item: rendererLoader.item
250 
251  function expand(expand, animate) {
252  heightBehaviour.enabled = animate;
253  expanded = expand;
254  }
255 
256  CardTool {
257  id: cardTool
258  objectName: "cardTool"
259  count: results ? results.count : 0
260  template: model.renderer
261  components: model.components
262  viewWidth: parent.width
263  }
264 
265  onExpandableChanged: {
266  // This can happen with the VJ that doesn't know how height it will be on creation
267  // so doesn't set expandable until a bit too late for onLoaded
268  if (expandable) {
269  var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
270  baseItem.expand(shouldExpand, false /*animate*/);
271  }
272  }
273 
274  onHeightChanged: rendererLoader.updateRanges();
275  onYChanged: rendererLoader.updateRanges();
276 
277  Loader {
278  id: rendererLoader
279  anchors {
280  top: parent.top
281  left: parent.left
282  right: parent.right
283  topMargin: name != "" ? 0 : units.gu(2)
284  }
285 
286  Behavior on height {
287  id: heightBehaviour
288  enabled: false
289  animation: UbuntuNumberAnimation {
290  duration: UbuntuAnimation.FastDuration
291  onRunningChanged: {
292  if (!running) {
293  heightBehaviour.enabled = false
294  if (categoryView.runMaximizeAfterSizeChanges > 0) {
295  categoryView.runMaximizeAfterSizeChanges--;
296  if (categoryView.runMaximizeAfterSizeChanges == 0) {
297  var firstCreated = categoryView.firstCreatedIndex();
298  for (var i = 0; i < categoryView.createdItemCount(); ++i) {
299  var baseItem = categoryView.item(firstCreated + i);
300  if (baseItem.category === categoryView.expandedCategoryId) {
301  categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
302  break;
303  }
304  }
305  }
306  }
307  }
308  }
309  }
310  }
311 
312  readonly property bool expanded: baseItem.expanded || !baseItem.expandable
313  height: expanded ? item.expandedHeight : item.collapsedHeight
314 
315  source: {
316  switch (cardTool.categoryLayout) {
317  case "carousel": return "CardCarousel.qml";
318  case "vertical-journal": return "CardVerticalJournal.qml";
319  case "horizontal-list": return "CardHorizontalList.qml";
320  case "grid":
321  default: return "CardGrid.qml";
322  }
323  }
324 
325  onLoaded: {
326  if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
327  item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
328  scopeView.enableHeightBehaviorOnNextCreation = false;
329  }
330  item.model = Qt.binding(function() { return results })
331  item.objectName = Qt.binding(function() { return categoryId })
332  item.scopeStyle = scopeView.scopeStyle;
333  if (baseItem.expandable) {
334  var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
335  baseItem.expand(shouldExpand, false /*animate*/);
336  }
337  updateRanges();
338  if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
339  // Yeah, hackish :/
340  if (scopeView.width > units.gu(45)) {
341  if (scopeView.width >= units.gu(70)) {
342  cardTool.cardWidth = units.gu(9);
343  } else {
344  cardTool.cardWidth = units.gu(10);
345  }
346  }
347  cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
348  }
349  item.cardTool = cardTool;
350  }
351 
352  Component.onDestruction: {
353  if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
354  scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
355  }
356  }
357 
358  Connections {
359  target: rendererLoader.item
360  onClicked: {
361  scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount(), baseItem.category);
362  }
363 
364  onPressAndHold: {
365  scopeView.itemPressedAndHeld(index, result, itemModel, target.model, categoryItemCount(), baseItem.category);
366  }
367 
368  function categoryItemCount() {
369  var categoryItemCount = -1;
370  if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
371  categoryItemCount = target.collapsedItemCount;
372  }
373  return categoryItemCount;
374  }
375  }
376  Connections {
377  target: categoryView
378  onOriginYChanged: rendererLoader.updateRanges();
379  onContentYChanged: rendererLoader.updateRanges();
380  onHeightChanged: rendererLoader.updateRanges();
381  onContentHeightChanged: rendererLoader.updateRanges();
382  }
383  Connections {
384  target: scopeView
385  onIsCurrentChanged: rendererLoader.updateRanges();
386  onVisibleToParentChanged: rendererLoader.updateRanges();
387  }
388  Connections {
389  target: holdingList
390  onMovingChanged: if (!moving) rendererLoader.updateRanges();
391  }
392 
393  function updateRanges() {
394  // Don't want to create stress by requesting more items during scope
395  // changes so unless you're not part of the visible scopes just return.
396  // For the visible scopes we need to do some work, the previously non visible
397  // scope needs to adjust its ranges so that we define the new visible range,
398  // that still means no creation/destruction of delegates, it's just about changing
399  // the culling of the items so they are actually visible
400  if (holdingList && holdingList.moving && !scopeView.visibleToParent) {
401  return;
402  }
403 
404  if (categoryView.moving) {
405  // Do not update the range if we are overshooting up or down, since we'll come back
406  // to the stable position and delete/create items without any reason
407  if (categoryView.contentY < categoryView.originY) {
408  return;
409  } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
410  categoryView.contentY + categoryView.height > categoryView.contentHeight) {
411  return;
412  }
413  }
414 
415  if (item && item.hasOwnProperty("displayMarginBeginning")) {
416  var buffer = wasCurrentOnMoveStart ? categoryView.height * 1.5 : 0;
417  var onViewport = baseItem.y + baseItem.height > 0 &&
418  baseItem.y < categoryView.height;
419  var onBufferViewport = baseItem.y + baseItem.height > -buffer &&
420  baseItem.y < categoryView.height + buffer;
421 
422  if (item.growsVertically) {
423  // A item view creates its delegates synchronously from
424  // -displayMarginBeginning
425  // to
426  // height + displayMarginEnd
427  // Around that area it adds the cacheBuffer area where delegates are created async
428  //
429  // We adjust displayMarginBeginning and displayMarginEnd so
430  // * In non visible scopes nothing is considered visible and we set cacheBuffer
431  // so that creates the items that would be in the viewport asynchronously
432  // * For the current scope set the visible range to the viewport and then
433  // use cacheBuffer to create extra items for categoryView.height * 1.5
434  // to make scrolling nicer by mantaining a higher number of
435  // cached items
436  // * For non current but visible scopes (i.e. when the user changes from one scope
437  // to the next, we set the visible range to the viewport so
438  // items are not culled (invisible) but still use no cacheBuffer
439  // (it will be set once the scope is the current one)
440  var displayMarginBeginning = baseItem.y + rendererLoader.anchors.topMargin;
441  displayMarginBeginning = -Math.max(-displayMarginBeginning, 0);
442  displayMarginBeginning = -Math.min(-displayMarginBeginning, baseItem.height);
443  displayMarginBeginning = Math.round(displayMarginBeginning);
444  var displayMarginEnd = -baseItem.height + seeAll.height + categoryView.height - baseItem.y;
445  displayMarginEnd = -Math.max(-displayMarginEnd, 0);
446  displayMarginEnd = -Math.min(-displayMarginEnd, baseItem.height);
447  displayMarginEnd = Math.round(displayMarginEnd);
448 
449  if (onBufferViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
450  item.displayMarginBeginning = displayMarginBeginning;
451  item.displayMarginEnd = displayMarginEnd;
452  if (holdingList && holdingList.moving) {
453  // If we are moving we need to reset the cache buffer of the
454  // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
455  // otherwise the cache buffer we had set to preload the items of the
456  // visible range will trigger some item creations and we want move to
457  // be as smooth as possible meaning no need creations
458  if (!wasCurrentOnMoveStart) {
459  item.cacheBuffer = 0;
460  }
461  } else {
462  // Protect us against cases where the item hasn't yet been positioned
463  if (!(categoryView.contentY === 0 && baseItem.y === 0 && index !== 0)) {
464  item.cacheBuffer = categoryView.height * 1.5;
465  }
466  }
467  } else {
468  var visibleRange = baseItem.height + displayMarginEnd + displayMarginBeginning;
469  if (visibleRange < 0) {
470  item.displayMarginBeginning = displayMarginBeginning;
471  item.displayMarginEnd = displayMarginEnd;
472  item.cacheBuffer = 0;
473  } else {
474  // This should be visibleRange/2 in each of the properties
475  // but some item views still (like GridView) like creating sync delegates even if
476  // the visible range is 0 so let's make sure the visible range is negative
477  item.displayMarginBeginning = displayMarginBeginning - visibleRange;
478  item.displayMarginEnd = displayMarginEnd - visibleRange;
479  item.cacheBuffer = visibleRange;
480  }
481  }
482  } else {
483  if (!onBufferViewport) {
484  // If not on the buffered viewport, don't load anything
485  item.displayMarginBeginning = 0;
486  item.displayMarginEnd = -item.innerWidth;
487  item.cacheBuffer = 0;
488  } else {
489  if (onViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
490  // If on the buffered viewport and the viewport and the on a visible scope
491  // Set displayMargin so that cards are rendered
492  // And if not moving the parent list also give it some extra asynchronously
493  // buffering
494  item.displayMarginBeginning = 0;
495  item.displayMarginEnd = 0;
496  if (holdingList && holdingList.moving) {
497  // If we are moving we need to reset the cache buffer of the
498  // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
499  // otherwise the cache buffer we had set to preload the items of the
500  // visible range will trigger some item creations and we want move to
501  // be as smooth as possible meaning no need creations
502  if (!wasCurrentOnMoveStart) {
503  item.cacheBuffer = 0;
504  }
505  } else {
506  item.cacheBuffer = baseItem.width * 1.5;
507  }
508  } else {
509  // If on the buffered viewport but either not in the real viewport
510  // or in the viewport of the non current scope, use displayMargin + cacheBuffer
511  // to render asynchronously the width of cards
512  item.displayMarginBeginning = 0;
513  item.displayMarginEnd = -item.innerWidth;
514  item.cacheBuffer = item.innerWidth;
515  }
516  }
517  }
518  }
519  }
520  }
521 
522  AbstractButton {
523  id: seeAll
524  objectName: "seeAll"
525  anchors {
526  top: rendererLoader.bottom
527  left: parent.left
528  right: parent.right
529  }
530  height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
531  visible: height != 0
532 
533  onClicked: {
534  if (categoryView.expandedCategoryId !== baseItem.category) {
535  categoryView.expandedCategoryId = baseItem.category;
536  floatingSeeLess.companionBase = baseItem;
537  } else {
538  categoryView.expandedCategoryId = "";
539  }
540  }
541 
542  Label {
543  id: seeAllLabel
544  text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
545  anchors {
546  centerIn: parent
547  verticalCenterOffset: units.gu(-0.5)
548  }
549  fontSize: "small"
550  font.weight: Font.Bold
551  color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
552  }
553  }
554 
555  Image {
556  visible: index != 0
557  anchors {
558  top: parent.top
559  left: parent.left
560  right: parent.right
561  }
562  fillMode: Image.Stretch
563  source: "graphics/dash_divider_top_lightgrad.png"
564  z: -1
565  }
566 
567  Image {
568  // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
569  visible: index != categoryView.model.count - 1
570  anchors {
571  bottom: seeAll.bottom
572  left: parent.left
573  right: parent.right
574  }
575  fillMode: Image.Stretch
576  source: "graphics/dash_divider_top_darkgrad.png"
577  z: -1
578  }
579  }
580 
581  sectionProperty: "name"
582  sectionDelegate: ListItems.Header {
583  objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
584  readonly property var delegate: categoryView.item(delegateIndex)
585  width: categoryView.width
586  height: section != "" ? units.gu(5) : 0
587  text: section
588  color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
589  iconName: delegate && delegate.headerLink ? "go-next" : ""
590  onClicked: {
591  if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
592  }
593  }
594 
595  pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
596  Loader {
597  id: pageHeaderLoader
598  width: parent.width
599  sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
600  Component {
601  id: pageHeaderComponent
602  PageHeader {
603  objectName: "scopePageHeader"
604  width: parent.width
605  title: scopeView.scope ? scopeView.scope.name : ""
606  searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.ctr("Label: Hint for dash search line edit", "Search")
607  showBackButton: scopeView.hasBackAction
608  searchEntryEnabled: true
609  settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
610  favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
611  favorite: scopeView.scope && scopeView.scope.favorite
612  scopeStyle: scopeView.scopeStyle
613  paginationCount: scopeView.paginationCount
614  paginationIndex: scopeView.paginationIndex
615 
616  bottomItem: DashNavigation {
617  scope: scopeView.scope
618  anchors { left: parent.left; right: parent.right }
619  windowHeight: scopeView.height
620  windowWidth: scopeView.width
621  scopeStyle: scopeView.scopeStyle
622  }
623 
624  onBackClicked: scopeView.backClicked()
625  onSettingsClicked: subPageLoader.openSubPage("settings")
626  onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
627  onSearchTextFieldFocused: scopeView.showHeader()
628  }
629  }
630  }
631  }
632 
633  Item {
634  id: pullToRefreshClippingItem
635  anchors.left: parent.left
636  anchors.right: parent.right
637  anchors.bottom: parent.bottom
638  height: parent.height - pullToRefresh.contentY + (pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].height - pageHeaderLoader.item.height : 0)
639  clip: true
640 
641  PullToRefresh {
642  id: pullToRefresh
643  objectName: "pullToRefresh"
644  target: categoryView
645 
646  readonly property real contentY: categoryView.contentY - categoryView.originY
647  y: -contentY - units.gu(5)
648 
649  onRefresh: {
650  refreshing = true
651  scopeView.scope.refresh()
652  }
653  anchors.left: parent.left
654  anchors.right: parent.right
655 
656  Connections {
657  target: scopeView
658  onProcessingChanged: if (!scopeView.processing) pullToRefresh.refreshing = false
659  }
660 
661  style: PullToRefreshScopeStyle {
662  anchors.fill: parent
663  activationThreshold: units.gu(14)
664  }
665  }
666  }
667 
668  AbstractButton {
669  id: floatingSeeLess
670  objectName: "floatingSeeLess"
671 
672  property Item companionTo: companionBase ? companionBase.seeAllButton : null
673  property Item companionBase: null
674  property bool showBecausePosition: false
675  property real yOffset: 0
676 
677  anchors {
678  left: categoryView.left
679  right: categoryView.right
680  }
681  y: parent.height - height + yOffset
682  height: seeLessLabel.font.pixelSize + units.gu(4)
683  visible: companionTo && showBecausePosition
684 
685  onClicked: categoryView.expandedCategoryId = "";
686 
687  function updateVisibility() {
688  var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
689  showBecausePosition = companionPos.y > 0;
690 
691  var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
692  yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
693  yOffset = Math.min(yOffset, height);
694 
695  if (!showBecausePosition && categoryView.expandedCategoryId === "") {
696  companionBase = null;
697  }
698  }
699 
700  Label {
701  id: seeLessLabel
702  text: i18n.tr("See less")
703  anchors {
704  centerIn: parent
705  verticalCenterOffset: units.gu(-0.5)
706  }
707  fontSize: "small"
708  font.weight: Font.Bold
709  color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
710  }
711 
712  Connections {
713  target: floatingSeeLess.companionTo ? categoryView : null
714  onContentYChanged: floatingSeeLess.updateVisibility();
715  }
716 
717  Connections {
718  target: floatingSeeLess.companionTo
719  onYChanged: floatingSeeLess.updateVisibility();
720  }
721  }
722 
723  LimitProxyModel {
724  id: previewLimitModel
725  }
726 
727  Loader {
728  id: subPageLoader
729  objectName: "subPageLoader"
730  visible: x != width
731  width: parent.width
732  height: parent.height
733  anchors.left: categoryView.right
734 
735  property bool open: false
736  property var scope: scopeView.scope
737  property var scopeStyle: scopeView.scopeStyle
738  property int initialIndex: -1
739  property var model: null
740 
741  readonly property bool processing: item && item.processing || false
742  readonly property int count: item && item.count || 0
743  readonly property int currentIndex: item && item.currentIndex || 0
744  readonly property var currentItem: item && item.currentItem || null
745 
746  property string subPage: ""
747  readonly property bool subPageShown: visible && status === Loader.Ready
748 
749  function openSubPage(page) {
750  subPage = page;
751  }
752 
753  function closeSubPage() {
754  open = false;
755  }
756 
757  source: switch(subPage) {
758  case "preview": return "PreviewListView.qml";
759  case "settings": return "ScopeSettingsPage.qml";
760  default: return "";
761  }
762 
763  onLoaded: {
764  item.scope = Qt.binding(function() { return subPageLoader.scope; } )
765  item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
766  if (subPage == "preview") {
767  item.open = Qt.binding(function() { return subPageLoader.open; } )
768  item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
769  item.model = Qt.binding(function() { return subPageLoader.model; } )
770  }
771  open = true;
772  }
773 
774  onOpenChanged: pageHeaderLoader.item.unfocus()
775 
776  onVisibleChanged: if (!visible) subPage = ""
777 
778  Connections {
779  target: subPageLoader.item
780  onBackClicked: subPageLoader.closeSubPage()
781  }
782  }
783 }