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