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