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
18import QtQuick 2.4
19import Lomiri.Components 1.3
20import "../Components"
21
22Item {
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}