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