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