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