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