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