Unity 8
 All Classes Functions
GenericScopeView.qml
1 /*
2  * Copyright (C) 2013-2014 Canonical, Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.0
18 import Ubuntu.Components 1.1
19 import Utils 0.1
20 import Unity 0.2
21 import Dash 0.1
22 import "../Components"
23 import "../Components/ListItems" as ListItems
24 
25 FocusScope {
26  id: scopeView
27 
28  readonly property bool navigationShown: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].openList : false
29  property bool forceNonInteractive: false
30  property var scope: null
31  property SortFilterProxyModel categories: categoryFilter
32  property bool isCurrent: false
33  property alias moving: categoryView.moving
34  property bool hasBackAction: false
35  property bool enableHeightBehaviorOnNextCreation: false
36  property var categoryView: categoryView
37  property bool showPageHeader: true
38  readonly property alias subPageShown: subPageLoader.subPageShown
39  property int paginationCount: 0
40  property int paginationIndex: 0
41  property alias pageHeaderTotallyVisible: categoryView.pageHeaderTotallyVisible
42  property var holdingList: null
43 
44  property var scopeStyle: ScopeStyle {
45  style: scope ? scope.customizations : {}
46  }
47 
48  readonly property bool processing: scope ? scope.searchInProgress || subPageLoader.processing : false
49 
50  signal backClicked()
51 
52  onScopeChanged: {
53  floatingSeeLess.companionBase = null;
54  }
55 
56  function positionAtBeginning() {
57  categoryView.positionAtBeginning()
58  }
59 
60  function showHeader() {
61  categoryView.showHeader()
62  }
63 
64  function closePreview() {
65  subPageLoader.closeSubPage()
66  }
67 
68  function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount) {
69  if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope") {
70  // TODO Technically it is possible that calling activate() will make the scope emit
71  // previewRequested so that we show a preview but there's no scope that does that yet
72  // so it's not implemented
73  scope.activate(result)
74  } else {
75  if (scope.preview(result)) {
76  openPreview(index, resultsModel, limitedCategoryItemCount);
77  }
78  }
79  }
80 
81  function itemPressedAndHeld(index, result, itemModel, resultsModel, limitedCategoryItemCount) {
82  if (itemModel.uri.indexOf("scope://") !== 0) {
83  if (scope.preview(result)) {
84  openPreview(index, resultsModel, limitedCategoryItemCount);
85  }
86  }
87  }
88 
89  function openPreview(index, resultsModel, limitedCategoryItemCount) {
90  if (limitedCategoryItemCount > 0) {
91  previewLimitModel.model = resultsModel;
92  previewLimitModel.limit = limitedCategoryItemCount;
93  subPageLoader.model = previewLimitModel;
94  } else {
95  subPageLoader.model = resultsModel;
96  }
97  subPageLoader.initialIndex = -1;
98  subPageLoader.initialIndex = index;
99  subPageLoader.openSubPage("preview");
100  }
101 
102  Binding {
103  target: scope
104  property: "isActive"
105  value: isCurrent && !subPageLoader.open
106  }
107 
108  SortFilterProxyModel {
109  id: categoryFilter
110  model: scope ? scope.categories : null
111  dynamicSortFilter: true
112  filterRole: Categories.RoleCount
113  filterRegExp: /^0$/
114  invertMatch: true
115  }
116 
117  onIsCurrentChanged: {
118  if (pageHeaderLoader.item && showPageHeader) {
119  pageHeaderLoader.item.resetSearch();
120  }
121  subPageLoader.closeSubPage();
122  }
123 
124  Binding {
125  target: scopeView.scope
126  property: "searchQuery"
127  value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
128  when: isCurrent && showPageHeader
129  }
130 
131  Binding {
132  target: pageHeaderLoader.item
133  property: "searchQuery"
134  value: scopeView.scope ? scopeView.scope.searchQuery : ""
135  when: isCurrent && showPageHeader
136  }
137 
138  Connections {
139  target: scopeView.scope
140  onShowDash: subPageLoader.closeSubPage()
141  onHideDash: subPageLoader.closeSubPage()
142  }
143 
144  Rectangle {
145  anchors.fill: parent
146  color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
147  visible: color != "transparent"
148  }
149 
150  ScopeListView {
151  id: categoryView
152  objectName: "categoryListView"
153  interactive: !forceNonInteractive
154 
155  x: subPageLoader.open ? -width : 0
156  visible: x != -width
157  Behavior on x { UbuntuNumberAnimation { } }
158  width: parent.width
159  height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
160  : parent.height
161  clip: height != parent.height
162 
163  model: scopeView.categories
164  forceNoClip: subPageLoader.open
165  pixelAligned: true
166 
167  property string expandedCategoryId: ""
168  property int runMaximizeAfterSizeChanges: 0
169 
170  readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
171  ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
172 
173  onExpandedCategoryIdChanged: {
174  var firstCreated = firstCreatedIndex();
175  var shrinkingAny = false;
176  var shrinkHeightDifference = 0;
177  for (var i = 0; i < createdItemCount(); ++i) {
178  var baseItem = item(firstCreated + i);
179  if (baseItem.expandable) {
180  var shouldExpand = baseItem.category === expandedCategoryId;
181  if (shouldExpand != baseItem.expanded) {
182  var animate = false;
183  if (!subPageLoader.open) {
184  var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
185  var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
186  animate = shrinkingAny || animateShrinking || animateGrowing;
187  }
188 
189  if (!shouldExpand) {
190  shrinkingAny = true;
191  shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
192  }
193 
194  if (shouldExpand && !subPageLoader.open) {
195  if (!shrinkingAny) {
196  categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
197  } else {
198  // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
199  // after the shrink/grow animation ends
200  var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
201  if (growHeightDifference > shrinkHeightDifference) {
202  runMaximizeAfterSizeChanges = 2;
203  } else {
204  runMaximizeAfterSizeChanges = 0;
205  }
206  }
207  }
208 
209  baseItem.expand(shouldExpand, animate);
210  }
211  }
212  }
213  }
214 
215  delegate: DashCategoryBase {
216  id: baseItem
217  objectName: "dashCategory" + category
218 
219  property Item seeAllButton: seeAll
220 
221  readonly property bool expandable: {
222  if (categoryView.model.count === 1) return false;
223  if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
224  if (item && item.expandedHeight > item.collapsedHeight) return true;
225  return false;
226  }
227  property bool expanded: false
228  readonly property string category: categoryId
229  readonly property string headerLink: model.headerLink
230  readonly property var item: rendererLoader.item
231 
232  function expand(expand, animate) {
233  heightBehaviour.enabled = animate;
234  expanded = expand;
235  }
236 
237  CardTool {
238  id: cardTool
239  objectName: "cardTool"
240  count: results ? results.count : 0
241  template: model.renderer
242  components: model.components
243  viewWidth: parent.width
244  }
245 
246  onExpandableChanged: {
247  // This can happen with the VJ that doesn't know how height it will be on creation
248  // so doesn't set expandable until a bit too late for onLoaded
249  if (expandable) {
250  var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
251  baseItem.expand(shouldExpand, false /*animate*/);
252  }
253  }
254 
255  onHeightChanged: rendererLoader.updateRanges();
256  onYChanged: rendererLoader.updateRanges();
257 
258  Loader {
259  id: rendererLoader
260  anchors {
261  top: parent.top
262  left: parent.left
263  right: parent.right
264  topMargin: name != "" ? 0 : units.gu(2)
265  }
266 
267  Behavior on height {
268  id: heightBehaviour
269  enabled: false
270  animation: UbuntuNumberAnimation {
271  duration: UbuntuAnimation.FastDuration
272  onRunningChanged: {
273  if (!running) {
274  heightBehaviour.enabled = false
275  if (categoryView.runMaximizeAfterSizeChanges > 0) {
276  categoryView.runMaximizeAfterSizeChanges--;
277  if (categoryView.runMaximizeAfterSizeChanges == 0) {
278  var firstCreated = categoryView.firstCreatedIndex();
279  for (var i = 0; i < categoryView.createdItemCount(); ++i) {
280  var baseItem = categoryView.item(firstCreated + i);
281  if (baseItem.category === categoryView.expandedCategoryId) {
282  categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
283  break;
284  }
285  }
286  }
287  }
288  }
289  }
290  }
291  }
292 
293  readonly property bool expanded: baseItem.expanded || !baseItem.expandable
294  height: expanded ? item.expandedHeight : item.collapsedHeight
295 
296  source: {
297  switch (cardTool.categoryLayout) {
298  case "carousel": return "CardCarousel.qml";
299  case "vertical-journal": return "CardVerticalJournal.qml";
300  case "horizontal-list": return "CardHorizontalList.qml";
301  case "grid":
302  default: return "CardGrid.qml";
303  }
304  }
305 
306  onLoaded: {
307  if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
308  item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
309  scopeView.enableHeightBehaviorOnNextCreation = false;
310  }
311  item.model = Qt.binding(function() { return results })
312  item.objectName = Qt.binding(function() { return categoryId })
313  item.scopeStyle = scopeView.scopeStyle;
314  if (baseItem.expandable) {
315  var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
316  baseItem.expand(shouldExpand, false /*animate*/);
317  }
318  updateRanges();
319  if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
320  // Yeah, hackish :/
321  cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
322  }
323  item.cardTool = cardTool;
324  }
325 
326  Component.onDestruction: {
327  if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
328  scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
329  }
330  }
331 
332  Connections {
333  target: rendererLoader.item
334  onClicked: {
335  scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount());
336  }
337 
338  onPressAndHold: {
339  scopeView.itemPressedAndHeld(index, result, itemModel, target.model, categoryItemCount());
340  }
341 
342  function categoryItemCount() {
343  var categoryItemCount = -1;
344  if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
345  categoryItemCount = target.collapsedItemCount;
346  }
347  return categoryItemCount;
348  }
349  }
350  Connections {
351  target: categoryView
352  onOriginYChanged: rendererLoader.updateRanges();
353  onContentYChanged: rendererLoader.updateRanges();
354  onHeightChanged: rendererLoader.updateRanges();
355  onContentHeightChanged: rendererLoader.updateRanges();
356  }
357  Connections {
358  target: scopeView
359  onIsCurrentChanged: rendererLoader.updateRanges();
360  }
361  Connections {
362  target: holdingList
363  onMovingChanged: if (!moving) rendererLoader.updateRanges();
364  }
365 
366  function updateRanges() {
367  if (holdingList && holdingList.moving) {
368  return;
369  }
370 
371  if (categoryView.moving) {
372  // Do not update the range if we are overshooting up or down, since we'll come back
373  // to the stable position and delete/create items without any reason
374  if (categoryView.contentY < categoryView.originY) {
375  return;
376  } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
377  categoryView.contentY + categoryView.height > categoryView.contentHeight) {
378  return;
379  }
380  }
381 
382  if (item && item.hasOwnProperty("visibleRangeBegin")) {
383  item.visibleRangeBegin = Math.max(-baseItem.y, 0)
384  item.visibleRangeEnd = item.visibleRangeBegin + Math.min(categoryView.height, rendererLoader.height)
385  }
386 
387  if (item && item.hasOwnProperty("displayMarginBeginning")) {
388  // TODO do we need item.originY here, test 1300302 once we have a silo
389  // and we can run it on the phone
390  if (scopeView.isCurrent) {
391  // 1073741823 is s^30 -1. A quite big number so that you have "infinite" display margin, but not so
392  // big so that if you add if with itself you're outside the 2^31 int range
393  item.displayMarginBeginning = 1073741823;
394  item.displayMarginEnd = 1073741823;
395  } else if (baseItem.y + baseItem.height <= 0) {
396  // Not visible (item at top of the list viewport)
397  item.displayMarginBeginning = -baseItem.height;
398  item.displayMarginEnd = 0;
399  } else if (baseItem.y >= categoryView.height) {
400  // Not visible (item at bottom of the list viewport)
401  item.displayMarginBeginning = 0;
402  item.displayMarginEnd = -baseItem.height;
403  } else {
404  item.displayMarginBeginning = Math.round(-Math.max(-baseItem.y, 0));
405  item.displayMarginEnd = -Math.round(Math.max(baseItem.height - seeAll.height -
406  categoryView.height + baseItem.y, 0));
407  }
408  }
409  }
410  }
411 
412  AbstractButton {
413  id: seeAll
414  objectName: "seeAll"
415  anchors {
416  top: rendererLoader.bottom
417  left: parent.left
418  right: parent.right
419  }
420  height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
421  visible: height != 0
422 
423  onClicked: {
424  if (categoryView.expandedCategoryId !== baseItem.category) {
425  categoryView.expandedCategoryId = baseItem.category;
426  floatingSeeLess.companionBase = baseItem;
427  } else {
428  categoryView.expandedCategoryId = "";
429  }
430  }
431 
432  Label {
433  id: seeAllLabel
434  text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
435  anchors {
436  centerIn: parent
437  verticalCenterOffset: units.gu(-0.5)
438  }
439  fontSize: "small"
440  font.weight: Font.Bold
441  color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
442  }
443  }
444 
445  Image {
446  visible: index != 0
447  anchors {
448  top: parent.top
449  left: parent.left
450  right: parent.right
451  }
452  fillMode: Image.Stretch
453  source: "graphics/dash_divider_top_lightgrad.png"
454  z: -1
455  }
456 
457  Image {
458  // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
459  visible: index != categoryView.model.count - 1
460  anchors {
461  bottom: seeAll.bottom
462  left: parent.left
463  right: parent.right
464  }
465  fillMode: Image.Stretch
466  source: "graphics/dash_divider_top_darkgrad.png"
467  z: -1
468  }
469  }
470 
471  sectionProperty: "name"
472  sectionDelegate: ListItems.Header {
473  objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
474  readonly property var delegate: categoryView.item(delegateIndex)
475  width: categoryView.width
476  height: section != "" ? units.gu(5) : 0
477  text: section
478  color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
479  iconName: delegate && delegate.headerLink ? "go-next" : ""
480  onClicked: {
481  if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
482  }
483  }
484 
485  pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
486  Loader {
487  id: pageHeaderLoader
488  width: parent.width
489  sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
490  Component {
491  id: pageHeaderComponent
492  PageHeader {
493  objectName: "scopePageHeader"
494  width: parent.width
495  title: scopeView.scope ? scopeView.scope.name : ""
496  searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
497  showBackButton: scopeView.hasBackAction
498  searchEntryEnabled: true
499  settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
500  favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
501  favorite: scopeView.scope && scopeView.scope.favorite
502  scopeStyle: scopeView.scopeStyle
503  paginationCount: scopeView.paginationCount
504  paginationIndex: scopeView.paginationIndex
505 
506  bottomItem: DashNavigation {
507  scope: scopeView.scope
508  anchors { left: parent.left; right: parent.right }
509  windowHeight: scopeView.height
510  windowWidth: scopeView.width
511  scopeStyle: scopeView.scopeStyle
512  }
513 
514  onBackClicked: scopeView.backClicked()
515  onSettingsClicked: subPageLoader.openSubPage("settings")
516  onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
517  }
518  }
519  }
520  }
521 
522  Item {
523  id: pullToRefreshClippingItem
524  anchors.left: parent.left
525  anchors.right: parent.right
526  anchors.bottom: parent.bottom
527  height: parent.height - pullToRefresh.contentY + (pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].height - pageHeaderLoader.item.height : 0)
528  clip: true
529 
530  PullToRefresh {
531  id: pullToRefresh
532  objectName: "pullToRefresh"
533  target: categoryView
534 
535  readonly property real contentY: categoryView.contentY - categoryView.originY
536  y: -contentY - units.gu(5)
537 
538  onRefresh: {
539  refreshing = true
540  scopeView.scope.refresh()
541  }
542  anchors.left: parent.left
543  anchors.right: parent.right
544 
545  Connections {
546  target: scopeView
547  onProcessingChanged: if (!scopeView.processing) pullToRefresh.refreshing = false
548  }
549 
550  style: PullToRefreshScopeStyle {
551  anchors.fill: parent
552  activationThreshold: units.gu(14)
553  }
554  }
555  }
556 
557  AbstractButton {
558  id: floatingSeeLess
559  objectName: "floatingSeeLess"
560 
561  property Item companionTo: companionBase ? companionBase.seeAllButton : null
562  property Item companionBase: null
563  property bool showBecausePosition: false
564  property real yOffset: 0
565 
566  anchors {
567  left: categoryView.left
568  right: categoryView.right
569  }
570  y: parent.height - height + yOffset
571  height: seeLessLabel.font.pixelSize + units.gu(4)
572  visible: companionTo && showBecausePosition
573 
574  onClicked: categoryView.expandedCategoryId = "";
575 
576  function updateVisibility() {
577  var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
578  showBecausePosition = companionPos.y > 0;
579 
580  var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
581  yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
582  yOffset = Math.min(yOffset, height);
583 
584  if (!showBecausePosition && categoryView.expandedCategoryId === "") {
585  companionBase = null;
586  }
587  }
588 
589  Label {
590  id: seeLessLabel
591  text: i18n.tr("See less")
592  anchors {
593  centerIn: parent
594  verticalCenterOffset: units.gu(-0.5)
595  }
596  fontSize: "small"
597  font.weight: Font.Bold
598  color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
599  }
600 
601  Connections {
602  target: floatingSeeLess.companionTo ? categoryView : null
603  onContentYChanged: floatingSeeLess.updateVisibility();
604  }
605 
606  Connections {
607  target: floatingSeeLess.companionTo
608  onYChanged: floatingSeeLess.updateVisibility();
609  }
610  }
611 
612  LimitProxyModel {
613  id: previewLimitModel
614  }
615 
616  Loader {
617  id: subPageLoader
618  objectName: "subPageLoader"
619  visible: x != width
620  width: parent.width
621  height: parent.height
622  anchors.left: categoryView.right
623 
624  property bool open: false
625  property var scope: scopeView.scope
626  property var scopeStyle: scopeView.scopeStyle
627  property int initialIndex: -1
628  property var model: null
629 
630  readonly property bool processing: item && item.processing || false
631  readonly property int count: item && item.count || 0
632  readonly property int currentIndex: item && item.currentIndex || 0
633  readonly property var currentItem: item && item.currentItem || null
634 
635  property string subPage: ""
636  readonly property bool subPageShown: visible && status === Loader.Ready
637 
638  function openSubPage(page) {
639  subPage = page;
640  }
641 
642  function closeSubPage() {
643  open = false;
644  }
645 
646  source: switch(subPage) {
647  case "preview": return "PreviewListView.qml";
648  case "settings": return "ScopeSettingsPage.qml";
649  default: return "";
650  }
651 
652  onLoaded: {
653  item.scope = Qt.binding(function() { return subPageLoader.scope; } )
654  item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
655  if (subPage == "preview") {
656  item.open = Qt.binding(function() { return subPageLoader.open; } )
657  item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
658  item.model = Qt.binding(function() { return subPageLoader.model; } )
659  }
660  open = true;
661  }
662 
663  onOpenChanged: pageHeaderLoader.item.unfocus()
664 
665  onVisibleChanged: if (!visible) subPage = ""
666 
667  Connections {
668  target: subPageLoader.item
669  onBackClicked: subPageLoader.closeSubPage()
670  }
671  }
672 }