Lomiri
PanelBar.qml
1 /*
2  * Copyright (C) 2014 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 Lomiri.Components 1.3
20 import "../Components"
21 
22 Item {
23  id: root
24  property alias expanded: row.expanded
25  property alias interactive: flickable.interactive
26  property alias model: row.model
27  property alias unitProgress: row.unitProgress
28  property alias enableLateralChanges: row.enableLateralChanges
29  property alias overFlowWidth: row.overFlowWidth
30  readonly property alias currentItemIndex: row.currentItemIndex
31  property real lateralPosition: -1
32  property int alignment: Qt.AlignRight
33  readonly property int rowContentX: row.contentX
34 
35  property alias hideRow: row.hideRow
36  property alias rowItemDelegate: row.delegate
37 
38  implicitWidth: flickable.contentWidth
39 
40  function selectItemAt(lateralPosition) {
41  if (!expanded) {
42  row.resetCurrentItem();
43  }
44  var mapped = root.mapToItem(row, lateralPosition, 0);
45  row.selectItemAt(mapped.x);
46  }
47 
48  function selectPreviousItem() {
49  if (!expanded) {
50  row.resetCurrentItem();
51  }
52  row.selectPreviousItem();
53  d.alignIndicators();
54  }
55 
56  function selectNextItem() {
57  if (!expanded) {
58  row.resetCurrentItem();
59  }
60  row.selectNextItem();
61  d.alignIndicators();
62  }
63 
64  function setCurrentItemIndex(index) {
65  if (!expanded) {
66  row.resetCurrentItem();
67  }
68  row.setCurrentItemIndex(index);
69  d.alignIndicators();
70  }
71 
72  function addScrollOffset(scrollAmmout) {
73  if (root.alignment == Qt.AlignLeft) {
74  scrollAmmout = -scrollAmmout;
75  }
76 
77  if (scrollAmmout < 0) { // left scroll
78  if (flickable.contentX + flickable.width > row.width) return; // already off the left.
79 
80  if (flickable.contentX + flickable.width - scrollAmmout > row.width) { // going to be off the left
81  scrollAmmout = (flickable.contentX + flickable.width) - row.width;
82  }
83  } else { // right scroll
84  if (flickable.contentX < 0) return; // already off the right.
85  if (flickable.contentX - scrollAmmout < 0) { // going to be off the right
86  scrollAmmout = flickable.contentX;
87  }
88  }
89  d.scrollOffset = d.scrollOffset + scrollAmmout;
90  }
91 
92  QtObject {
93  id: d
94  property var initialItem
95  // the non-expanded distance from alignment edge to center of initial item
96  property real originalDistanceFromEdge: -1
97 
98  // calculate the distance from row alignment edge edge to center of initial item
99  property real distanceFromEdge: {
100  if (originalDistanceFromEdge == -1) return 0;
101  if (!initialItem) return 0;
102 
103  if (root.alignment == Qt.AlignLeft) {
104  return initialItem.x - initialItem.width / 2;
105  } else {
106  return row.width - initialItem.x - initialItem.width / 2;
107  }
108  }
109 
110  // offset to the intially selected expanded item
111  property real rowOffset: 0
112  property real scrollOffset: 0
113  property real alignmentAdjustment: 0
114  property real combinedOffset: 0
115 
116  // when the scroll offset changes, we need to reclaculate the relative lateral position
117  onScrollOffsetChanged: root.lateralPositionChanged()
118 
119  onInitialItemChanged: {
120  if (root.alignment == Qt.AlignLeft) {
121  originalDistanceFromEdge = initialItem ? (initialItem.x - initialItem.width/2) : -1;
122  } else {
123  originalDistanceFromEdge = initialItem ? (row.width - initialItem.x - initialItem.width/2) : -1;
124  }
125  }
126 
127  Behavior on alignmentAdjustment {
128  NumberAnimation { duration: LomiriAnimation.BriskDuration; easing: LomiriAnimation.StandardEasing}
129  }
130 
131  function alignIndicators() {
132  flickable.resetContentXComponents();
133 
134  if (expanded && !flickable.moving) {
135 
136  if (root.alignment == Qt.AlignLeft) {
137  // current item overlap on left
138  if (row.currentItem && flickable.contentX > (row.currentItem.x - row.contentX)) {
139  d.alignmentAdjustment -= (flickable.contentX - (row.currentItem.x - row.contentX));
140 
141  // current item overlap on right
142  } else if (row.currentItem && flickable.contentX + flickable.width < (row.currentItem.x - row.contentX) + row.currentItem.width) {
143  d.alignmentAdjustment += ((row.currentItem.x - row.contentX) + row.currentItem.width) - (flickable.contentX + flickable.width);
144  }
145  } else {
146  // gap between left and row?
147  if (flickable.contentX + flickable.width > row.width) {
148  // row width is less than flickable
149  if (row.width < flickable.width) {
150  d.alignmentAdjustment -= flickable.contentX;
151  } else {
152  d.alignmentAdjustment -= ((flickable.contentX + flickable.width) - row.width);
153  }
154 
155  // gap between right and row?
156  } else if (flickable.contentX < 0) {
157  d.alignmentAdjustment -= flickable.contentX;
158 
159  // current item overlap on left
160  } else if (row.currentItem && (flickable.contentX + flickable.width) < (row.width - (row.currentItem.x - row.contentX))) {
161  d.alignmentAdjustment += ((row.width - (row.currentItem.x - row.contentX)) - (flickable.contentX + flickable.width));
162 
163  // current item overlap on right
164  } else if (row.currentItem && flickable.contentX > (row.width - (row.currentItem.x - row.contentX) - row.currentItem.width)) {
165  d.alignmentAdjustment -= flickable.contentX - (row.width - (row.currentItem.x - row.contentX) - row.currentItem.width);
166  }
167  }
168  }
169  }
170  }
171 
172  Rectangle {
173  id: grayLine
174  height: units.dp(2)
175  width: parent.width
176  anchors.bottom: parent.bottom
177 
178  color: "#888888"
179  opacity: expanded ? 1.0 : 0.0
180  Behavior on opacity { NumberAnimation { duration: LomiriAnimation.SnapDuration } }
181  }
182 
183  Item {
184  id: rowContainer
185  anchors.fill: parent
186  clip: expanded || row.width > rowContainer.width
187 
188  Flickable {
189  id: flickable
190  objectName: "flickable"
191 
192  // we rotate it because we want the Flickable to align its content item
193  // on the right instead of on the left
194  rotation: root.alignment != Qt.AlignRight ? 0 : 180
195 
196  anchors.fill: parent
197  contentWidth: row.width
198  contentX: d.combinedOffset
199  interactive: false
200 
201  // contentX can change by user interaction as well as user offset changes
202  // This function re-aligns the offsets so that the offsets match the contentX
203  function resetContentXComponents() {
204  d.scrollOffset += d.combinedOffset - flickable.contentX;
205  }
206 
207  rebound: Transition {
208  NumberAnimation {
209  properties: "x"
210  duration: 600
211  easing.type: Easing.OutCubic
212  }
213  }
214 
215  PanelItemRow {
216  id: row
217  objectName: "panelItemRow"
218  anchors {
219  top: parent.top
220  bottom: parent.bottom
221  }
222 
223  // Compensate for the Flickable rotation (ie, counter-rotate)
224  rotation: root.alignment != Qt.AlignRight ? 0 : 180
225 
226  lateralPosition: {
227  if (root.lateralPosition == -1) return -1;
228 
229  var mapped = root.mapToItem(row, root.lateralPosition, 0);
230  return Math.min(Math.max(mapped.x, 0), row.width);
231  }
232 
233  onCurrentItemChanged: {
234  if (!currentItem) d.initialItem = undefined;
235  else if (!d.initialItem) d.initialItem = currentItem;
236  }
237 
238  MouseArea {
239  anchors.fill: parent
240  enabled: root.expanded
241  onClicked: {
242  row.selectItemAt(mouse.x);
243  d.alignIndicators();
244  }
245  }
246  }
247 
248  }
249  }
250 
251  Timer {
252  id: alignmentTimer
253  interval: LomiriAnimation.FastDuration // enough for row animation.
254  repeat: false
255 
256  onTriggered: d.alignIndicators();
257  }
258 
259  states: [
260  State {
261  name: "minimized"
262  when: !expanded
263  PropertyChanges {
264  target: d
265  rowOffset: 0
266  scrollOffset: 0
267  alignmentAdjustment: 0
268  combinedOffset: 0
269  restoreEntryValues: false
270  }
271  },
272  State {
273  name: "expanded"
274  when: expanded && !interactive
275 
276  PropertyChanges {
277  target: d
278  combinedOffset: rowOffset + alignmentAdjustment - scrollOffset
279  }
280  PropertyChanges {
281  target: d
282  rowOffset: {
283  if (!initialItem) return 0;
284  if (distanceFromEdge - initialItem.width <= 0) return 0;
285 
286  var rowOffset = distanceFromEdge - originalDistanceFromEdge;
287  return rowOffset;
288  }
289  restoreEntryValues: false
290  }
291  },
292  State {
293  name: "interactive"
294  when: expanded && interactive
295 
296  StateChangeScript {
297  script: {
298  // don't use row offset anymore.
299  d.scrollOffset -= d.rowOffset;
300  d.rowOffset = 0;
301  d.initialItem = undefined;
302  alignmentTimer.start();
303  }
304  }
305  PropertyChanges {
306  target: d
307  combinedOffset: rowOffset + alignmentAdjustment - scrollOffset
308  restoreEntryValues: false
309  }
310  }
311  ]
312 
313  transitions: [
314  Transition {
315  from: "expanded"
316  to: "minimized"
317  PropertyAction {
318  target: d
319  properties: "rowOffset, scrollOffset, alignmentAdjustment"
320  value: 0
321  }
322  PropertyAnimation {
323  target: d
324  properties: "combinedOffset"
325  duration: LomiriAnimation.SnapDuration
326  easing: LomiriAnimation.StandardEasing
327  }
328  }
329  ]
330 }