fix(regionSelector): Freehand->Rect auto-swap fires at Crop entry, not exit

The swap in onCaptureModeChanged read isCropMode - a derived binding
that may not have re-evaluated inside the handler - so it skipped at
entry (stale false) and fired at exit (stale true), leaving the tab
oval on Freehand for the whole Crop session. Read captureMode directly.

Also hardened while diagnosing: ToolbarTabBar indicator binds its edge
targets directly to children[currentIndex] (drops the property Item
hop), and gains an interactive property so callers dim via opacity
instead of enabled: false.
This commit is contained in:
DJ committed 2026-07-05 00:40:17 -04:00
1 parent 3bbfdbb6bf
commit ef737f5992
3 files changed
+27 -11

No files matched your search

@@ -10,6 +10,10 @@ Item {
id: root
property alias currentIndex: tabBar.currentIndex
required property var tabButtonList
// Gate user input without `enabled: false` — disabling the bar stalls the
// active-indicator animation until re-enable (the oval visually sticks on the
// old tab). Callers dim via opacity and set interactive: false instead.
property bool interactive: true
function incrementCurrentIndex() {
tabBar.incrementCurrentIndex()
@@ -40,12 +44,13 @@ Item {
text: modelData.name
materialSymbol: modelData.icon
onClicked: {
root.setCurrentIndex(index)
if (root.interactive) root.setCurrentIndex(index)
}
}
}
}
Rectangle {
id: activeIndicator
z: 0
@@ -53,19 +58,24 @@ Item {
implicitWidth: contentItem.children[root.currentIndex]?.implicitWidth ?? 0
implicitHeight: contentItem.children[root.currentIndex]?.implicitHeight ?? 0
radius: height / 2
// Animation
property Item targetItem: contentItem.children[root.currentIndex]
// Animation. Bind the edge targets straight to the children/currentIndex
// expression — an intermediate `property Item targetItem` hop silently failed
// to re-fire the pair's index binding on programmatic index changes, leaving
// the indicator stranded on the old tab.
readonly property real targetX: contentItem.children[root.currentIndex]?.x ?? 0
readonly property real targetRight: (contentItem.children[root.currentIndex]?.x ?? 0)
+ (contentItem.children[root.currentIndex]?.width ?? 0)
AnimatedTabIndexPair {
id: leftBound
idx1Duration: 50
idx2Duration: 200
index: activeIndicator.targetItem.x
index: activeIndicator.targetX
}
AnimatedTabIndexPair {
id: rightBound
idx1Duration: 50
idx2Duration: 200
index: activeIndicator.targetItem.x + activeIndicator.targetItem.width
index: activeIndicator.targetRight
}
x: Math.min(leftBound.idx1, leftBound.idx2)
width: Math.max(rightBound.idx1, rightBound.idx2) - x
@@ -77,6 +87,7 @@ Item {
acceptedButtons: Qt.NoButton
cursorShape: Qt.PointingHandCursor
onWheel: (event) => {
if (!root.interactive) return;
if (event.angleDelta.y < 0) {
root.incrementCurrentIndex();
}
@@ -76,11 +76,12 @@ Toolbar {
}
// Selection mode tabs (Rect/Freehand) - inert while Crop is active (Crop implies
// rectangles; entering Crop from Freehand auto-swaps to Rect)
// rectangles; entering Crop from Freehand auto-swaps to Rect). Dimmed via opacity +
// interactive, NOT enabled — see ToolbarTabBar.interactive.
ToolbarTabBar {
id: tabBar
enabled: !root.cropActive
opacity: enabled ? 1 : 0.4
interactive: !root.cropActive
opacity: interactive ? 1 : 0.4
tabButtonList: [
{"icon": "activity_zone", "name": Translation.tr("Rect")},
{"icon": "gesture", "name": Translation.tr("Freehand")}
@@ -101,8 +102,8 @@ Toolbar {
// (Esc, C and the FABs are the editor exits)
ToolbarTabBar {
id: captureModeTabBar
enabled: !root.cropEditing
opacity: enabled ? 1 : 0.4
interactive: !root.cropEditing
opacity: interactive ? 1 : 0.4
tabButtonList: [
{"icon": "bolt", "name": Translation.tr("Instant")},
{"icon": "crop", "name": Translation.tr("Crop")}
@@ -54,7 +54,11 @@ PanelWindow {
root.cropEditing = false;
root.pendingSnipPoints = [];
dragState.reset();
if (root.isCropMode && root.selectionMode === RegionSelection.SelectionMode.Circle) {
// Read captureMode directly, NOT isCropMode: the derived binding may not have
// re-evaluated yet inside this handler (stale read = the swap fires one toggle
// late, on exit instead of entry)
if (root.captureMode === RegionSelection.CaptureMode.Crop
&& root.selectionMode === RegionSelection.SelectionMode.Circle) {
root.selectionMode = RegionSelection.SelectionMode.RectCorners;
}
}