Unity 8
Drawer.qml
1 /*
2  * Copyright (C) 2016 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 Unity.Launcher 0.1
20 import Utils 0.1
21 import "../Components"
22 import Qt.labs.settings 1.0
23 import GSettings 1.0
24 
25 FocusScope {
26  id: root
27 
28  property int panelWidth: 0
29  readonly property bool moving: listLoader.item && listLoader.item.moving
30  readonly property Item searchTextField: searchField
31  readonly property real delegateWidth: units.gu(10)
32 
33  signal applicationSelected(string appId)
34 
35  property bool draggingHorizontally: false
36  property int dragDistance: 0
37 
38  onFocusChanged: {
39  if (focus) {
40  searchField.selectAll();
41  }
42  }
43 
44  function focusInput() {
45  searchField.selectAll();
46  searchField.focus = true;
47  }
48 
49  Keys.onPressed: {
50  if (event.text.trim() !== "") {
51  focusInput();
52  searchField.text = event.text;
53  }
54  // Catch all presses here in case the navigation lets something through
55  // We never want to end up in the launcher with focus
56  event.accepted = true;
57  }
58 
59  Settings {
60  property alias selectedTab: sections.selectedIndex
61  }
62 
63  MouseArea {
64  anchors.fill: parent
65  hoverEnabled: true
66  acceptedButtons: Qt.AllButtons
67  onWheel: wheel.accepted = true
68  }
69 
70  Rectangle {
71  anchors.fill: parent
72  color: "#BF000000"
73 
74  AppDrawerModel {
75  id: appDrawerModel
76  }
77 
78  AppDrawerProxyModel {
79  id: sortProxyModel
80  source: appDrawerModel
81  filterString: searchField.displayText
82  sortBy: AppDrawerProxyModel.SortByAToZ
83  }
84 
85  Item {
86  id: contentContainer
87  anchors.fill: parent
88  anchors.leftMargin: root.panelWidth
89 
90  TextField {
91  id: searchField
92  objectName: "searchField"
93  anchors { left: parent.left; top: parent.top; right: parent.right; margins: units.gu(1) }
94  placeholderText: i18n.tr("Search…")
95  focus: true
96 
97  KeyNavigation.down: sections
98 
99  onAccepted: {
100  if (searchField.displayText != "" && listLoader.item) {
101  // In case there is no currentItem (it might have been filtered away) lets reset it to the first item
102  if (!listLoader.item.currentItem) {
103  listLoader.item.currentIndex = 0;
104  }
105  root.applicationSelected(listLoader.item.getFirstAppId());
106  }
107  }
108  }
109 
110  Item {
111  id: sectionsContainer
112  anchors { left: parent.left; top: searchField.bottom; right: parent.right; }
113  height: sections.height
114  clip: true
115  z: 2
116 
117  Sections {
118  id: sections
119  objectName: "drawerSections"
120  width: parent.width
121 
122  KeyNavigation.up: searchField
123  KeyNavigation.down: headerFocusScope
124  KeyNavigation.backtab: searchField
125  KeyNavigation.tab: headerFocusScope
126 
127  actions: [
128  Action {
129  text: i18n.ctr("Apps sorted alphabetically", "A-Z")
130  // TODO: Disabling this for now as we don't get the right input from u-a-l yet.
131 // },
132 // Action {
133 // text: i18n.ctr("Most used apps", "Most used")
134  }
135  ]
136 
137  Rectangle {
138  anchors.bottom: parent.bottom
139  height: units.dp(1)
140  color: 'gray'
141  width: contentContainer.width
142  }
143  }
144  }
145 
146  FocusScope {
147  id: headerFocusScope
148  objectName: "headerFocusScope"
149  KeyNavigation.up: sections
150  KeyNavigation.down: listLoader.item
151  KeyNavigation.backtab: sections
152  KeyNavigation.tab: listLoader.item
153  activeFocusOnTab: true
154 
155  GSettings {
156  id: settings
157  schema.id: "com.canonical.Unity8"
158  }
159 
160  Keys.onPressed: {
161  switch (event.key) {
162  case Qt.Key_Return:
163  case Qt.Key_Enter:
164  case Qt.Key_Space:
165  trigger();
166  event.accepted = true;
167  }
168  }
169 
170  function trigger() {
171  Qt.openUrlExternally(settings.appstoreUri)
172  }
173  }
174 
175  Loader {
176  id: listLoader
177  objectName: "drawerListLoader"
178  anchors { left: parent.left; top: sectionsContainer.bottom; right: parent.right; bottom: parent.bottom }
179 
180  KeyNavigation.up: headerFocusScope
181  KeyNavigation.down: searchField
182  KeyNavigation.backtab: headerFocusScope
183  KeyNavigation.tab: searchField
184 
185  sourceComponent: {
186  switch (sections.selectedIndex) {
187  case 0: return aToZComponent;
188  case 1: return mostUsedComponent;
189  }
190  }
191  Binding {
192  target: listLoader.item || null
193  property: "objectName"
194  value: "drawerItemList"
195  }
196  }
197 
198  MouseArea {
199  parent: listLoader.item ? listLoader.item : null
200  anchors.fill: parent
201  propagateComposedEvents: true
202  property int oldX: 0
203  onPressed: {
204  oldX = mouseX;
205  }
206  onMouseXChanged: {
207  var diff = oldX - mouseX;
208  root.draggingHorizontally |= diff > units.gu(2);
209  if (!root.draggingHorizontally) {
210  return;
211  }
212  propagateComposedEvents = false;
213  parent.interactive = false;
214  root.dragDistance += diff;
215  oldX = mouseX
216  }
217  onReleased: reset();
218  onCanceled: reset();
219  function reset() {
220  if (root.draggingHorizontally) {
221  root.draggingHorizontally = false;
222  parent.interactive = true;
223  }
224  reactivateTimer.start();
225  }
226 
227  Timer {
228  id: reactivateTimer
229  interval: 0
230  onTriggered: parent.propagateComposedEvents = true;
231  }
232  }
233 
234  Component {
235  id: mostUsedComponent
236  DrawerListView {
237  id: mostUsedListView
238 
239  header: MoreAppsHeader {
240  width: parent.width
241  height: units.gu(6)
242  highlighted: headerFocusScope.activeFocus
243  onClicked: headerFocusScope.trigger();
244  }
245 
246  model: AppDrawerProxyModel {
247  source: sortProxyModel
248  group: AppDrawerProxyModel.GroupByAll
249  sortBy: AppDrawerProxyModel.SortByUsage
250  dynamicSortFilter: false
251  }
252 
253  delegate: UbuntuShape {
254  width: parent.width - units.gu(2)
255  anchors.horizontalCenter: parent.horizontalCenter
256  color: "#20ffffff"
257  aspect: UbuntuShape.Flat
258  // NOTE: Cannot use gridView.rows here as it would evaluate to 0 at first and only update later,
259  // which messes up the ListView.
260  height: (Math.ceil(mostUsedGridView.model.count / mostUsedGridView.columns) * mostUsedGridView.delegateHeight) + units.gu(2)
261 
262  readonly property string appId: model.appId
263 
264  DrawerGridView {
265  id: mostUsedGridView
266  anchors.fill: parent
267  topMargin: units.gu(1)
268  bottomMargin: units.gu(1)
269  clip: true
270 
271  interactive: true
272  focus: index == mostUsedListView.currentIndex
273 
274  model: sortProxyModel
275 
276  delegateWidth: root.delegateWidth
277  delegateHeight: units.gu(10)
278  delegate: drawerDelegateComponent
279  }
280  }
281  }
282  }
283 
284  Component {
285  id: aToZComponent
286  DrawerListView {
287  id: aToZListView
288 
289  header: MoreAppsHeader {
290  width: parent.width
291  height: units.gu(6)
292  highlighted: headerFocusScope.activeFocus
293  onClicked: headerFocusScope.trigger();
294  }
295 
296  model: AppDrawerProxyModel {
297  source: sortProxyModel
298  sortBy: AppDrawerProxyModel.SortByAToZ
299  group: AppDrawerProxyModel.GroupByAToZ
300  dynamicSortFilter: false
301  }
302 
303  delegate: UbuntuShape {
304  width: parent.width - units.gu(2)
305  anchors.horizontalCenter: parent.horizontalCenter
306  color: "#20ffffff"
307  aspect: UbuntuShape.Flat
308 
309  readonly property string appId: model.appId
310 
311  // NOTE: Cannot use gridView.rows here as it would evaluate to 0 at first and only update later,
312  // which messes up the ListView.
313  height: (Math.ceil(gridView.model.count / gridView.columns) * gridView.delegateHeight) +
314  categoryNameLabel.implicitHeight + units.gu(2)
315 
316  Label {
317  id: categoryNameLabel
318  anchors { left: parent.left; top: parent.top; right: parent.right; margins: units.gu(1) }
319  text: model.letter
320  }
321 
322  DrawerGridView {
323  id: gridView
324  anchors { left: parent.left; top: categoryNameLabel.bottom; right: parent.right; topMargin: units.gu(1) }
325  height: rows * delegateHeight
326 
327  interactive: true
328  focus: index == aToZListView.currentIndex
329 
330  model: AppDrawerProxyModel {
331  id: categoryModel
332  source: sortProxyModel
333  filterLetter: model.letter
334  dynamicSortFilter: false
335  }
336  delegateWidth: root.delegateWidth
337  delegateHeight: units.gu(10)
338  delegate: drawerDelegateComponent
339  }
340  }
341  }
342  }
343  }
344 
345  Component {
346  id: drawerDelegateComponent
347  AbstractButton {
348  id: drawerDelegate
349  width: GridView.view.cellWidth
350  height: units.gu(10)
351  objectName: "drawerItem_" + model.appId
352 
353  readonly property bool focused: index === GridView.view.currentIndex && GridView.view.activeFocus
354 
355  onClicked: root.applicationSelected(model.appId)
356  z: loader.active ? 1 : 0
357 
358  Column {
359  width: units.gu(8)
360  anchors.horizontalCenter: parent.horizontalCenter
361  height: childrenRect.height
362  spacing: units.gu(1)
363 
364  UbuntuShape {
365  id: appIcon
366  width: units.gu(6)
367  height: 7.5 / 8 * width
368  anchors.horizontalCenter: parent.horizontalCenter
369  radius: "medium"
370  borderSource: 'undefined'
371  source: Image {
372  id: sourceImage
373  sourceSize.width: appIcon.width
374  source: model.icon
375  }
376  sourceFillMode: UbuntuShape.PreserveAspectCrop
377 
378  StyledItem {
379  styleName: "FocusShape"
380  anchors.fill: parent
381  StyleHints {
382  visible: drawerDelegate.focused
383  radius: units.gu(2.55)
384  }
385  }
386  }
387 
388  Label {
389  id: label
390  text: model.name
391  width: parent.width
392  horizontalAlignment: Text.AlignHCenter
393  fontSize: "small"
394  elide: Text.ElideRight
395 
396  Loader {
397  id: loader
398  x: {
399  var aux = 0;
400  if (item) {
401  aux = label.width / 2 - item.width / 2;
402  var containerXMap = mapToItem(contentContainer, aux, 0).x
403  if (containerXMap < 0) {
404  aux = aux - containerXMap;
405  containerXMap = 0;
406  }
407  if (containerXMap + item.width > contentContainer.width) {
408  aux = aux - (containerXMap + item.width - contentContainer.width);
409  }
410  }
411  return aux;
412  }
413  y: -units.gu(0.5)
414  active: label.truncated && (drawerDelegate.hovered || drawerDelegate.focused)
415  sourceComponent: Rectangle {
416  color: UbuntuColors.jet
417  width: fullLabel.contentWidth + units.gu(1)
418  height: fullLabel.height + units.gu(1)
419  radius: units.dp(4)
420  Label {
421  id: fullLabel
422  width: Math.min(root.delegateWidth * 2, implicitWidth)
423  wrapMode: Text.Wrap
424  horizontalAlignment: Text.AlignHCenter
425  maximumLineCount: 3
426  elide: Text.ElideRight
427  anchors.centerIn: parent
428  text: model.name
429  fontSize: "small"
430  }
431  }
432  }
433  }
434  }
435  }
436  }
437  }
438 }