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