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 
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 expandable: {
247  if (categoryView.model.count === 1) return false;
248  if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
249  if (item && item.expandedHeight > item.collapsedHeight) return true;
250  return false;
251  }
252  property bool expanded: false
253  readonly property string category: categoryId
254  readonly property string headerLink: model.headerLink
255  readonly property var item: rendererLoader.item
256 
257  function expand(expand, animate) {
258  heightBehaviour.enabled = animate;
259  expanded = expand;
260  }
261 
262  CardTool {
263  id: cardTool
264  objectName: "cardTool"
265  count: results ? results.count : 0
266  template: model.renderer
267  components: model.components
268  viewWidth: parent.width
269  }
270 
271  onExpandableChanged: {
272  // This can happen with the VJ that doesn't know how height it will be on creation
273  // so doesn't set expandable until a bit too late for onLoaded
274  if (expandable) {
275  var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
276  baseItem.expand(shouldExpand, false /*animate*/);
277  }
278  }
279 
280  onHeightChanged: rendererLoader.updateRanges();
281  onYChanged: rendererLoader.updateRanges();
282 
283  Loader {
284  id: rendererLoader
285  anchors {
286  top: parent.top
287  left: parent.left
288  right: parent.right
289  topMargin: name != "" ? 0 : units.gu(2)
290  }
291 
292  Behavior on height {
293  id: heightBehaviour
294  enabled: false
295  animation: UbuntuNumberAnimation {
296  duration: UbuntuAnimation.FastDuration
297  onRunningChanged: {
298  if (!running) {
299  heightBehaviour.enabled = false
300  if (categoryView.runMaximizeAfterSizeChanges > 0) {
301  categoryView.runMaximizeAfterSizeChanges--;
302  if (categoryView.runMaximizeAfterSizeChanges == 0) {
303  var firstCreated = categoryView.firstCreatedIndex();
304  for (var i = 0; i < categoryView.createdItemCount(); ++i) {
305  var baseItem = categoryView.item(firstCreated + i);
306  if (baseItem.category === categoryView.expandedCategoryId) {
307  categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
308  break;
309  }
310  }
311  }
312  }
313  }
314  }
315  }
316  }
317 
318  readonly property bool expanded: baseItem.expanded || !baseItem.expandable
319  height: expanded ? item.expandedHeight : item.collapsedHeight
320 
321  source: {
322  switch (cardTool.categoryLayout) {
323  case "carousel": return "CardCarousel.qml";
324  case "vertical-journal": return "CardVerticalJournal.qml";
325  case "horizontal-list": return "CardHorizontalList.qml";
326  case "grid":
327  default: return "CardGrid.qml";
328  }
329  }
330 
331  onLoaded: {
332  if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
333  item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
334  scopeView.enableHeightBehaviorOnNextCreation = false;
335  }
336  item.model = Qt.binding(function() { return results })
337  item.objectName = Qt.binding(function() { return categoryId })
338  item.scopeStyle = scopeView.scopeStyle;
339  if (baseItem.expandable) {
340  var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
341  baseItem.expand(shouldExpand, false /*animate*/);
342  }
343  updateRanges();
344  clickScopeSizingHacks();
345  if (scope && (scope.id === "clickscope" || scope.id === "libertine-scope.ubuntu_libertine-scope")) {
346  if (scope.id === "libertine-scope.ubuntu_libertine-scope" || categoryId === "predefined" || categoryId === "local") {
347  cardTool.artShapeSize = Qt.binding(function() { return Qt.size(units.gu(8), units.gu(7.5)) });
348  cardTool.artShapeStyle = "icon";
349  } else {
350  // Should be ubuntu store icon
351  cardTool.artShapeStyle = "flat";
352  item.backgroundShapeStyle = "shadow";
353  }
354  }
355  item.cardTool = cardTool;
356  }
357 
358  Component.onDestruction: {
359  if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
360  scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
361  }
362  }
363  // FIXME: directly connecting to onUnitsChanged cause a compile error:
364  // Cannot assign to non-existent property "onUnitsChanged"
365  // Until the units object is reworked to properly do all we need, let's go through a intermediate property
366  property int pxpgu: units.gu(1);
367  onPxpguChanged: clickScopeSizingHacks();
368 
369  function clickScopeSizingHacks() {
370  if (scope &&
371  ((scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) ||
372  scope.id === "libertine-scope.ubuntu_libertine-scope")) {
373  // Yeah, hackish :/
374  if (scopeView.width > units.gu(45)) {
375  if (scopeView.width >= units.gu(70)) {
376  cardTool.cardWidth = units.gu(11);
377  item.minimumHorizontalSpacing = units.gu(5);
378  return;
379  } else {
380  cardTool.cardWidth = units.gu(10);
381  }
382  } else {
383  cardTool.cardWidth = units.gu(12);
384  }
385  item.minimumHorizontalSpacing = item.defaultMinimumHorizontalSpacing;
386  }
387  }
388 
389  Connections {
390  target: rendererLoader.item
391  onClicked: { // (int index, var result, var item, var itemModel)
392  scopeView.itemClicked(result, baseItem.category);
393  }
394 
395  onPressAndHold: { // (int index, var result, var itemModel)
396  scopeView.itemPressedAndHeld(result, baseItem.category);
397  }
398 
399  onAction: { // (int index, var result, var actionId)
400  scope.activateAction(result, baseItem.category, actionId);
401  }
402 
403  function categoryItemCount() {
404  var categoryItemCount = -1;
405  if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
406  categoryItemCount = target.collapsedItemCount;
407  }
408  return categoryItemCount;
409  }
410  }
411  Connections {
412  target: categoryView
413  onOriginYChanged: rendererLoader.updateRanges();
414  onContentYChanged: rendererLoader.updateRanges();
415  onHeightChanged: rendererLoader.updateRanges();
416  onContentHeightChanged: rendererLoader.updateRanges();
417  }
418  Connections {
419  target: scopeView
420  onIsCurrentChanged: rendererLoader.updateRanges();
421  onVisibleToParentChanged: rendererLoader.updateRanges();
422  onWidthChanged: rendererLoader.clickScopeSizingHacks();
423  }
424  Connections {
425  target: holdingList
426  onMovingChanged: if (!moving) rendererLoader.updateRanges();
427  }
428 
429  function updateRanges() {
430  // Don't want to create stress by requesting more items during scope
431  // changes so unless you're not part of the visible scopes just return.
432  // For the visible scopes we need to do some work, the previously non visible
433  // scope needs to adjust its ranges so that we define the new visible range,
434  // that still means no creation/destruction of delegates, it's just about changing
435  // the culling of the items so they are actually visible
436  if (holdingList && holdingList.moving && !scopeView.visibleToParent) {
437  return;
438  }
439 
440  if (categoryView.moving) {
441  // Do not update the range if we are overshooting up or down, since we'll come back
442  // to the stable position and delete/create items without any reason
443  if (categoryView.contentY < categoryView.originY) {
444  return;
445  } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
446  categoryView.contentY + categoryView.height > categoryView.contentHeight) {
447  return;
448  }
449  }
450 
451  if (item && item.hasOwnProperty("displayMarginBeginning")) {
452  var buffer = wasCurrentOnMoveStart ? categoryView.height * 1.5 : 0;
453  var onViewport = baseItem.y + baseItem.height > 0 &&
454  baseItem.y < categoryView.height;
455  var onBufferViewport = baseItem.y + baseItem.height > -buffer &&
456  baseItem.y < categoryView.height + buffer;
457 
458  if (item.growsVertically) {
459  // A item view creates its delegates synchronously from
460  // -displayMarginBeginning
461  // to
462  // height + displayMarginEnd
463  // Around that area it adds the cacheBuffer area where delegates are created async
464  //
465  // We adjust displayMarginBeginning and displayMarginEnd so
466  // * In non visible scopes nothing is considered visible and we set cacheBuffer
467  // so that creates the items that would be in the viewport asynchronously
468  // * For the current scope set the visible range to the viewport and then
469  // use cacheBuffer to create extra items for categoryView.height * 1.5
470  // to make scrolling nicer by mantaining a higher number of
471  // cached items
472  // * For non current but visible scopes (i.e. when the user changes from one scope
473  // to the next, we set the visible range to the viewport so
474  // items are not culled (invisible) but still use no cacheBuffer
475  // (it will be set once the scope is the current one)
476  var displayMarginBeginning = baseItem.y + rendererLoader.anchors.topMargin;
477  displayMarginBeginning = -Math.max(-displayMarginBeginning, 0);
478  displayMarginBeginning = -Math.min(-displayMarginBeginning, baseItem.height);
479  displayMarginBeginning = Math.round(displayMarginBeginning);
480  var displayMarginEnd = -baseItem.height + seeAll.height + categoryView.height - baseItem.y;
481  displayMarginEnd = -Math.max(-displayMarginEnd, 0);
482  displayMarginEnd = -Math.min(-displayMarginEnd, baseItem.height);
483  displayMarginEnd = Math.round(displayMarginEnd);
484 
485  if (onBufferViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
486  item.displayMarginBeginning = displayMarginBeginning;
487  item.displayMarginEnd = displayMarginEnd;
488  if (holdingList && holdingList.moving) {
489  // If we are moving we need to reset the cache buffer of the
490  // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
491  // otherwise the cache buffer we had set to preload the items of the
492  // visible range will trigger some item creations and we want move to
493  // be as smooth as possible meaning no need creations
494  if (!wasCurrentOnMoveStart) {
495  item.cacheBuffer = 0;
496  }
497  } else {
498  // Protect us against cases where the item hasn't yet been positioned
499  if (!(categoryView.contentY === 0 && baseItem.y === 0 && index !== 0)) {
500  item.cacheBuffer = categoryView.height * 1.5;
501  }
502  }
503  } else {
504  var visibleRange = baseItem.height + displayMarginEnd + displayMarginBeginning;
505  if (visibleRange < 0) {
506  item.displayMarginBeginning = displayMarginBeginning;
507  item.displayMarginEnd = displayMarginEnd;
508  item.cacheBuffer = 0;
509  } else {
510  // This should be visibleRange/2 in each of the properties
511  // but some item views still (like GridView) like creating sync delegates even if
512  // the visible range is 0 so let's make sure the visible range is negative
513  item.displayMarginBeginning = displayMarginBeginning - visibleRange;
514  item.displayMarginEnd = displayMarginEnd - visibleRange;
515  item.cacheBuffer = visibleRange;
516  }
517  }
518  } else {
519  if (!onBufferViewport) {
520  // If not on the buffered viewport, don't load anything
521  item.displayMarginBeginning = 0;
522  item.displayMarginEnd = -item.innerWidth;
523  item.cacheBuffer = 0;
524  } else {
525  if (onViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
526  // If on the buffered viewport and the viewport and the on a visible scope
527  // Set displayMargin so that cards are rendered
528  // And if not moving the parent list also give it some extra asynchronously
529  // buffering
530  item.displayMarginBeginning = 0;
531  item.displayMarginEnd = 0;
532  if (holdingList && holdingList.moving) {
533  // If we are moving we need to reset the cache buffer of the
534  // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
535  // otherwise the cache buffer we had set to preload the items of the
536  // visible range will trigger some item creations and we want move to
537  // be as smooth as possible meaning no need creations
538  if (!wasCurrentOnMoveStart) {
539  item.cacheBuffer = 0;
540  }
541  } else {
542  item.cacheBuffer = baseItem.width * 1.5;
543  }
544  } else {
545  // If on the buffered viewport but either not in the real viewport
546  // or in the viewport of the non current scope, use displayMargin + cacheBuffer
547  // to render asynchronously the width of cards
548  item.displayMarginBeginning = 0;
549  item.displayMarginEnd = -item.innerWidth;
550  item.cacheBuffer = item.innerWidth;
551  }
552  }
553  }
554  }
555  }
556  }
557 
558  AbstractButton {
559  id: seeAll
560  objectName: "seeAll"
561  anchors {
562  top: rendererLoader.bottom
563  left: parent.left
564  right: parent.right
565  }
566  height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
567  visible: height != 0
568 
569  onClicked: {
570  if (categoryView.expandedCategoryId !== baseItem.category) {
571  categoryView.expandedCategoryId = baseItem.category;
572  floatingSeeLess.companionBase = baseItem;
573  } else {
574  categoryView.expandedCategoryId = "";
575  }
576  }
577 
578  Label {
579  id: seeAllLabel
580  text: baseItem.expanded ? i18n.tr("Show less") : i18n.tr("Show all")
581  anchors {
582  centerIn: parent
583  verticalCenterOffset: units.gu(-0.5)
584  }
585  fontSize: "small"
586  font.weight: Font.Bold
587  color: scopeStyle ? scopeStyle.foreground : theme.palette.normal.baseText
588  }
589  }
590 
591  Image {
592  visible: index != 0
593  anchors {
594  top: parent.top
595  left: parent.left
596  right: parent.right
597  }
598  fillMode: Image.Stretch
599  source: "graphics/dash_divider_top_lightgrad.png"
600  z: -1
601  }
602 
603  Image {
604  // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
605  visible: index != categoryView.model.count - 1
606  anchors {
607  bottom: seeAll.bottom
608  left: parent.left
609  right: parent.right
610  }
611  fillMode: Image.Stretch
612  source: "graphics/dash_divider_top_darkgrad.png"
613  z: -1
614  }
615  }
616 
617  sectionProperty: "name"
618  sectionDelegate: ListItems.Header {
619  objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
620  property int delegateIndex: -1
621  readonly property var delegate: categoryView.item(delegateIndex)
622  width: categoryView.width
623  height: text != "" ? units.gu(5) : 0
624  color: scopeStyle ? scopeStyle.foreground : theme.palette.normal.baseText
625  iconName: delegate && delegate.headerLink ? "go-next" : ""
626  onClicked: {
627  if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
628  }
629  }
630 
631  pageHeader: DashPageHeader {
632  objectName: "scopePageHeader"
633  width: parent.width
634  title: scopeView.scope ? scopeView.scope.name : ""
635  extraPanel: peExtraPanel
636  searchHistory: SearchHistoryModel
637  searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.ctr("Label: Hint for dash search line edit", "Search")
638  scopeHasFilters: scopeView.scope.filters != null
639  activeFiltersCount: scopeView.scope.activeFiltersCount
640  showBackButton: scopeView.hasBackAction
641  searchEntryEnabled: true
642  settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
643  favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
644  favorite: scopeView.scope && scopeView.scope.favorite
645  navigationTag: scopeView.scope ? scopeView.scope.primaryNavigationTag : ""
646  scopeStyle: scopeView.scopeStyle
647  paginationCount: scopeView.paginationCount
648  paginationIndex: scopeView.paginationIndex
649 
650  onBackClicked: scopeView.backClicked()
651  onSettingsClicked: subPageLoader.openSubPage("settings")
652  onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
653  onSearchTextFieldFocused: scopeView.showHeader()
654  onClearSearch: { // keepPanelOpen
655  var panelOpen = peExtraPanel.visible;
656  resetSearch(keepPanelOpen);
657  scopeView.scope.resetPrimaryNavigationTag();
658  peExtraPanel.resetNavigation();
659  if ((panelOpen || searchHistory.count > 0) && keepPanelOpen) {
660  openPopup();
661  }
662  }
663  onShowFiltersPopup: { // item
664  extraPanel.visible = false;
665  scopeView.filtersPopover = PopupUtils.open(Qt.resolvedUrl("FiltersPopover.qml"), item, { "contentWidth": scopeView.width - units.gu(2) } );
666  scopeView.filtersPopover.Component.onDestruction.connect(function () {
667  categoryView.pageHeader.closePopup(false, true);
668  categoryView.pageHeader.unfocus(true); // remove the focus from the search field
669  })
670  }
671  }
672 
673  PageHeaderExtraPanel {
674  id: peExtraPanel
675  objectName: "peExtraPanel"
676  width: parent.width >= units.gu(60) ? units.gu(40) : parent.width
677  anchors {
678  top: categoryView.pageHeader.bottom
679  topMargin: -categoryView.pageHeader.signatureLineHeight
680  }
681  z: 1
682  visible: false
683 
684  searchHistory: SearchHistoryModel
685  scope: scopeView.scope
686  windowHeight: scopeView.height
687 
688  onHistoryItemClicked: {
689  SearchHistoryModel.addQuery(text);
690  categoryView.pageHeader.searchQuery = text;
691  categoryView.pageHeader.unfocus();
692  }
693 
694  onDashNavigationLeafClicked: {
695  categoryView.pageHeader.closePopup();
696  categoryView.pageHeader.unfocus();
697  }
698 
699  onExtraPanelOptionSelected: {
700  categoryView.pageHeader.closePopup();
701  categoryView.pageHeader.unfocus();
702  }
703  }
704  }
705 
706  Item {
707  id: pullToRefreshClippingItem
708  anchors.left: parent.left
709  anchors.right: parent.right
710  anchors.bottom: parent.bottom
711  height: parent.height - pullToRefresh.contentY - categoryView.pageHeader.height
712  clip: true
713 
714  PullToRefresh {
715  id: pullToRefresh
716  objectName: "pullToRefresh"
717  target: categoryView
718 
719  readonly property real contentY: categoryView.contentY - categoryView.originY
720  y: -contentY - units.gu(5)
721 
722  onRefresh: {
723  refreshing = true
724  scopeView.scope.refresh()
725  }
726  anchors.left: parent.left
727  anchors.right: parent.right
728 
729  Connections {
730  target: scopeView
731  onProcessingChanged: if (!scopeView.processing) pullToRefresh.refreshing = false
732  }
733 
734  style: PullToRefreshScopeStyle {
735  anchors.fill: parent
736  activationThreshold: Math.min(units.gu(14), scopeView.height / 5)
737  }
738  }
739  }
740 
741  AbstractButton {
742  id: floatingSeeLess
743  objectName: "floatingSeeLess"
744 
745  property Item companionTo: companionBase ? companionBase.seeAllButton : null
746  property Item companionBase: null
747  property bool showBecausePosition: false
748  property real yOffset: 0
749 
750  anchors {
751  left: categoryView.left
752  right: categoryView.right
753  }
754  y: parent.height - height + yOffset
755  height: seeLessLabel.font.pixelSize + units.gu(4)
756  visible: companionTo && showBecausePosition
757 
758  onClicked: categoryView.expandedCategoryId = "";
759 
760  function updateVisibility() {
761  var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
762  showBecausePosition = companionPos.y > 0;
763 
764  var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
765  yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
766  yOffset = Math.min(yOffset, height);
767 
768  if (!showBecausePosition && categoryView.expandedCategoryId === "") {
769  companionBase = null;
770  }
771  }
772 
773  Label {
774  id: seeLessLabel
775  text: i18n.tr("Show less")
776  anchors {
777  centerIn: parent
778  verticalCenterOffset: units.gu(-0.5)
779  }
780  fontSize: "small"
781  font.weight: Font.Bold
782  color: scopeStyle ? scopeStyle.foreground : theme.palette.normal.baseText
783  }
784 
785  Connections {
786  target: floatingSeeLess.companionTo ? categoryView : null
787  onContentYChanged: floatingSeeLess.updateVisibility();
788  }
789 
790  Connections {
791  target: floatingSeeLess.companionTo
792  onYChanged: floatingSeeLess.updateVisibility();
793  }
794  }
795 
796  LimitProxyModel {
797  id: previewLimitModel
798  }
799 
800  Loader {
801  id: subPageLoader
802  objectName: "subPageLoader"
803  visible: x != width
804  width: parent.width
805  height: parent.height
806  anchors.left: categoryView.right
807 
808  property bool open: false
809  property var scope: scopeView.scope
810  property var scopeStyle: scopeView.scopeStyle
811  property int initialIndex: -1
812  property var previewModel;
813 
814  readonly property bool processing: item && item.processing || false
815  readonly property int count: item && item.count || 0
816  readonly property var currentItem: item && item.currentItem || null
817 
818  property string subPage: ""
819  readonly property bool subPageShown: visible && status === Loader.Ready
820 
821  function openSubPage(page) {
822  subPage = page;
823  }
824 
825  function closeSubPage() {
826  open = false;
827  }
828 
829  source: switch(subPage) {
830  case "preview": return "PreviewView.qml";
831  case "settings": return "ScopeSettingsPage.qml";
832  default: return "";
833  }
834 
835  onLoaded: {
836  item.scope = Qt.binding(function() { return subPageLoader.scope; } )
837  item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
838  if (subPage == "preview") {
839  item.open = Qt.binding(function() { return subPageLoader.open; } )
840  item.previewModel = subPageLoader.previewModel;
841  subPageLoader.previewModel = null;
842  }
843  open = true;
844  }
845 
846  onOpenChanged: categoryView.pageHeader.unfocus()
847 
848  onVisibleChanged: if (!visible) subPage = ""
849 
850  Connections {
851  target: subPageLoader.item
852  onBackClicked: subPageLoader.closeSubPage()
853  }
854  }
855 }