Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
f92bf11
added a draft version of a new selectable table view
seventil Mar 20, 2026
50f4f48
Removed delegate to a separate file
seventil Mar 22, 2026
a947f3d
delegate not working fix
seventil Mar 23, 2026
27240b0
removed the initialized delegate from NewTableView component
seventil Mar 23, 2026
8c86671
changes to the scrollbar
seventil Mar 24, 2026
fb10fa2
updating scrollbar in the tableview
seventil Mar 24, 2026
0448146
Merge branch 'develop' into selectable_table_view
seventil Mar 24, 2026
fd20025
removed the color safeguards in the tableview
seventil Mar 24, 2026
3fcb201
removed antialiasing in tableview as there is another pr for that
seventil Mar 24, 2026
e15579d
make scrollbar and scroll indicator optional in ListView via vertical…
seventil Mar 27, 2026
6877173
Added a flag to enable/disable multiselection
seventil Mar 27, 2026
aca8418
Added a 0.5 row length to listview Height to visually indicate that t…
seventil Mar 27, 2026
a24d708
Cleaned the interface of the delegate to use listView property
seventil Mar 27, 2026
9570598
Changed the calculation of height
seventil Apr 9, 2026
a8e2734
Changed the column width calculation
seventil Apr 9, 2026
0b24b4d
added ListViewHeader to qmldir for imports
seventil Apr 9, 2026
963648d
listview cleanup
seventil Apr 9, 2026
0c24d0f
Fixing vibecoding issues
seventil Apr 9, 2026
bbbedfd
Refactored listview to not be encapsulated in item, resolved minor is…
seventil Apr 10, 2026
7ddaa63
Namspace clash fix
seventil Apr 10, 2026
a98f69c
Refactor ListView: separate public/companion/internal API, derive hea…
seventil Apr 13, 2026
844d107
Made shift+click additive, instead of a new selection
seventil Apr 13, 2026
1902300
added a small angle cap to indicate deselected anchor
seventil Apr 13, 2026
0360a0f
Fixed enum error in ListView that blocked depicting the scrollbar/ind…
seventil Apr 13, 2026
b6dbf9f
Vibecoding cleanup
seventil Apr 13, 2026
e3d6360
scope-aware selection visuals via focus tracking
seventil Apr 14, 2026
6faaf85
changed the default of selectionActive to true
seventil Apr 14, 2026
4fbf861
Fix the mousearea intercepting clicks completely
seventil Apr 16, 2026
d4c74c6
Added padding to left and right on delegate row of ListViewDelegate
seventil Apr 16, 2026
2c51f26
Changed the selection coloring so that it would disappear while editi…
seventil Apr 17, 2026
7cf00f0
Fixing the hover change forcing editing finished on ListView cells
seventil Apr 17, 2026
33757dc
Moved hover highlight from listview to delegate
seventil Apr 20, 2026
b4cc4b5
guard anchor reset on adding/removing from model
seventil Apr 20, 2026
29dd95b
Merge branch 'develop' into selectable_table_view
seventil Apr 20, 2026
53a9861
Got rid of indicator alltogether to use scrollbar with an interactive…
seventil Apr 20, 2026
91bf25e
Added hover highlight on editing and the color flash from TextInput
seventil Apr 21, 2026
0d33f56
Changed light-themed selection to be more readable with selected text…
seventil Apr 21, 2026
2188850
adjusted colors for dark mode selection, trying to fix selection/edit…
seventil Apr 21, 2026
4d29e6e
another dark theme adjustment for selection colors
seventil Apr 21, 2026
6167a3e
Fix for mixed up selections upon TextInput editing
seventil Apr 22, 2026
52ef34e
Merge branch 'develop' into selectable_table_view
seventil Apr 22, 2026
9973856
Changed the selection to be optional onediting, introduced a flag for it
seventil Apr 27, 2026
8a633d8
Removed hoveredindex, as hovered can be referenced within delegate an…
seventil Apr 27, 2026
bb389d2
Added selectable flag, some cases I want depict+edit without selections
seventil Apr 28, 2026
dc4ed1f
Rewired focus works in listview, so that clicking the header would st…
seventil Apr 28, 2026
8a4229b
Merge branch 'develop' into selectable_table_view
seventil Apr 29, 2026
0feb3bd
Merge branch 'develop' into selectable_table_view
seventil Apr 30, 2026
c17ef20
App->Application
seventil Apr 30, 2026
32cdd86
rm settings
seventil Apr 30, 2026
ae134a7
centered header/delegate for symmetric widths resolution without -1
seventil Apr 30, 2026
4cbb4bf
defocus on Esc in text input
seventil Apr 30, 2026
e75ec8b
added the ability to size columns based on header title width
seventil May 1, 2026
3f67191
gitignore settings.ini
seventil May 1, 2026
0e42b63
Added cursor reset on blur for ListViewTextInput. Ensures that overfl…
seventil May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ node_modules/
*.pyproject.user
*.pyproject.user.*
CMakeLists.txt.user*
*settings.ini

# PyCharm
.idea/
Expand Down
289 changes: 289 additions & 0 deletions src/EasyApplication/Gui/Components/ListView.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,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
}
}
Loading
Loading