-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathListView.qml
More file actions
289 lines (239 loc) · 11 KB
/
Copy pathListView.qml
File metadata and controls
289 lines (239 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import QtQuick
import QtQuick.Controls
import EasyApplication.Gui.Globals as EaGlobals
import EasyApplication.Gui.Style as EaStyle
import EasyApplication.Gui.Animations as EaAnimations
import EasyApplication.Gui.Elements as EaElements
import EasyApplication.Gui.Components as EaComponents
ListView {
id: listView
// ── Public API ──────────────────────────────────────────────────────
// Properties and functions for consumers instantiating this component.
width: EaStyle.Sizes.sideBarContentWidth
// When true, rows use 1.5x height.
property bool tallRows: false
// Max visible rows before scrolling kicks in.
property int maxRowCountShow: EaStyle.Sizes.tableMaxRowCountShow
// Text shown when ListView model is empty.
property alias defaultInfoText: defaultInfoLabel.text
// ScrollBar.AsNeeded / ScrollBar.AlwaysOff / ScrollBar.AlwaysOn
property int scrollBarPolicy: ScrollBar.AsNeeded
// false = indicator style: thin, non-draggable, shows only while scrolling
property bool scrollBarInteractive: true
// When false, clicks never modify the selection model. Use for lists
// with inline editors but no row-level selection concept.
property bool selectable: true
// Allow ctrl/shift multi-select.
property bool multiSelection: true
// When false, clicking a cell editor (TextInput) does not select the row.
// Editing and selection remain orthogonal.
property bool selectOnEdit: false
// Claim the enclosing FocusScope's default focus target.
focus: true
// Whether the row highlight stays lit. Default true (always). Bind to a
// focus expression (e.g. `myScope.activeFocus`) to dim when focus leaves
// that scope.
property bool selectionActive: true
// Column widths definition. Each entry is one of:
// positive int — fixed width in pixels.
// EaStyle.Sizes.tableColumnFlex (-1) — fill remaining row width.
// Multiple flex columns split the leftover space evenly.
// EaStyle.Sizes.tableColumnAuto (0) — fit the header label's implicit
// text width plus autoColumnPadding. Requires a header
// delegate (ListViewHeader) so the resolver has a label
// to measure; falls back to 0 if header not yet ready.
// Example: columnWidths: [40, EaStyle.Sizes.tableColumnFlex, EaStyle.Sizes.tableColumnAuto, 100]
property var columnWidths: []
// Padding added to each tableColumnAuto column on top of the measured
// header text width. Defaults to 2x tableColumnSpacing — same breathing
// room as a typical hand-tuned column.
property real autoColumnPadding: EaStyle.Sizes.tableColumnSpacing * 2
// Horizontal padding inside each row (header + delegate). Subtracted from
// flex-column budget so flex columns don't overflow the row.
property real rowPadding: EaStyle.Sizes.tableColumnSpacing
// Clear all selection and reset anchor.
function clearSelection() {
selectionModel.clearSelection()
anchorRow = -1
}
// Release any cell editor that currently owns focus inside the list.
// Forces focus to a non-FocusScope sibling so the editScope's inner
// focus chain is broken at the listView level — `forceActiveFocus()`
// on listView itself would re-confirm the existing chain instead.
function endEditing() {
focusSink.forceActiveFocus()
}
// ── Companion API ───────────────────────────────────────────────────
// Used by ListViewHeader and ListViewDelegate. Not intended for direct consumer use.
// Anchor row index for shift-selection range tracking.
// Used by: ListViewDelegate (anchor indicator when row is not selected)
property int anchorRow: -1
onCountChanged: if (anchorRow >= count) anchorRow = -1
// Row height in px, derived from tallRows.
// Used by: ListViewDelegate (implicitHeight), ListViewHeader (own height)
property int tableRowHeight: tallRows ?
1.5 * EaStyle.Sizes.tableRowHeight :
EaStyle.Sizes.tableRowHeight
// Current selection state.
// Used by: ListViewDelegate (binding dependency for row color)
readonly property var selectedIndexes: selectionModel.selectedIndexes
// Computed px widths from columnWidths.
// Used by: ListViewHeader + ListViewDelegate (subscribe via onResolvedColumnWidthsChanged)
//
// Two-pass resolution:
// 1. tableColumnAuto (0) → header child implicitWidth + autoColumnPadding.
// 2. tableColumnFlex (-1) → split leftover row width evenly.
readonly property var resolvedColumnWidths: {
if (!columnWidths.length) return []
// Pass 1: resolve auto columns from header implicit widths.
const headerWidths = (headerItem && headerItem.implicitColumnWidths) || []
let widths = columnWidths.map((w, i) => {
if (w === EaStyle.Sizes.tableColumnAuto && i < headerWidths.length)
return headerWidths[i] + autoColumnPadding
return w
})
// Pass 2: distribute remainder among flex columns.
let fixed = 0, flexCount = 0
for (let w of widths) {
if (w > 0) fixed += w
else flexCount++
}
const spacing = EaStyle.Sizes.tableColumnSpacing * (widths.length - 1)
const border = EaStyle.Sizes.borderThickness * 2
const fill = flexCount > 0 ? Math.max(0, (width - fixed - spacing - border - rowPadding * 2) / flexCount) : 0
return widths.map(w => w > 0 ? w : fill)
}
// Apply resolvedColumnWidths to children of a Row item.
// Used by: ListViewHeader + ListViewDelegate (onCompleted + onResolvedColumnWidthsChanged)
function applyWidths(row) {
for (let i = 0; i < row.children.length && i < resolvedColumnWidths.length; i++)
row.children[i].width = resolvedColumnWidths[i]
}
// Check if given row index is selected.
// Used by: ListViewDelegate (row background color)
function isSelected(row) {
let idx = _index(row)
return idx && idx.valid ? selectionModel.isSelected(idx) : false
}
// Select row with ctrl/shift modifier logic.
// Used by: ListViewDelegate (MouseArea.onClicked)
function selectWithModifiers(row, modifiers) {
if (!selectable) return
let idx = _index(row)
if (!idx) return
// SHIFT: range selection
if (listView.multiSelection && modifiers & Qt.ShiftModifier) {
if (anchorRow < 0) {
anchorRow = row
}
let savedAnchor = anchorRow
let from = Math.min(anchorRow, row)
let to = Math.max(anchorRow, row)
if (!(modifiers & Qt.ControlModifier)) {
selectionModel.clearSelection()
}
for (let i = from; i <= to; i++) {
let rIdx = _index(i)
if (rIdx) {
selectionModel.select(
rIdx,
ItemSelectionModel.Select | ItemSelectionModel.Rows
)
}
}
anchorRow = savedAnchor
return
}
// CTRL: toggle. Multi mode: add/remove from existing selection.
// Single mode: deselect same row, or replace selection with new row.
if (modifiers & Qt.ControlModifier) {
if (listView.multiSelection) {
selectionModel.select(idx, ItemSelectionModel.Toggle | ItemSelectionModel.Rows)
anchorRow = row
return
}
if (selectionModel.isSelected(idx)) {
selectionModel.clearSelection()
anchorRow = -1
} else {
selectionModel.select(idx, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows)
anchorRow = row
}
return
}
// DEFAULT: single selection
selectionModel.select(
idx,
ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows
)
anchorRow = row
}
// ── Internals ───────────────────────────────────────────────────────
// Convert row int to QModelIndex for selectionModel.
function _index(row) {
if (!selectionModel.model || row < 0 || row >= count)
return null
return selectionModel.model.index(row, 0)
}
// Fixes clicks not registering right after scroll.
pressDelay: 10
property bool hasMoreRows: count > maxRowCountShow
property real visibleRowCount: hasMoreRows ? maxRowCountShow + 0.5 : count
// headerItem is non-null when a header delegate is set (e.g. ListViewHeader).
// Uses actual headerItem.height so custom headers with different heights work.
property real _headerHeight: headerItem ? headerItem.height : 0
height: count === 0
? 2 * EaStyle.Sizes.tableRowHeight
: tableRowHeight * visibleRowCount + _headerHeight
clip: true
headerPositioning: ListView.OverlayHeader
boundsBehavior: Flickable.StopAtBounds
enabled: count > 0
ScrollBar.vertical: EaElements.ScrollBar {
policy: listView.scrollBarPolicy
interactive: listView.scrollBarInteractive
topInset: listView._headerHeight
topPadding: listView._headerHeight
}
// Empty-state label.
Rectangle {
parent: listView
visible: listView.count === 0
width: listView.width
height: EaStyle.Sizes.tableRowHeight * 2
color: EaStyle.Colors.themeBackground
Behavior on color { EaAnimations.ThemeChange {} }
EaElements.Label {
id: defaultInfoLabel
anchors.verticalCenter: parent.verticalCenter
leftPadding: EaStyle.Sizes.fontPixelSize
}
}
// Table border, z above all content.
Rectangle {
parent: listView
z: 4
anchors.fill: parent
color: "transparent"
// Fixes disappearing border lines
antialiasing: true
border.color: EaStyle.Colors.appBarComboBoxBorder
Behavior on border.color { EaAnimations.ThemeChange {} }
}
ItemSelectionModel {
id: selectionModel
model: listView.model
onSelectionChanged: {
if (selectedIndexes.length === 0)
listView.anchorRow = -1
}
}
// Sink target for endEditing(). Non-FocusScope child of listView, so
// forceActiveFocus on it rewrites listView.focusedChild and breaks any
// delegate's editScope chain — which forceActiveFocus on listView itself
// cannot do (re-confirms the existing chain instead).
Item {
id: focusSink
parent: listView
}
}