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