Lomiri
Loading...
Searching...
No Matches
PanelItemRow.qml
1/*
2 * Copyright (C) 2013-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.12
19import Lomiri.Components 1.3
20import "../Components"
21
22Item {
23 id: root
24 implicitWidth: row.width
25 implicitHeight: units.gu(3)
26
27 property bool hideRow: false
28 property QtObject model: null
29 property real overFlowWidth: width
30 property bool expanded: false
31 readonly property alias currentItem: row.currentItem
32 readonly property alias currentItemIndex: row.currentIndex
33
34 property real unitProgress: 0.0
35 property real selectionChangeBuffer: units.gu(2)
36 property bool enableLateralChanges: false
37 property color hightlightColor: "#ffffff"
38
39 property alias delegate: row.delegate
40 property alias contentX: row.contentX
41
42 property real lateralPosition: -1
43 onLateralPositionChanged: {
44 updateItemFromLateralPosition();
45 }
46
47 onEnableLateralChangesChanged: {
48 updateItemFromLateralPosition();
49 }
50
51 function updateItemFromLateralPosition() {
52 if (currentItem && !enableLateralChanges) return;
53 if (lateralPosition === -1) return;
54
55 if (!currentItem) {
56 selectItemAt(lateralPosition);
57 return;
58 }
59
60 var maximumBufferOffset = selectionChangeBuffer * unitProgress;
61 var proposedItem = indicatorAt(lateralPosition, 0);
62 if (proposedItem) {
63 var bufferExceeded = false;
64
65 if (proposedItem !== currentItem) {
66 // Proposed item is not directly adjacent to current?
67 if (Math.abs(proposedItem.ownIndex - currentItem.ownIndex) > 1) {
68 bufferExceeded = true;
69 } else { // no
70 var currentItemLateralPosition = root.mapToItem(proposedItem, lateralPosition, 0).x;
71
72 // Is the distance into proposed item greater than max buffer?
73 // Proposed item is before current item
74 if (proposedItem.x < currentItem.x) {
75 bufferExceeded = (proposedItem.width - currentItemLateralPosition) > maximumBufferOffset;
76 } else { // After
77 bufferExceeded = currentItemLateralPosition > maximumBufferOffset;
78 }
79 }
80 if (bufferExceeded) {
81 selectItemAt(lateralPosition);
82 }
83 }
84 } else {
85 selectItemAt(lateralPosition);
86 }
87 }
88
89 function indicatorAt(x, y) {
90 var item = row.itemAt(x + row.contentX, y);
91 return item && item.hasOwnProperty("ownIndex") ? item : null;
92 }
93
94 function resetCurrentItem() {
95 d.firstItemSwitch = true;
96 d.previousItem = null;
97 row.currentIndex = -1;
98 }
99
100 function selectPreviousItem() {
101 var indexToSelect = currentItemIndex - 1;
102 while (indexToSelect >= 0) {
103 if (setCurrentItemIndex(indexToSelect))
104 return;
105 indexToSelect = indexToSelect - 1;
106 }
107 }
108
109 function selectNextItem() {
110 var indexToSelect = currentItemIndex + 1;
111 while (indexToSelect < row.contentItem.children.length) {
112 if (setCurrentItemIndex(indexToSelect))
113 return;
114 indexToSelect = indexToSelect + 1;
115 }
116 }
117
118 function setCurrentItemIndex(index) {
119 for (var i = 0; i < row.contentItem.children.length; i++) {
120 var item = row.contentItem.children[i];
121 if (item.hasOwnProperty("ownIndex") && item.ownIndex === index && item.enabled) {
122 if (currentItem !== item) {
123 row.currentIndex = index;
124 }
125 return true;
126 }
127 }
128 return false;
129 }
130
131 function selectItemAt(lateralPosition) {
132 var item = indicatorAt(lateralPosition, 0);
133 if (item && item.opacity > 0 && item.enabled) {
134 row.currentIndex = item.ownIndex;
135 } else {
136 // Select default item.
137 var searchIndex = lateralPosition >= width ? row.count - 1 : 0;
138
139 for (var i = 0; i < row.contentItem.children.length; i++) {
140 if (row.contentItem.children[i].hasOwnProperty("ownIndex") &&
141 row.contentItem.children[i].ownIndex === searchIndex &&
142 row.contentItem.children[i].enabled)
143 {
144 item = row.contentItem.children[i];
145 break;
146 }
147 }
148 if (item && currentItem !== item) {
149 row.currentIndex = item.ownIndex;
150 }
151 }
152 }
153
154 QtObject {
155 id: d
156 property bool firstItemSwitch: true
157 property var previousItem
158 property bool forceAlignmentAnimationDisabled: false
159 }
160
161 onCurrentItemChanged: {
162 if (d.previousItem) {
163 d.firstItemSwitch = false;
164 }
165 d.previousItem = currentItem;
166 }
167
168 ListView {
169 id: row
170 objectName: "panelRow"
171 orientation: ListView.Horizontal
172 model: root.model
173 opacity: hideRow ? 0 : 1
174 // dont set visible on basis of opacity; otherwise width will not be calculated correctly
175 anchors {
176 top: parent.top
177 bottom: parent.bottom
178 }
179
180 // TODO: make this better
181 // when the width changes, the highlight will lag behind due to animation, so we need to disable the animation
182 // and adjust the highlight X immediately.
183 width: contentItem.width
184 Behavior on width {
185 SequentialAnimation {
186 ScriptAction {
187 script: {
188 d.forceAlignmentAnimationDisabled = true;
189 highlight.currentItemX = Qt.binding(function() { return currentItem ? currentItem.x - row.contentX : 0 });
190 d.forceAlignmentAnimationDisabled = false;
191 }
192 }
193 }
194 }
195
196 Behavior on opacity { NumberAnimation { duration: LomiriAnimation.SnapDuration } }
197 }
198
199 Rectangle {
200 id: highlight
201 objectName: "highlight"
202
203 anchors.bottom: row.bottom
204 height: units.dp(2)
205 color: root.hightlightColor
206 visible: currentItem !== null
207 opacity: 0.0
208
209 width: currentItem ? currentItem.width : 0
210 Behavior on width {
211 enabled: !d.firstItemSwitch && expanded
212 LomiriNumberAnimation { duration: LomiriAnimation.FastDuration; easing: LomiriAnimation.StandardEasing }
213 }
214
215 // micromovements of the highlight line when user moves the finger across the items while pulling
216 // the handle downwards.
217 property real highlightCenterOffset: {
218 if (!currentItem || lateralPosition == -1 || !enableLateralChanges) return 0;
219
220 var itemMapped = root.mapToItem(currentItem, lateralPosition, 0);
221
222 var distanceFromCenter = itemMapped.x - currentItem.width / 2;
223 if (distanceFromCenter > 0) {
224 distanceFromCenter = Math.max(0, distanceFromCenter - currentItem.width / 8);
225 } else {
226 distanceFromCenter = Math.min(0, distanceFromCenter + currentItem.width / 8);
227 }
228
229 if (currentItem && currentItem.ownIndex === 0 && distanceFromCenter < 0) {
230 return 0;
231 } else if (currentItem && currentItem.ownIndex === row.count-1 & distanceFromCenter > 0) {
232 return 0;
233 }
234 return (distanceFromCenter / (currentItem.width / 4)) * units.gu(1);
235 }
236 Behavior on highlightCenterOffset {
237 NumberAnimation { duration: LomiriAnimation.FastDuration; easing: LomiriAnimation.StandardEasing }
238 }
239
240 property real currentItemX: currentItem ? currentItem.x - row.contentX : 0
241 Behavior on currentItemX {
242 id: currentItemXBehavior
243 enabled: !d.firstItemSwitch && expanded && !d.forceAlignmentAnimationDisabled
244 NumberAnimation { duration: LomiriAnimation.FastDuration; easing: LomiriAnimation.StandardEasing }
245 }
246 x: currentItemX + highlightCenterOffset
247 }
248
249 states: [
250 State {
251 name: "minimised"
252 when: !expanded
253 },
254 State {
255 name: "expanded"
256 when: expanded
257 PropertyChanges { target: highlight; opacity: 0.9 }
258 }
259 ]
260
261 transitions: [
262 Transition {
263 PropertyAnimation {
264 properties: "opacity";
265 duration: LomiriAnimation.SnapDuration
266 easing: LomiriAnimation.StandardEasing
267 }
268 }
269 ]
270}