Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
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
245 changes: 245 additions & 0 deletions src/EasyApp/Gui/Components/ListView.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import QtQuick
import QtQuick.Controls

import EasyApp.Gui.Globals as EaGlobals
import EasyApp.Gui.Style as EaStyle
import EasyApp.Gui.Animations as EaAnimations
import EasyApp.Gui.Elements as EaElements
import EasyApp.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

// Scroll bar display mode.
// Internally, use enum type name: ScrollBarMode.Indicator, etc.
// Externally, use full module path: EaComponents.ListView.ScrollBarMode.Indicator, etc.
// Cannot use id (listView.Indicator) — enums are type-scoped, not instance-scoped.
// Cannot use ListView.Indicator — resolves to QtQuick's ListView, not this component.
enum ScrollBarMode {
Indicator,
AsNeeded,
AlwaysOn
}
property int scrollBarMode: ScrollBarMode.Indicator

// Allow ctrl/shift multi-select.
property bool multiSelection: true

// Column widths definition. Each entry is a width in px, or -1 to fill remaining space.
// Example: columnWidths: [40, -1, 100]
property var columnWidths: []

// Clear all selection and reset anchor.
function clearSelection() {
selectionModel.clearSelection()
anchorRow = -1
}

// ── 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

// 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)
readonly property var resolvedColumnWidths: {
if (!columnWidths.length) return []
let fixed = 0, flexCount = 0
for (let w of columnWidths) {
if (w > 0) fixed += w
else flexCount++
}
const spacing = EaStyle.Sizes.tableColumnSpacing * (columnWidths.length - 1)
const border = EaStyle.Sizes.borderThickness * 2
const fill = flexCount > 0 ? Math.max(0, (width - fixed - spacing - border) / flexCount) : 0
return columnWidths.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) {
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
if (listView.multiSelection && modifiers & Qt.ControlModifier) {
selectionModel.select(
idx,
ItemSelectionModel.Toggle | 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

highlightMoveDuration: EaStyle.Sizes.tableHighlightMoveDuration
highlight: Rectangle {
z: 2
color: mouseHoverHandler.hovered ?
EaStyle.Colors.tableHighlight :
"transparent"
Behavior on color { EaAnimations.ThemeChange {} }
}

HoverHandler {
id: mouseHoverHandler
acceptedDevices: PointerDevice.AllDevices
blocking: false
}

ScrollBar.vertical: EaElements.ScrollBar {
// ScrollBarMode enum not in scope inside child objects; use int values.
// AlwaysOn=2, AsNeeded=1
policy: listView.scrollBarMode === 2 ? ScrollBar.AlwaysOn
: listView.scrollBarMode === 1 ? ScrollBar.AsNeeded
: ScrollBar.AlwaysOff
topInset: listView._headerHeight
topPadding: listView.padding + listView._headerHeight
}

ScrollIndicator.vertical: EaElements.ScrollIndicator {
// Indicator=0
active: listView.scrollBarMode === 0
topInset: listView._headerHeight
topPadding: listView.padding + 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"
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
}
}
}
94 changes: 94 additions & 0 deletions src/EasyApp/Gui/Components/ListViewDelegate.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import QtQuick

import EasyApp.Gui.Style as EaStyle
import EasyApp.Gui.Animations as EaAnimations

Rectangle {
id: control

default property alias contentRowData: contentRow.data
property Item listView: ListView.view ?? null

implicitWidth: listView.width
implicitHeight: listView.tableRowHeight

color: {
// Read selectedIndexes to create a binding dependency — forces
// this color expression to re-evaluate whenever the selection changes.
listView.selectedIndexes

let selected = index >= 0 && listView.isSelected(index)
let c1 = EaStyle.Colors.themeAccentMinor
let c2 = EaStyle.Colors.themeBackgroundHovered2
let c3 = EaStyle.Colors.themeBackgroundHovered1

return selected ? c1 : (index % 2 ? c2 : c3)
}
Behavior on color { EaAnimations.ThemeChange {} }

Component.onCompleted: if (listView) listView.applyWidths(contentRow)

Connections {
target: listView
function onResolvedColumnWidthsChanged() { listView.applyWidths(contentRow) }
}

Row {
id: contentRow

height: parent.height
spacing: EaStyle.Sizes.tableColumnSpacing
}

// Anchor indicator: small triangle in top-right corner when row is
// the shift-selection anchor but not currently selected.
Item {
visible: {
// Read selectedIndexes to create binding dependency for reactivity.
listView.selectedIndexes
return index === listView.anchorRow && !listView.isSelected(index)
}
anchors.top: parent.top
anchors.right: parent.right
width: 8
height: 8
clip: true
layer.enabled: true
layer.smooth: false

Rectangle {
width: parent.width * 1.5
height: parent.height * 1.5
rotation: 45
x: Math.round(parent.width / 2)
y: Math.round(-parent.height * 0.75)
antialiasing: false
color: EaStyle.Colors.themeAccentMinor
Behavior on color { EaAnimations.ThemeChange {} }
}
}

//Mouse area to react on click events
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: false
onClicked: (mouse) => {
if (index >= 0)
listView.selectWithModifiers(index, mouse.modifiers)
}
}

// Mouse-only navigation: highlight follows hover.
// If keyboard navigation is added, guard this with a keyboardActive flag.
HoverHandler {
id: mouseHoverHandler
acceptedDevices: PointerDevice.AllDevices
cursorShape: Qt.PointingHandCursor
blocking: false
onHoveredChanged: {
if (hovered && index >= 0)
listView.currentIndex = index
}
}
}
33 changes: 33 additions & 0 deletions src/EasyApp/Gui/Components/ListViewHeader.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Controls

import EasyApp.Gui.Style as EaStyle
import EasyApp.Gui.Animations as EaAnimations

Rectangle {
id: listViewHeader
default property alias contentRowData: contentRow.data
property Item listView: ListView.view ?? null

z: 3 // To display header above delegate and highlighted area

implicitWidth: parent === null ? 0 : parent.width
implicitHeight: listView ? listView.tableRowHeight : 0

color: EaStyle.Colors.contentBackground
Behavior on color { EaAnimations.ThemeChange {} }

Component.onCompleted: if (listView) listView.applyWidths(contentRow)

Connections {
target: listView
function onResolvedColumnWidthsChanged() { listView.applyWidths(contentRow) }
}

Row {
id: contentRow

height: parent.height
spacing: EaStyle.Sizes.tableColumnSpacing
}
}
3 changes: 3 additions & 0 deletions src/EasyApp/Gui/Components/qmldir
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ SideBarColumn 1.0 SideBarColumn.qml
PreferencesDialog 1.0 PreferencesDialog.qml
ProjectDescriptionDialog 1.0 ProjectDescriptionDialog.qml

ListView 1.0 ListView.qml
ListViewDelegate 1.0 ListViewDelegate.qml
ListViewHeader 1.0 ListViewHeader.qml
TableView 1.0 TableView.qml
TableViewHeader 1.0 TableViewHeader.qml
TableViewDelegate 1.0 TableViewDelegate.qml
Expand Down