1919 // 1. DATA BINDING FROM PYTHON
2020 // =========================================================================
2121 required property var rowBuilder
22+ required property var tabController
2223 property var rows: []
2324
2425 Connections {
@@ -82,6 +83,13 @@ Item {
8283 // =========================================================================
8384 Components .SelectionModel {
8485 id: selectionModel
86+
87+ onSelectionChanged: {
88+ // Push selection update to backend
89+ if (tabController) {
90+ tabController .updateSelection (selection)
91+ }
92+ }
8593 }
8694
8795 SystemPalette { id: activePalette; colorGroup: SystemPalette .Active }
@@ -95,6 +103,7 @@ Item {
95103 readonly property bool isSystemDark: Qt .styleHints .colorScheme === Qt .ColorScheme .Dark
96104 color: isSystemDark ? Qt .darker (activePalette .base , 1.3 ) : activePalette .base
97105 focus: true
106+ clip: true
98107
99108 // 2. DropArea — External file drops
100109 DropArea {
@@ -111,16 +120,13 @@ Item {
111120 }
112121 }
113122
114- // 3. Background Actions (Deselect / Context Menu)
115- // Placed as siblings to ListView. Since ListView is interactive: false,
116- // clicks on empty areas should pass through or be handled here.
123+ // 3. Background Actions
117124 TapHandler {
118125 acceptedButtons: Qt .LeftButton
119- acceptedModifiers: Qt .KeyboardModifierMask // Don't clear selection if user is Ctrl-clicking empty space
126+ acceptedModifiers: Qt .KeyboardModifierMask
120127 onTapped: {
121128 selectionModel .clear ()
122129 root .forceActiveFocus ()
123- root .pathBeingRenamed = " "
124130 }
125131 }
126132 TapHandler {
@@ -132,10 +138,44 @@ Item {
132138 }
133139 }
134140
141+ // =========================================================================
142+ // 7. BACKGROUND MESSAGE (Empty State / Error / Info)
143+ // =========================================================================
144+ property string messageText: " "
145+ property string messageIcon: " "
146+
147+ Column {
148+ anchors .centerIn : parent
149+ spacing: 16
150+ opacity: 0.4
151+ visible: root .messageText !== " "
152+ z: 0
153+
154+ Text {
155+ text: root .messageIcon || " "
156+ font .pixelSize : 64
157+ color: activePalette .text
158+ anchors .horizontalCenter : parent .horizontalCenter
159+ }
160+ Text {
161+ text: root .messageText || " "
162+ font .pixelSize : 22
163+ font .bold : true
164+ color: activePalette .text
165+ anchors .horizontalCenter : parent .horizontalCenter
166+ }
167+ }
168+
135169 // 4. Main List View (Direct, no ScrollView wrapper)
136170 ListView {
137171 id: rowListView
138172 anchors .fill : parent
173+ // PADDING: Use internal margins instead of anchors to avoid clipping scrollbars/dead zones
174+ leftMargin: 12
175+ rightMargin: 18
176+ topMargin: 18
177+ bottomMargin: 12
178+
139179 clip: true
140180
141181 // Interaction:
@@ -146,101 +186,26 @@ Item {
146186 // ScrollBar: Attached directly.
147187 // "active: true" keeps it visible/fading correctly.
148188 // "interactive: true" ensures we can drag it even if ListView is interactive: false.
149- // ScrollBar: Specialized GTK component
189+ // ScrollBar: Specialized GTK component with Turbo Boost
150190 ScrollBar .vertical : Components .GtkScrollBar {
191+ id: verticalScrollBar
151192 flickable: rowListView
193+ showTrack: true
194+ physicsEnabled: true // Enable internal physics engine
195+ turboMode: rowListView .turboMode
152196 }
153197
154198 // Physics State
155- property real lastWheelTime: 0
156- property real acceleration: 1.0
157- property int lastDeltaSign: 0
158199 property bool turboMode: true // Defaulted to true for testing
159200
160- // SnapBack Timer: Returns to bounds after overshooting
161- Timer {
162- id: snapBackTimer
163- interval: 150
164- onTriggered: {
165- let maxY = Math .max (0 , rowListView .contentHeight - rowListView .height )
166- if (rowListView .contentY < 0 ) {
167- rowListView .contentY = 0
168- } else if (rowListView .contentY > maxY) {
169- rowListView .contentY = maxY
170- }
171- }
172- }
173-
174- // Wheel Handling: Custom logic to inject smooth scrolls
175- // We do this manually because interactive: false kills native wheeling too.
201+ // Proxy WheelHandler: Captures events on the View and forwards to ScrollBar logic
176202 WheelHandler {
177203 target: rowListView
178- onWheel : (event ) => {
179- let maxY = Math .max (0 , rowListView .contentHeight - rowListView .height )
180-
181- // Optimization: Micro-overflow check (STRICT MODE)
182- // RISK: If contentY gets stuck out of bounds (e.g. negative) due to resize/scrollbar drag,
183- // this strict check WILL prevent the wheel from recovering it.
184- // The view will be frozen until a resize or scrollbar interaction resets it.
185- if (maxY < 20 ) {
186- return
187- }
188-
189- // 1. Acceleration Logic
190- let now = new Date ().getTime ()
191- let dt = now - rowListView .lastWheelTime
192- rowListView .lastWheelTime = now
193-
194- // If same direction and fast (<100ms), ramp up acceleration
195- // (Only relevant for Mouse Wheel steps, but we calculate it generally)
196- let currentSign = (event .angleDelta .y > 0 ) ? 1 : - 1
197- if (dt < 100 && currentSign === rowListView .lastDeltaSign && event .angleDelta .y !== 0 ) {
198- let ramp = rowListView .turboMode ? 1.0 : 0.5
199- let limit = rowListView .turboMode ? 10.0 : 6.0
200-
201- rowListView .acceleration = Math .min (rowListView .acceleration + ramp, limit)
202- } else {
203- rowListView .acceleration = 1.0
204- }
205- rowListView .lastDeltaSign = currentSign
206-
207- // 2. Base Delta & Acceleration Handling
208- let delta = 0
209- let isTrackpad = false
210-
211- if (event .angleDelta .y !== 0 ) {
212- // Mouse Wheel: Apply Acceleration
213- delta = - (event .angleDelta .y / 1.2 )
214- delta *= rowListView .acceleration
215- } else if (event .pixelDelta .y !== 0 ) {
216- // Trackpad: Direct 1:1 Mapping (No Turbo/Acceleration)
217- delta = - event .pixelDelta .y
218- isTrackpad = true
219- }
220-
221- if (delta === 0 ) return
222-
223- // 3. Resistance (Bounce)
224- // If we are ALREADY out of bounds, apply heavy friction
225- if (rowListView .contentY < 0 || rowListView .contentY > maxY) {
226- delta *= 0.3
227- }
228-
229- // 4. Propose New Position
230- let newY = rowListView .contentY + delta
231-
232- // 5. Relaxed Clamping (Allow Overshoot up to 300px)
233- if (newY < - 300 ) newY = - 300
234- if (newY > maxY + 300 ) newY = maxY + 300
235-
236- // 6. Apply
237- rowListView .contentY = newY
238-
239- // Restart SnapBack
240- snapBackTimer .restart ()
241- }
204+ acceptedDevices: PointerDevice .Mouse | PointerDevice .TouchPad
205+ onWheel : (event ) => verticalScrollBar .handleWheel (event )
242206 }
243207
208+
244209 // Native-like Smoothing
245210 // DISABLED when ScrollBar is pressed to allow instant 1:1 dragging.
246211 Behavior on contentY {
@@ -263,11 +228,12 @@ Item {
263228 rowBuilder: root .rowBuilder // Pass down
264229 view: root
265230 imageHeight: root .rowHeight
266- x: 10
231+ // x: 0 // Default (relative to contentItem which respects leftMargin)
267232 }
268233
269234 onWidthChanged: {
270- if (rowBuilder) rowBuilder .setAvailableWidth (width)
235+ // Correctly inform Python of the ACTUAL available width for content
236+ if (rowBuilder) rowBuilder .setAvailableWidth (width - leftMargin - rightMargin)
271237 }
272238 }
273239
@@ -285,8 +251,9 @@ Item {
285251 var newY = rowListView .contentY + scrollSpeed
286252
287253 // Clamp
288- var maxY = Math .max (0 , rowListView .contentHeight - rowListView .height )
289- newY = Math .max (0 , Math .min (newY, maxY))
254+ var minY = - rowListView .topMargin
255+ var maxY = Math .max (minY, rowListView .contentHeight - rowListView .height + rowListView .bottomMargin )
256+ newY = Math .max (minY, Math .min (newY, maxY))
290257
291258 if (rowListView .contentY !== newY) {
292259 rowListView .contentY = newY
@@ -313,8 +280,14 @@ Item {
313280 var start = centroid .pressPosition
314281
315282 // Capture Start in CONTENT SPACE
316- startContentX = start .x - 10 // Correct for padding
317- startContentY = start .y + rowListView .contentY
283+ // Visual X (relative to Rectangle) -> Content X
284+ // ListView has leftMargin (previously hardcoded 10+10). Now just LeftMargin.
285+ startContentX = start .x - rowListView .leftMargin
286+
287+ // Visual Y (relative to Rectangle) -> Content Y
288+ // ListView has topMargin. ContentY starts at -topMargin.
289+ // Visual Y = topMargin + (Item Y - contentY) -> Item Y = Visual Y - topMargin + contentY
290+ startContentY = start .y - rowListView .topMargin + rowListView .contentY
318291
319292 rubberBand .show ()
320293 updateSelection ()
@@ -338,9 +311,9 @@ Item {
338311 var currentVisualX = centroid .position .x
339312 var currentVisualY = centroid .position .y
340313
341- // 1. Calculate Current Content Pos
342- var currentContentX = currentVisualX - 10
343- var currentContentY = currentVisualY + rowListView .contentY
314+ // 1. Calculate Current Content Pos (Same logic as Start)
315+ var currentContentX = currentVisualX - rowListView . leftMargin
316+ var currentContentY = currentVisualY - rowListView . topMargin + rowListView .contentY
344317
345318 // 2. Define Rect in CONTENT SPACE
346319 // (Min/Max between Start and Current)
@@ -350,10 +323,10 @@ Item {
350323 var h = Math .abs (currentContentY - startContentY)
351324
352325 // 3. Update Visual RubberBand (Project back to Visual Space)
353- // VisualY = ContentY - rowListView.contentY
354- // VisualX = ContentX + 10
355- rubberBand .x = x + 10
356- rubberBand .y = y - rowListView .contentY
326+ // Visual X = Content X + leftMargin
327+ // Visual Y = Content Y - contentY + topMargin
328+ rubberBand .x = x + rowListView . leftMargin
329+ rubberBand .y = y - rowListView .contentY + rowListView . topMargin
357330 rubberBand .width = w
358331 rubberBand .height = h
359332
0 commit comments