Skip to content

Commit 94ae07b

Browse files
committed
fix(qml): padding, rename, selection sync, and thumbnail pre-computation
- Add ListView margins (left/right/top/bottom) for breathing room - Fix marquee selection coordinate math to account for margins - Fix auto-scroll clamping to respect margins - Add background message placeholder (empty state / error / info) - Refactor RenameField from TextArea to TextField with commit-on-focus-loss - Pass tabController to JustifiedView for selection sync - Use pre-computed thumbnailUrl in FileDelegate (no bridge.getThumbnailPath) - Add clip:true to content rectangle - Document why sourceSize is disabled for thumbnails
1 parent 96ad66d commit 94ae07b

File tree

5 files changed

+119
-129
lines changed

5 files changed

+119
-129
lines changed

ui/qml/components/FileDelegate.qml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Item {
3535
property real columnWidth: 200 // Width calculated from aspect ratio
3636
property int thumbnailMaxWidth: 0 // 0 = no cap (icons/vectors can scale)
3737
property int thumbnailMaxHeight: 0 // Actual thumbnail cache dimensions
38+
property string thumbnailUrl: "" // Pre-computed by RowBuilder (no Python calls during scroll)
3839

3940
// =========================================================================
4041
// 3. STATE PROPS (Passed from parent for styling)
@@ -128,7 +129,7 @@ Item {
128129

129130
anchors.centerIn: parent
130131

131-
source: isVisual ? (bridge ? bridge.getThumbnailPath(path) : "") : ""
132+
source: thumbnailUrl
132133

133134
// Use Fit to ensure we see the whole image within our box
134135
fillMode: Image.PreserveAspectFit
@@ -137,7 +138,10 @@ Item {
137138
cache: true
138139
mipmap: false // [FIX] Disabled for sharper downscaling (matches Nemo)
139140

140-
// Request at fixed display size for efficiency
141+
// [PERF] Keep sourceSize DISABLED for thumbnails!
142+
// 1. Thumbnails are already small (256px). Scaling 256->200 on CPU is wasteful; GPU does it better.
143+
// 2. Binding sourceSize to width causes massive cache thrashing when resizing the window.
144+
// 3. Allows "Instant Zoom" (using the 256px texture) without reloading.
141145
// sourceSize: Qt.size(width, height)
142146

143147
// SHIMMER EFFECT: Visual feedback during loading

ui/qml/components/RenameField.qml

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import QtQuick
22
import QtQuick.Controls
33

4-
TextArea {
4+
TextField {
55
id: root
66

77
// --- API ---
@@ -16,19 +16,15 @@ TextArea {
1616
font.pixelSize: 12
1717

1818
// --- STYLING ---
19-
// Style to match native look
2019
background: Rectangle {
2120
color: activePalette.base
2221
border.color: activePalette.highlight
2322
border.width: 1
2423
radius: 2
2524
}
2625

27-
// Note: SystemPalette must be available in parent context or we instantiate one
28-
// Ideally components should be self-contained, so let's check activePalette access.
29-
// Standard QtQuick SystemPalette usage:
3026
SystemPalette { id: activePalette; colorGroup: SystemPalette.Active }
31-
27+
3228
color: activePalette.text
3329
selectedTextColor: activePalette.highlightedText
3430
selectionColor: activePalette.highlight
@@ -41,25 +37,38 @@ TextArea {
4137

4238
// --- BEHAVIOR ---
4339

44-
onActiveChanged: {
45-
if (active) {
46-
forceActiveFocus()
47-
// Select filename without extension
48-
var name = text
49-
var lastDot = name.lastIndexOf(".")
50-
if (lastDot > 0) {
51-
select(0, lastDot)
52-
} else {
53-
selectAll()
54-
}
40+
function initSession() {
41+
forceActiveFocus()
42+
// Select filename without extension
43+
var name = text
44+
var lastDot = name.lastIndexOf(".")
45+
if (lastDot > 0) {
46+
select(0, lastDot)
47+
} else {
48+
selectAll()
5549
}
5650
}
5751

52+
onActiveChanged: if (active) initSession()
53+
Component.onCompleted: if (active) initSession()
54+
55+
// 1. Commit on Enter
56+
onAccepted: {
57+
root.commit(text)
58+
focus = false // Release focus
59+
}
60+
61+
// 2. Commit on Focus Loss (Clicking away)
62+
onActiveFocusChanged: {
63+
if (!activeFocus && active) {
64+
// We lost focus while active -> Commit
65+
root.commit(text)
66+
}
67+
}
68+
69+
// 3. Cancel on Escape
5870
Keys.onPressed: (event) => {
59-
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
60-
root.commit(root.text)
61-
event.accepted = true
62-
} else if (event.key === Qt.Key_Escape) {
71+
if (event.key === Qt.Key_Escape) {
6372
root.cancel()
6473
event.accepted = true
6574
}

ui/qml/components/RowDelegate.qml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ Row {
6363
thumbnailMaxWidth: itemWrapper.itemData.thumbnailWidth || 0
6464
thumbnailMaxHeight: itemWrapper.itemData.thumbnailHeight || 0
6565

66+
// Pre-computed thumbnail URL (no Python calls during scroll)
67+
thumbnailUrl: itemWrapper.itemData.thumbnailUrl || ""
68+
6669
// 3. STATE PROPS
6770
selected: (rowDelegateRoot.view && rowDelegateRoot.view.currentSelection)
6871
? rowDelegateRoot.view.currentSelection.indexOf(path) !== -1

ui/qml/views/JustifiedView.qml

Lines changed: 75 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Item {
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

Comments
 (0)