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