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