Skip to content

Commit d06b630

Browse files
cagnuleinclaude
andauthored
Enable scroll in lock mode + fix Android storage path (#4755)
* Fix tile drag: auto-scroll GridView when dragging to screen edges - Home.qml: Added autoScrollTimer (50ms interval) that scrolls the GridView when dragging a tile near the top or bottom viewport edge, with speed proportional to proximity to the edge. - Home.qml: Added indexAtMouse() helper that accounts for contentY offset when computing which tile is under the cursor during drag, fixing incorrect target index when the grid has scrolled. - Home.qml: Fixed active tile visual position to include gridView.contentY so the dragged tile stays visually anchored to the cursor while scrolling. - Home.qml: Track startIndex and tileName at long-press time to correctly identify source tile and compare against drop position at release. - homeform.cpp: Fixed moveTile() edge case where inserting a tile at the last position never wrote its order setting because i only reached newIndex after the loop ended. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix tile drag: use ghost tile instead of repositioning delegate Moving the dragged tile's y position within GridView's contentItem coordinate system confused the GridView's virtualization, causing other tiles to disappear during drag. Fix by: - Keeping the source tile in place (just dimming it to opacity 0.3) - Showing a separate ghost Item outside GridView that follows the mouse Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Remove Behavior on x/y from tile delegate to fix disappearing tiles during drag The OutBack easing on delegates caused newly-virtualized tiles to animate from y=0 (above the viewport when contentY > 0) to their target position, making them invisible for 400ms during auto-scroll. Since moveTile() rebuilds the entire model via sortTiles() on release (not appModel.move()), these animations served no useful purpose. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix moveTile: use correct QSettings key for tiles with camelCase m_id moveTile() constructed settings keys as "tile_" + m_id.toLower() + "_order", but several DataObjects use camelCase m_ids (e.g. "avgWattLap") that don't match the actual QZSettings key ("tile_avg_watt_lap_order"). As a result, the moved tile's order was written to the wrong settings key and sortTiles() ignored it, leaving the tile in its original position after drag-and-drop. Fixed with a static lookup table in moveTile() mapping the mismatched m_ids to their correct QZSettings order key. Affected tiles: - avgWattLap → tile_avg_watt_lap_order - joul → tile_jouls_order - steeringangle → tile_steering_angle_order - stride_length → tile_instantaneous_stride_length_order - external_inclination → tile_ext_incline_order - target_inclination → tile_target_incline_order Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Enable scroll in lock mode and fix Android storage path on Waydroid In lock mode (lockTiles=true) the GridView's built-in interactive scrolling is disabled to prevent accidental tile drags. This meant any tiles beyond the visible viewport could never be reached. Fix: set `interactive: !window.lockTiles` so built-in scrolling is active only in normal mode, and add a manual scroll handler in the MouseArea's onPositionChanged that moves gridView.contentY when no drag is in progress (currentId === -1). An `isSwiping` flag suppresses long-press-to-drag when the gesture starts as a swipe. Also fix getAndroidDataAppDir() to handle Waydroid/emulator environments where isExternalStorageRemovable() throws IllegalArgumentException: clear the pending JNI exception and fall back to internal storage if external storage resolution fails entirely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fb3b806 commit d06b630

2 files changed

Lines changed: 141 additions & 41 deletions

File tree

src/Home.qml

Lines changed: 120 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ HomeForm {
293293
model: appModel
294294
leftMargin: { if(OS_VERSION === "Android") (Screen.width % cellWidth) / 2; else (parent.width % cellWidth) / 2; }
295295
anchors.topMargin: (!window.lockTiles ? rootItem.topBarHeight + 30 : 0)
296+
interactive: !window.lockTiles
296297
id: gridView
297298
objectName: "gridview"
298299
onMovementEnded: { headerToolbar.visible = (contentY == 0) || window.lockTiles; }
@@ -320,15 +321,6 @@ HomeForm {
320321
Accessible.description: largeButton ? largeButtonLabel : (secondLine !== "" ? secondLine : (writable ? qsTr("Adjustable. Current value: ") + value : qsTr("Current value: ") + value))
321322
Accessible.focusable: true
322323

323-
Behavior on x {
324-
enabled: id1.state != "active"
325-
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
326-
}
327-
328-
Behavior on y {
329-
enabled: id1.state != "active"
330-
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
331-
}
332324

333325
SequentialAnimation on rotation {
334326
NumberAnimation { to: 2; duration: 60 }
@@ -340,10 +332,10 @@ HomeForm {
340332

341333
states: State {
342334
name: "active"; when: loc.currentId === gridId && window.lockTiles
343-
PropertyChanges { target: id1; x: loc.mouseX - gridView.x - width/2; y: loc.mouseY - gridView.y - height/2; scale: 0.5; z: 10 }
335+
PropertyChanges { target: id1; opacity: 0.3 }
344336
}
345337

346-
transitions: Transition { NumberAnimation { property: "scale"; duration: 200} }
338+
transitions: Transition { NumberAnimation { property: "opacity"; duration: 200} }
347339

348340
Rectangle {
349341
width: 168 * settings.ui_zoom / 100
@@ -630,27 +622,133 @@ HomeForm {
630622
}
631623
}
632624

625+
Item {
626+
id: ghostTile
627+
visible: loc.currentId !== -1 && window.lockTiles
628+
x: loc.mouseX - width / 2
629+
y: loc.mouseY - height / 2
630+
width: 85 * settings.ui_zoom / 100
631+
height: 63 * settings.ui_zoom / 100
632+
z: 200
633+
634+
Rectangle {
635+
anchors.fill: parent
636+
radius: 3
637+
color: settings.theme_tile_background_color
638+
opacity: 0.9
639+
border.width: 2
640+
border.color: settings.theme_tile_shadow_color
641+
642+
Text {
643+
anchors.centerIn: parent
644+
color: "white"
645+
text: loc.tileName
646+
font.pointSize: 10 * settings.ui_zoom / 100
647+
font.bold: true
648+
horizontalAlignment: Text.AlignHCenter
649+
width: parent.width - 8
650+
wrapMode: Text.WordWrap
651+
}
652+
}
653+
}
654+
655+
Timer {
656+
id: autoScrollTimer
657+
interval: 50
658+
repeat: true
659+
running: false
660+
property real scrollSpeed: 15
661+
onTriggered: {
662+
if (loc.currentId === -1) { running = false; return; }
663+
var edgeZone = 80
664+
if (loc.mouseY > gridView.height - edgeZone) {
665+
var factor = (loc.mouseY - (gridView.height - edgeZone)) / edgeZone
666+
gridView.contentY = Math.min(
667+
gridView.contentHeight - gridView.height,
668+
gridView.contentY + scrollSpeed * (1 + factor * 2)
669+
)
670+
} else if (loc.mouseY < edgeZone) {
671+
var factor2 = (edgeZone - loc.mouseY) / edgeZone
672+
gridView.contentY = Math.max(0, gridView.contentY - scrollSpeed * (1 + factor2 * 2))
673+
} else {
674+
running = false
675+
}
676+
}
677+
}
678+
633679
MouseArea {
634-
property int currentId: -1 // Original position in model
635-
property int newIndex // Current Position in model
636-
property int index: (Math.floor(gridView.width / gridView.cellWidth) * Math.floor(mouseY / gridView.cellHeight)) + Math.floor(mouseX / gridView.cellWidth) // gridView.indexAt(mouseX - gridView.x, mouseY - gridView.y) // Item underneath cursor
680+
property int currentId: -1
681+
property int newIndex
682+
property int startIndex: -1
683+
property string tileName: ""
684+
property real lastScrollY: 0
685+
property bool isSwiping: false
686+
687+
function indexAtMouse(mx, my) {
688+
var cols = Math.max(1, Math.floor(gridView.width / gridView.cellWidth))
689+
var adjustedY = my + gridView.contentY
690+
var col = Math.floor(mx / gridView.cellWidth)
691+
var row = Math.floor(adjustedY / gridView.cellHeight)
692+
var idx = row * cols + col
693+
if (idx < 0 || idx >= appModel.count) return -1
694+
return idx
695+
}
637696

638697
id: loc
639698
enabled: window.lockTiles
640699
anchors.fill: parent
641-
onPressAndHold: { console.log("onPressAndHold " + index); if(index !== -1) currentId = appModel[newIndex = index].gridId; else currentId = -1; }
700+
701+
onPressed: {
702+
lastScrollY = mouseY
703+
isSwiping = false
704+
}
705+
706+
onPressAndHold: {
707+
if (isSwiping) return
708+
var idx = indexAtMouse(mouseX, mouseY)
709+
if (idx !== -1) {
710+
startIndex = idx
711+
newIndex = idx
712+
currentId = appModel[idx].gridId
713+
tileName = appModel[idx].name
714+
} else {
715+
currentId = -1
716+
tileName = ""
717+
}
718+
}
719+
642720
onReleased: {
643-
console.log("onReleased " + currentId + " " + index );
644-
if (currentId !== -1 && index !== -1 && index !== newIndex) {
645-
rootItem.moveTile(appModel[currentId].name, index, newIndex);
646-
} currentId = -1
721+
autoScrollTimer.running = false
722+
if (currentId !== -1) {
723+
var idx = indexAtMouse(mouseX, mouseY)
724+
if (idx !== -1 && idx !== startIndex)
725+
rootItem.moveTile(tileName, idx, startIndex)
726+
}
727+
currentId = -1
728+
startIndex = -1
729+
tileName = ""
730+
isSwiping = false
647731
}
648732

649733
onPositionChanged: {
650-
console.log("onPositionChanged " + currentId + " " + index + " " + newIndex + " " + mouseX + " " + mouseY)
651-
if (currentId !== -1 && index !== -1 && index !== newIndex) {
652-
//appModel.move(newIndex, newIndex = index)
734+
if (currentId === -1) {
735+
// No drag in progress: scroll the grid like a normal flick
736+
var dy = mouseY - lastScrollY
737+
if (Math.abs(dy) > 5) isSwiping = true
738+
gridView.contentY = Math.max(0,
739+
Math.min(gridView.contentHeight - gridView.height,
740+
gridView.contentY - dy))
741+
lastScrollY = mouseY
742+
return
653743
}
744+
var edgeZone = 80
745+
if (mouseY > gridView.height - edgeZone || mouseY < edgeZone)
746+
autoScrollTimer.running = true
747+
else
748+
autoScrollTimer.running = false
749+
750+
var idx = indexAtMouse(mouseX, mouseY)
751+
if (idx !== -1 && idx !== newIndex) newIndex = idx
654752
}
655753
}
656754
}

src/homeform.cpp

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
638638
true, QStringLiteral("peloton_remaining"), valueElapsedFontSize, labelFontSize);
639639
strokesCount = new DataObject(tr("Strokes Count"), QStringLiteral("icons/icons/cadence.png"),
640640
QStringLiteral("0"), false, QStringLiteral("strokes_count"), 48, labelFontSize);
641-
strokesLength = new DataObject(tr("Stroke Length"), QStringLiteral("icons/icons/cadence.png"),
641+
strokesLength = new DataObject(tr("Strokes Length"), QStringLiteral("icons/icons/cadence.png"),
642642
QStringLiteral("0"), false, QStringLiteral("strokes_length"), 48, labelFontSize);
643643
gears = new DataObject(tr("Gears"), QStringLiteral("icons/icons/elevationgain.png"),
644644
QStringLiteral("0"), true, QStringLiteral("gears"), 48, labelFontSize);
@@ -4537,35 +4537,37 @@ void homeform::moveTile(QString name, int newIndex, int oldIndex) {
45374537
if (current) {
45384538
qDebug() << "moveTile" << name << newIndex << oldIndex;
45394539

4540-
foreach (QString s, settings.allKeys()) {
4541-
if (s.contains(QStringLiteral("tile_")) && s.contains(QStringLiteral("_order"))) {
4542-
4543-
qDebug() << s << settings.value(s);
4544-
}
4545-
}
4540+
// Some DataObject m_ids don't match their QZSettings _order key (camelCase vs snake_case).
4541+
// This lambda returns the correct settings key for a given DataObject.
4542+
auto orderKey = [](const DataObject *d) -> QString {
4543+
static const QHash<QString, QString> overrides = {
4544+
{QStringLiteral("avgWattLap"), QStringLiteral("tile_avg_watt_lap_order")},
4545+
{QStringLiteral("joul"), QStringLiteral("tile_jouls_order")},
4546+
{QStringLiteral("steeringangle"), QStringLiteral("tile_steering_angle_order")},
4547+
{QStringLiteral("stride_length"), QStringLiteral("tile_instantaneous_stride_length_order")},
4548+
{QStringLiteral("external_inclination"),QStringLiteral("tile_ext_incline_order")},
4549+
{QStringLiteral("target_inclination"), QStringLiteral("tile_target_incline_order")},
4550+
};
4551+
auto it = overrides.constFind(d->m_id);
4552+
if (it != overrides.constEnd()) return it.value();
4553+
return QStringLiteral("tile_") + d->m_id.toLower() + QStringLiteral("_order");
4554+
};
45464555

45474556
int i = 0;
45484557
foreach (QObject *d, dataList) {
45494558
if (i == newIndex) {
4550-
settings.setValue("tile_" + current->m_id.toLower() + "_order", i);
4559+
settings.setValue(orderKey(current), i);
45514560
i++;
45524561
}
4553-
QString n = ((DataObject *)d)->m_id;
45544562
if (((DataObject *)d)->name().compare(name)) {
4555-
settings.setValue("tile_" + n.toLower() + "_order", i);
4563+
settings.setValue(orderKey((DataObject *)d), i);
45564564
i++;
45574565
}
45584566
}
4559-
4560-
foreach (QString s, settings.allKeys()) {
4561-
if (s.contains(QStringLiteral("tile_")) && s.contains(QStringLiteral("_order"))) {
4562-
4563-
qDebug() << s << settings.value(s);
4564-
}
4567+
if (i <= newIndex) {
4568+
settings.setValue(orderKey(current), newIndex);
45654569
}
45664570

4567-
// sortTiles();
4568-
// dataList.move(oldIndex, newIndex);
45694571
// very dirty, but i needed a way to synchronize QML with C++
45704572
QTimer::singleShot(100, this, &homeform::sortTilesTimeout);
45714573
}
@@ -10760,7 +10762,7 @@ void homeform::sendMail() {
1076010762
((rower *)bluetoothManager->device())->maxPace().toString(QStringLiteral("m:ss")) +
1076110763
QStringLiteral("\n");
1076210764
textMessage +=
10763-
QStringLiteral("Average Stroke Length: ") +
10765+
QStringLiteral("Average Strokes Length: ") +
1076410766
QString::number(((rower *)bluetoothManager->device())->currentStrokesLength().average(), 'f', 1) + "\n";
1076510767
} else if (bluetoothManager->device()->deviceType() == TREADMILL || bluetoothManager->device()->deviceType() == ELLIPTICAL) {
1076610768
textMessage += QStringLiteral("Average Pace: ") +

0 commit comments

Comments
 (0)