import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons Item { id: root // Public API property var model: [] property var disabledIds: [] property color activeColor: Color.mPrimary property color activeOnColor: Color.mOnPrimary property color dragHandleColor: Color.mOutline property int baseSize: Style.baseWidgetSize * 0.7 property int spacing: Style.marginM signal itemToggled(int index, bool enabled) signal itemsReordered(int fromIndex, int toIndex) signal dragPotentialStarted signal dragPotentialEnded implicitHeight: listView.contentHeight function toggleItem(index) { if (index < 0 || index >= root.model.length) return; var item = root.model[index]; if (item.required) return; // Create a new array to trigger binding update var newModel = root.model.slice(); newModel[index] = Object.assign({}, item, { "enabled": !item.enabled }); root.model = newModel; root.itemToggled(index, newModel[index].enabled); } function moveItem(fromIndex, toIndex) { if (fromIndex === toIndex) return; if (fromIndex < 0 || fromIndex >= root.model.length) return; if (toIndex < 0 || toIndex >= root.model.length) return; // Create a new array with item moved var newModel = root.model.slice(); var item = newModel.splice(fromIndex, 1)[0]; newModel.splice(toIndex, 0, item); root.model = newModel; root.itemsReordered(fromIndex, toIndex); } ListView { id: listView anchors.fill: parent spacing: root.spacing interactive: false clip: true model: root.model delegate: Item { id: delegateItem width: listView.width height: checkboxRow.height required property int index required property var modelData property string text: modelData.text || "" property bool enabled: modelData.enabled || false property bool required: modelData.required || false readonly property bool isDisabled: (root.disabledIds || []).indexOf(modelData.id) !== -1 readonly property bool canDrag: !delegateItem.isDisabled property bool dragging: false property int dragStartY: 0 property int dragStartIndex: -1 property int dragTargetIndex: -1 property int itemSpacing: root.spacing RowLayout { id: checkboxRow width: parent.width spacing: Style.marginS opacity: isDisabled ? 0.5 : 1.0 // Drag handle Rectangle { id: dragHandle Layout.preferredWidth: root.baseSize Layout.preferredHeight: root.baseSize radius: Style.iRadiusXS color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : Color.transparent Behavior on color { ColorAnimation { duration: Style.animationFast } } ColumnLayout { anchors.centerIn: parent spacing: Style.marginS Repeater { model: 3 Rectangle { Layout.preferredWidth: root.baseSize * 0.4 Layout.preferredHeight: 2 radius: 1 color: root.dragHandleColor } } } MouseArea { id: dragHandleMouseArea anchors.fill: parent cursorShape: delegateItem.canDrag ? Qt.SizeVerCursor : Qt.ArrowCursor hoverEnabled: true preventStealing: false enabled: delegateItem.canDrag z: 1000 onPressed: mouse => { if (!delegateItem.canDrag) { return; } delegateItem.dragStartIndex = delegateItem.index; delegateItem.dragTargetIndex = delegateItem.index; delegateItem.dragStartY = delegateItem.y; delegateItem.dragging = true; delegateItem.z = 999; // Signal that interaction started (prevents panel close) preventStealing = true; root.dragPotentialStarted(); } onPositionChanged: mouse => { if (delegateItem.dragging) { var dy = mouse.y - dragHandle.height / 2; var newY = delegateItem.y + dy; // Constrain within bounds newY = Math.max(0, Math.min(newY, listView.contentHeight - delegateItem.height)); delegateItem.y = newY; // Calculate target index (but don't apply yet) var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + delegateItem.itemSpacing)); targetIndex = Math.max(0, Math.min(targetIndex, listView.count - 1)); delegateItem.dragTargetIndex = targetIndex; } } onReleased: { // Always signal end of interaction preventStealing = false; root.dragPotentialEnded(); // Apply the model change now that drag is complete if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) { root.moveItem(delegateItem.dragStartIndex, delegateItem.dragTargetIndex); } delegateItem.dragging = false; delegateItem.dragStartIndex = -1; delegateItem.dragTargetIndex = -1; delegateItem.z = 0; } onCanceled: { // Handle cancel (e.g., ESC key pressed during drag) preventStealing = false; root.dragPotentialEnded(); delegateItem.dragging = false; delegateItem.dragStartIndex = -1; delegateItem.dragTargetIndex = -1; delegateItem.z = 0; } } } // Checkbox Rectangle { id: box Layout.preferredWidth: root.baseSize Layout.preferredHeight: root.baseSize radius: Style.iRadiusXS color: delegateItem.enabled ? root.activeColor : Color.mSurface border.color: delegateItem.required ? root.activeColor : Color.mOutline border.width: Style.borderS opacity: delegateItem.required ? 0.7 : 1.0 Behavior on color { ColorAnimation { duration: Style.animationFast } } Behavior on border.color { ColorAnimation { duration: Style.animationFast } } NIcon { visible: delegateItem.enabled anchors.centerIn: parent anchors.horizontalCenterOffset: -1 icon: "check" color: root.activeOnColor pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5) } MouseArea { anchors.fill: parent cursorShape: (!delegateItem.required && !delegateItem.isDisabled) ? Qt.PointingHandCursor : Qt.ArrowCursor enabled: !delegateItem.required && !delegateItem.isDisabled onClicked: { if (!delegateItem.required && !delegateItem.isDisabled) { root.toggleItem(delegateItem.index); } } } } // Label NText { Layout.fillWidth: true text: delegateItem.text color: Color.mOnSurface verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } // Required indicator NText { visible: delegateItem.required text: "(required)" color: Color.mOnSurfaceVariant verticalAlignment: Text.AlignVCenter } } // Position binding for non-dragging state y: { if (delegateItem.dragging) { return delegateItem.y; } // Check if any item is being dragged var draggedIndex = -1; var targetIndex = -1; for (var i = 0; i < listView.count; i++) { var item = listView.itemAtIndex(i); if (item && item.dragging) { draggedIndex = item.dragStartIndex; targetIndex = item.dragTargetIndex; break; } } // If an item is being dragged, adjust positions if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) { var currentIndex = delegateItem.index; if (draggedIndex < targetIndex) { // Dragging down: shift items up between draggedIndex and targetIndex if (currentIndex > draggedIndex && currentIndex <= targetIndex) { return (currentIndex - 1) * (delegateItem.height + delegateItem.itemSpacing); } } else { // Dragging up: shift items down between targetIndex and draggedIndex if (currentIndex >= targetIndex && currentIndex < draggedIndex) { return (currentIndex + 1) * (delegateItem.height + delegateItem.itemSpacing); } } } return delegateItem.index * (delegateItem.height + delegateItem.itemSpacing); } Behavior on y { enabled: !delegateItem.dragging NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutQuad } } } } }