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