Files
noctalia-shell/Widgets/NReorderCheckboxes.qml
Ly-sec 309648d6d6 Calendar: add timer
LocationTab: rework calendar settings
SoundService: add simple service to play & loop sounds
2025-11-26 19:18:30 +01:00

308 lines
9.7 KiB
QML

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.radiusXS
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.radiusXS
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
}
}
}
}
}