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