Unity 8
Drawer.qml
1 /*
2  * Copyright (C) 2016 Canonical, Ltd.
3  * Copyright (C) 2020 UBports Foundation.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 import QtQuick 2.4
19 import Ubuntu.Components 1.3
20 import Unity.Launcher 0.1
21 import Utils 0.1
22 import "../Components"
23 import Qt.labs.settings 1.0
24 import GSettings 1.0
25 import AccountsService 0.1
26 import QtGraphicalEffects 1.0
27 
28 FocusScope {
29  id: root
30 
31  property int panelWidth: 0
32  readonly property bool moving: (appList && appList.moving) ? true : false
33  readonly property Item searchTextField: searchField
34  readonly property real delegateWidth: units.gu(10)
35  property url background
36  visible: x > -width
37  property var fullyOpen: x === 0
38  property var fullyClosed: x === -width
39 
40  signal applicationSelected(string appId)
41 
42  // Request that the Drawer is opened fully, if it was partially closed then
43  // brought back
44  signal openRequested()
45 
46  // Request that the Drawer (and maybe its parent) is hidden, normally if
47  // the Drawer has been dragged away.
48  signal hideRequested()
49 
50  property bool allowSlidingAnimation: false
51  property bool draggingHorizontally: false
52  property int dragDistance: 0
53 
54  property var hadFocus: false
55  property var oldSelectionStart: null
56  property var oldSelectionEnd: null
57 
58  anchors {
59  onRightMarginChanged: {
60  if (fullyOpen && hadFocus) {
61  // See onDraggingHorizontallyChanged below
62  searchField.focus = hadFocus;
63  searchField.select(oldSelectionStart, oldSelectionEnd);
64  } else if (fullyClosed || fullyOpen) {
65  searchField.text = "";
66  resetOldFocus();
67  }
68  }
69  }
70 
71  Behavior on anchors.rightMargin {
72  enabled: allowSlidingAnimation && !draggingHorizontally
73  NumberAnimation {
74  duration: 300
75  easing.type: Easing.OutCubic
76  }
77  }
78 
79  onDraggingHorizontallyChanged: {
80  if (draggingHorizontally) {
81  // Remove (and put back using anchors.onRightMarginChanged) the
82  // focus for the searchfield in order to hide the copy/paste
83  // popover when we move the drawer
84  hadFocus = searchField.focus;
85  oldSelectionStart = searchField.selectionStart;
86  oldSelectionEnd = searchField.selectionEnd;
87  searchField.focus = false;
88  } else {
89  if (x < -units.gu(10)) {
90  hideRequested();
91  } else {
92  openRequested();
93  }
94  }
95  }
96 
97  Keys.onEscapePressed: {
98  root.hideRequested()
99  }
100 
101  onDragDistanceChanged: {
102  anchors.rightMargin = Math.max(-drawer.width, anchors.rightMargin + dragDistance);
103  }
104 
105  function resetOldFocus() {
106  hadFocus = false;
107  oldSelectionStart = null;
108  oldSelectionEnd = null;
109  appList.currentIndex = 0;
110  searchField.focus = false;
111  appList.focus = false;
112  }
113 
114  function focusInput() {
115  searchField.selectAll();
116  searchField.focus = true;
117  }
118 
119  function unFocusInput() {
120  searchField.focus = false;
121  }
122 
123  Keys.onPressed: {
124  if (event.text.trim() !== "") {
125  focusInput();
126  searchField.text = event.text;
127  }
128  switch (event.key) {
129  case Qt.Key_Right:
130  case Qt.Key_Left:
131  case Qt.Key_Down:
132  appList.focus = true;
133  break;
134  case Qt.Key_Up:
135  focusInput();
136  break;
137  }
138  // Catch all presses here in case the navigation lets something through
139  // We never want to end up in the launcher with focus
140  event.accepted = true;
141  }
142 
143  MouseArea {
144  anchors.fill: parent
145  hoverEnabled: true
146  acceptedButtons: Qt.AllButtons
147  onWheel: wheel.accepted = true
148  }
149 
150  Rectangle {
151  anchors.fill: parent
152  color: "#111111"
153  opacity: 0.99
154 
155  Wallpaper {
156  id: background
157  anchors.fill: parent
158  source: root.background
159  }
160 
161  FastBlur {
162  anchors.fill: background
163  source: background
164  radius: 64
165  cached: true
166  }
167 
168  // Images with fastblur can't use opacity, so we'll put this on top
169  Rectangle {
170  anchors.fill: background
171  color: parent.color
172  opacity: 0.67
173  }
174 
175  MouseArea {
176  id: drawerHandle
177  objectName: "drawerHandle"
178  anchors {
179  right: parent.right
180  top: parent.top
181  bottom: parent.bottom
182  }
183  width: units.gu(2)
184  property int oldX: 0
185 
186  onPressed: {
187  handle.active = true;
188  oldX = mouseX;
189  }
190  onMouseXChanged: {
191  var diff = oldX - mouseX;
192  root.draggingHorizontally |= diff > units.gu(2);
193  if (!root.draggingHorizontally) {
194  return;
195  }
196  root.dragDistance += diff;
197  oldX = mouseX
198  }
199  onReleased: reset()
200  onCanceled: reset()
201 
202  function reset() {
203  root.draggingHorizontally = false;
204  handle.active = false;
205  }
206 
207  Handle {
208  id: handle
209  anchors.fill: parent
210  active: parent.pressed
211  }
212  }
213 
214  AppDrawerModel {
215  id: appDrawerModel
216  }
217 
218  AppDrawerProxyModel {
219  id: sortProxyModel
220  source: appDrawerModel
221  filterString: searchField.displayText
222  sortBy: AppDrawerProxyModel.SortByAToZ
223  }
224 
225  Item {
226  id: contentContainer
227  anchors {
228  left: parent.left
229  right: drawerHandle.left
230  top: parent.top
231  bottom: parent.bottom
232  leftMargin: root.panelWidth
233  }
234 
235  Item {
236  id: searchFieldContainer
237  height: units.gu(4)
238  anchors { left: parent.left; top: parent.top; right: parent.right; margins: units.gu(1) }
239 
240  TextField {
241  id: searchField
242  objectName: "searchField"
243  inputMethodHints: Qt.ImhNoPredictiveText; //workaround to get the clear button enabled without the need of a space char event or change in focus
244  anchors {
245  left: parent.left
246  top: parent.top
247  right: parent.right
248  bottom: parent.bottom
249  }
250  placeholderText: i18n.tr("Search…")
251  z: 100
252 
253  KeyNavigation.down: appList
254 
255  onAccepted: {
256  if (searchField.displayText != "" && appList) {
257  // In case there is no currentItem (it might have been filtered away) lets reset it to the first item
258  if (!appList.currentItem) {
259  appList.currentIndex = 0;
260  }
261  root.applicationSelected(appList.getFirstAppId());
262  }
263  }
264  }
265  }
266 
267  DrawerGridView {
268  id: appList
269  objectName: "drawerAppList"
270  anchors {
271  left: parent.left
272  right: parent.right
273  top: searchFieldContainer.bottom
274  bottom: parent.bottom
275  }
276  height: rows * delegateHeight
277  clip: true
278 
279  model: sortProxyModel
280  delegateWidth: root.delegateWidth
281  delegateHeight: units.gu(11)
282  delegate: drawerDelegateComponent
283  onDraggingVerticallyChanged: {
284  if (draggingVertically) {
285  unFocusInput();
286  }
287  }
288 
289  refreshing: appDrawerModel.refreshing
290  onRefresh: {
291  appDrawerModel.refresh();
292  }
293  }
294  }
295 
296  Component {
297  id: drawerDelegateComponent
298  AbstractButton {
299  id: drawerDelegate
300  width: GridView.view.cellWidth
301  height: units.gu(11)
302  objectName: "drawerItem_" + model.appId
303 
304  readonly property bool focused: index === GridView.view.currentIndex && GridView.view.activeFocus
305 
306  onClicked: root.applicationSelected(model.appId)
307  onPressAndHold: {
308  if (model.appId.includes(".")) { // Open OpenStore page if app is a click
309  var splitAppId = model.appId.split("_");
310  Qt.openUrlExternally("https://open-store.io/app/" + model.appId.replace("_" + splitAppId[splitAppId.length-1],"") + "/");
311  }
312  }
313  z: loader.active ? 1 : 0
314 
315  Column {
316  width: units.gu(9)
317  anchors.horizontalCenter: parent.horizontalCenter
318  height: childrenRect.height
319  spacing: units.gu(1)
320 
321  UbuntuShape {
322  id: appIcon
323  width: units.gu(6)
324  height: 7.5 / 8 * width
325  anchors.horizontalCenter: parent.horizontalCenter
326  radius: "medium"
327  borderSource: 'undefined'
328  source: Image {
329  id: sourceImage
330  asynchronous: true
331  sourceSize.width: appIcon.width
332  source: model.icon
333  }
334  sourceFillMode: UbuntuShape.PreserveAspectCrop
335 
336  StyledItem {
337  styleName: "FocusShape"
338  anchors.fill: parent
339  StyleHints {
340  visible: drawerDelegate.focused
341  radius: units.gu(2.55)
342  }
343  }
344  }
345 
346  Label {
347  id: label
348  text: model.name
349  width: parent.width
350  anchors.horizontalCenter: parent.horizontalCenter
351  horizontalAlignment: Text.AlignHCenter
352  fontSize: "small"
353  wrapMode: Text.WordWrap
354  maximumLineCount: 2
355  elide: Text.ElideRight
356 
357  Loader {
358  id: loader
359  x: {
360  var aux = 0;
361  if (item) {
362  aux = label.width / 2 - item.width / 2;
363  var containerXMap = mapToItem(contentContainer, aux, 0).x
364  if (containerXMap < 0) {
365  aux = aux - containerXMap;
366  containerXMap = 0;
367  }
368  if (containerXMap + item.width > contentContainer.width) {
369  aux = aux - (containerXMap + item.width - contentContainer.width);
370  }
371  }
372  return aux;
373  }
374  y: -units.gu(0.5)
375  active: label.truncated && (drawerDelegate.hovered || drawerDelegate.focused)
376  sourceComponent: Rectangle {
377  color: UbuntuColors.jet
378  width: fullLabel.contentWidth + units.gu(1)
379  height: fullLabel.height + units.gu(1)
380  radius: units.dp(4)
381  Label {
382  id: fullLabel
383  width: Math.min(root.delegateWidth * 2, implicitWidth)
384  wrapMode: Text.Wrap
385  horizontalAlignment: Text.AlignHCenter
386  maximumLineCount: 3
387  elide: Text.ElideRight
388  anchors.centerIn: parent
389  text: model.name
390  fontSize: "small"
391  }
392  }
393  }
394  }
395  }
396  }
397  }
398  }
399 }