2 * Copyright (C) 2013-2014 Canonical Ltd.
3 * Copyright (C) 2020-2026 UBports Foundation
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.
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.
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/>.
19import Lomiri.Components 1.3
22import "../../../Components"
26 implicitHeight: units.gu(3)
28 property int align: Qt.AlignRight
29 property int orientation: Qt.Horizontal
30 property bool clip: true
31 property alias interactive: flickable.interactive
32 property var collapsedCutouts: []
33 property var expandedCutouts: []
34 property real collapsedHeight: units.gu(3)
35 property bool hideRow: false
36 property QtObject model: null
37 property real overFlowWidth: width
38 property bool expanded: false
39 property bool finishedExpanding: false
40 property var screenOrientation: Qt.PrimaryOrientation
42 property var currentItem: null
43 property int currentItemIndex: 0
44 property real unitProgress: 0.0
45 property real selectionChangeBuffer: units.gu(2)
46 property bool enableLateralChanges: false
48 property real lastSelectedX: 0.0
49 property alias delegate: repeater.delegate
50 property alias contentX: flickable.contentX
51 property alias contentWidth: flickable.contentWidth
53 property real lateralPosition: -1
54 property int screenIndex: -1
58 expanded: root.expanded
59 orientation: screenOrientation
60 enabled: screenIndex === 0
61 lightMode: theme.palette.normal.background.hslLightness >= 0.5
69 target: DebuggingController
71 function onDeviceInfoReloadRequested() {
72 cutoutsModel.reloadConfig();
76 onCurrentItemIndexChanged: {
77 currentItem = repeater.itemAt(currentItemIndex);
89 onFinishedExpandingChanged: {
91 flickable.contentX = 0;
96 onLateralPositionChanged: {
97 updateItemFromLateralPosition();
100 onEnableLateralChangesChanged: {
101 updateItemFromLateralPosition();
106 function onCountChanged() {
107 d.recalculateItems();
111 function updateItemFromLateralPosition() {
112 if (!enableLateralChanges || !root.finishedExpanding || lateralPosition === -1)
115 selectItemAt(lateralPosition);
118 function indicatorAt(x, y) {
119 const pos = flickable.contentItem.mapFromItem(root, x, y);
120 const indicator = flickable.contentItem.childAt(pos.x, pos.y);
126 // Backwards compatibility, consumers of this function don't expect holes
127 let closestBefore = null;
128 let closestAfter = null;
129 for (let i = 0; i < repeater.count; i++) {
130 const item = repeater.itemAt(d.isFlipped ? i : repeater.count - i - 1);
131 if (item.x > pos.x) {
132 closestBefore = item;
133 closestAfter = repeater.itemAt(i + 1);
136 if (!closestBefore) {
139 if (!closestAfter || closestBefore.x + closestBefore.width - pos.x < pos.x - closestAfter.x) {
140 return closestBefore;
146 function resetCurrentItem() {
147 flickable.contentX = 0;
148 root.lastSelectedX = 0.0;
149 root.currentItemIndex = -1;
152 function selectPreviousItem() {
153 let indexToSelect = currentItemIndex - 1;
154 while (indexToSelect >= 0) {
155 if (setCurrentItemIndex(indexToSelect)) {
162 function selectNextItem() {
163 let indexToSelect = currentItemIndex + 1;
164 while (indexToSelect < repeater.count) {
165 if (setCurrentItemIndex(indexToSelect)) {
172 function setCurrentItemIndex(index) {
173 for (let i = 0; i < repeater.count; i++) {
174 const item = repeater.itemAt(i);
175 if (item.hasOwnProperty('ownIndex') && item.ownIndex === index && item.enabled) {
176 root.currentItemIndex = index;
184 function selectItemAt(x) {
185 root.lastSelectedX = x;
186 const item = indicatorAt(x, flickable.height / 2);
187 if (item && item.opacity > 0 && item.enabled) {
188 root.currentItemIndex = item.ownIndex;
190 const searchIndex = x < width / 2 ? 0 : repeater.count - 1;
191 setCurrentItemIndex(searchIndex);
198 property bool isFlipped: {
199 root.align === Qt.AlignRight && root.orientation === Qt.Horizontal || root.align === Qt.AlignBottom && root.orientation === Qt.Vertical;
201 property bool isHorizontal: root.orientation === Qt.Horizontal
203 function recalculateLater() {
204 Qt.callLater(recalculateItems);
207 function recalculateItems() {
208 const sizeField = isHorizontal ? 'width' : 'height';
209 const posField = isHorizontal ? 'x' : 'y';
210 const flickableField = isHorizontal ? 'contentX' : 'contentY';
215 if (!root.finishedExpanding && root.currentItemIndex >= 0) {
216 const mapped = flickable.mapToItem(root, root.lastSelectedX, 0);
218 for (let i = 0; i < repeater.count; i++) {
219 if ((d.isFlipped && i >= root.currentItemIndex) || (!d.isFlipped && i < root.currentItemIndex)) {
220 sumBefore += repeater.itemAt(i)[sizeField];
223 for (let i = 0; i < cutoutsModel.rowCount(); i++) {
224 const cutout = cutoutsModel.data(cutoutsModel.index(i, 0));
225 if ((d.isFlipped && cutout[posField] + cutout[sizeField] > root.lastSelectedX) || (!d.isFlipped && cutout[posField] < root.lastSelectedX)) {
226 sumBefore += cutout[sizeField];
230 if (root.lastSelectedX > 0) {
231 flickable[flickableField] = sumBefore - mapped.x;
235 for (let i = 0; i < repeater.count; i++) {
236 const item = repeater.itemAt(d.isFlipped ? repeater.count - 1 - i : i);
237 const itemSize = Math.ceil(item[sizeField]);
239 if (itemSize > itemMaxSize) {
240 itemMaxSize = itemSize;
243 screenX = currentX - flickable[flickableField];
245 for (let ci = 0; ci < cutoutsModel.rowCount(); ci++) {
246 const cutout = cutoutsModel.data(cutoutsModel.index(ci, 0));
247 const cutoutSize = Math.floor(cutout[sizeField]);
248 const cutoutPos = Math.floor(d.isFlipped ? root[sizeField] - cutout[posField] - cutoutSize : cutout[posField]);
250 if (screenX > cutoutPos + cutoutSize || screenX + itemSize <= cutoutPos) {
254 screenX = cutoutPos + cutoutSize;
255 currentX = screenX + flickable[flickableField];
258 item[posField] = currentX;
259 item.rotation = d.isFlipped ? -180 : 0;
260 currentX += itemSize;
263 const lastCutout = cutoutsModel.data(cutoutsModel.index(cutoutsModel.rowCount() - 1, 0));
264 const lastSectionSize = Math.floor(root[sizeField] - (lastCutout ? lastCutout[posField] - lastCutout[sizeField] : 0));
265 const lastItemSize = repeater.itemAt(repeater.count - 1) ? Math.ceil(repeater.itemAt(repeater.count - 1)[sizeField]) : 0;
267 for (let i = 0; i < cutoutsModel.rowCount(); i++) {
268 const cutout = cutoutsModel.data(cutoutsModel.index(i, 0));
269 cutoutsSize += cutout[sizeField];
271 if (lastSectionSize < lastItemSize) {
272 // TODO: this is not the proper solution
273 // with this, you can slide the last item too far in the direction of the alignment
274 // but otherwise the last item will be forced outside the bounds
275 currentX += cutoutsSize;
277 flickable[isHorizontal ? 'contentWidth' : 'contentHeight'] = currentX;
278 updateItemFromLateralPosition();
287 flickableDirection: d.isHorizontal ? Flickable.HorizontalFlick : Flickable.VerticalFlick
288 rotation: d.isFlipped ? 180 : 0
290 Behavior on contentX {
291 enabled: !enableLateralChanges && root.finishedExpanding
293 duration: LomiriAnimation.BriskDuration
294 easing: LomiriAnimation.StandardEasing
299 d.recalculateItems();
302 d.recalculateItems();
304 onContentWidthChanged: {
306 d.recalculateLater();
313 objectName: "panelRow"
316 item.widthChanged.connect(d.recalculateItems);
323 objectName: "highlight"
327 topMargin: flickable.height - height
330 color: theme.palette.normal.foregroundText
331 visible: currentItem !== null
334 width: currentItem ? currentItem.width : 0
336 enabled: enableLateralChanges && expanded
337 LomiriNumberAnimation {
338 duration: LomiriAnimation.FastDuration
339 easing: LomiriAnimation.StandardEasing
343 // micromovements of the highlight line when user moves the finger across the items while pulling
344 // the handle downwards.
345 property real highlightCenterOffset: {
346 if (!currentItem || lateralPosition == -1 || !enableLateralChanges)
349 var itemMapped = root.mapToItem(currentItem, lateralPosition, 0);
351 var distanceFromCenter = itemMapped.x - currentItem.width / 2;
352 if (distanceFromCenter > 0) {
353 distanceFromCenter = Math.max(0, distanceFromCenter - currentItem.width / 8);
355 distanceFromCenter = Math.min(0, distanceFromCenter + currentItem.width / 8);
358 if (currentItem && currentItem.ownIndex === 0 && distanceFromCenter < 0) {
360 } else if (currentItem && currentItem.ownIndex === repeater.count - 1 & distanceFromCenter > 0) {
363 return (distanceFromCenter / (currentItem.width / 4)) * units.gu(1);
366 Behavior on highlightCenterOffset {
368 duration: LomiriAnimation.FastDuration
369 easing: LomiriAnimation.StandardEasing
373 // FIXME: flickable.contentX != null is only there to trigger the binding because currentItem.mapToItem won't
374 property real currentItemX: currentItem && flickable.contentX != null ? currentItem.mapToItem(root, 0, 0).x : 0
376 Behavior on currentItemX {
377 id: currentItemXBehavior
378 enabled: enableLateralChanges && expanded && !flickable.moving && root.finishedExpanding
380 duration: LomiriAnimation.FastDuration
381 easing: LomiriAnimation.StandardEasing
384 x: currentItemX + highlightCenterOffset
395 PropertyChanges { target: highlight; opacity: 0.9 }
402 properties: "opacity";
403 duration: LomiriAnimation.SnapDuration
404 easing: LomiriAnimation.StandardEasing