diff --git a/examples/gallerydata/contents/ui/gallery/ListViewGallery.qml b/examples/gallerydata/contents/ui/gallery/ListViewGallery.qml --- a/examples/gallerydata/contents/ui/gallery/ListViewGallery.qml +++ b/examples/gallerydata/contents/ui/gallery/ListViewGallery.qml @@ -20,7 +20,7 @@ import QtQuick 2.4 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.0 as Controls -import org.kde.kirigami 2.4 as Kirigami +import org.kde.kirigami 2.5 as Kirigami Kirigami.ScrollablePage { id: page @@ -81,20 +81,23 @@ } } - ListView { - Timer { - id: refreshRequestTimer - interval: 3000 - onTriggered: page.refreshing = false - } - model: 200 - delegate: Kirigami.SwipeListItem { + Component { + id: delegateComponent + Kirigami.SwipeListItem { id: listItem - contentItem: Controls.Label { - height: Math.max(implicitHeight, Kirigami.Units.iconSizes.smallMedium) - anchors.verticalCenter: parent.verticalCenter - text: "Item " + modelData - color: listItem.checked || (listItem.pressed && !listItem.checked && !listItem.sectionDelegate) ? listItem.activeTextColor : listItem.textColor + contentItem: RowLayout { + Kirigami.ListItemDragHandle { + listItem: listItem + listView: mainList + onMoveRequested: listModel.move(oldIndex, newIndex, 1) + } + + Controls.Label { + Layout.fillWidth: true + height: Math.max(implicitHeight, Kirigami.Units.iconSizes.smallMedium) + text: model.title + color: listItem.checked || (listItem.pressed && !listItem.checked && !listItem.sectionDelegate) ? listItem.activeTextColor : listItem.textColor + } } actions: [ Kirigami.Action { @@ -109,4 +112,34 @@ }] } } + ListView { + id: mainList + Timer { + id: refreshRequestTimer + interval: 3000 + onTriggered: page.refreshing = false + } + model: ListModel { + id: listModel + + Component.onCompleted: { + for (var i = 0; i < 200; ++i) { + listModel.append({"title": "Item " + i, + "actions": [{text: "Action 1", icon: "document-decrypt"}, + {text: "Action 2", icon: "mail-reply-sender"}] + }) + } + } + } + moveDisplaced: Transition { + YAnimator { + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + } + delegate: Kirigami.DelegateRecycler { + width: parent ? parent.width : implicitWidth + sourceComponent: delegateComponent + } + } } diff --git a/kirigami.qrc b/kirigami.qrc --- a/kirigami.qrc +++ b/kirigami.qrc @@ -57,6 +57,7 @@ src/controls/BasicListItem.qml src/controls/AbstractApplicationHeader.qml src/controls/FormLayout.qml + src/controls/ListItemDragHandle.qml src/styles/Material/AbstractListItem.qml src/styles/Material/Theme.qml src/styles/Material/SwipeListItem.qml diff --git a/src/controls/ListItemDragHandle.qml b/src/controls/ListItemDragHandle.qml new file mode 100644 --- /dev/null +++ b/src/controls/ListItemDragHandle.qml @@ -0,0 +1,193 @@ +/* +* Copyright (C) 2018 by Marco Martin +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU Library General Public License as +* published by the Free Software Foundation; either version 2, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Library General Public License for more details +* +* You should have received a copy of the GNU Library General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., +* 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. +*/ + +import QtQuick 2.6 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.4 as Kirigami + +/** + * Implements a drag handle supposed to be in items in ListViews to reorder items + * The ListView must visualize a model which supports item reordering, + * such as ListModel.move() or QAbstractItemModel instances with moveRows() correctly implemented. + * In order for ListItemDragHandle to work correctly, the listItem that is being dragged + * should not directly be the delegate of the ListView, but a child of it. + * + * It is recomended to use DelagateRecycler as base delegate like the following code: + * @code + * ... + * Component { + * id: delegateComponent + * Kirigami.AbstractListItem { + * id: listItem + * contentItem: RowLayout { + * Kirigami.ListItemDragHandle { + * listItem: listItem + * listView: mainList + * onMoveRequested: listModel.move(oldIndex, newIndex, 1) + * } + * Controls.Label { + * text: model.label + * } + * } + * } + * } + * ListView { + * id: mainList + * + * model: ListModel { + * id: listModel + * ListItem { + * lablel: "Item 1" + * } + * ListItem { + * lablel: "Item 2" + * } + * ListItem { + * lablel: "Item 3" + * } + * } + * //this is optional to make list items animated when reordered + * moveDisplaced: Transition { + * YAnimator { + * duration: Kirigami.Units.longDuration + * easing.type: Easing.InOutQuad + * } + * } + * delegate: Kirigami.DelegateRecycler { + * width: mainList.width + * sourceComponent: delegateComponent + * } + * } + * ... + * @endcode + * + * @inherits MouseArea + * @since 2.5 + */ +MouseArea { + id: root + + /** + * listItem: Item + * The id of the delegate that we want to drag around, which *must* + * be a child of the actual ListView's delegate + */ + property Item listItem + + /** + * listView: Listview + * The id of the ListView the delegates belong to. + */ + property ListView listView + + /** + * Emitted when the drag handle wants to move the item in the model + * The following example does the move in the case a ListModel is used + * @code + * onMoveRequested: listModel.move(oldIndex, newIndex, 1) + * @endcode + * @param oldIndex the index the item is currently at + * @param newIndex the index we want to move the item to + */ + signal moveRequested(int oldIndex, int newIndex) + + hoverEnabled: !Kirigami.Settings.tabletMode + + drag { + target: listItem + axis: Drag.YAxis + minimumY: 0 + maximumY: listView.height - listItem.height + } + Kirigami.Icon { + id: internal + source: "handle-sort" + property int startY + property int mouseDownY + property Item originalParent + property int autoScrollThreshold: listItem.height * 3 + opacity: root.pressed || root.containsMouse ? 1 : 0.6 + + function arrangeItem() { + var newIndex = listView.indexAt(1, listView.contentItem.mapFromItem(listItem, 0, 0).y + internal.mouseDownY); + + if (Math.abs(listItem.y - internal.startY) > height && newIndex > -1 && newIndex != index) { + root.moveRequested(index, newIndex); + } + } + + anchors.fill: parent + } + preventStealing: true + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: implicitWidth + + + onPressed: { + internal.originalParent = listItem.parent; + listItem.parent = listView; + listItem.y = internal.originalParent.mapToItem(listItem.parent, listItem.x, listItem.y).y; + internal.originalParent.z = 99; + internal.startY = listItem.y; + internal.mouseDownY = mouse.y; + } + + onPositionChanged: { + internal.arrangeItem(); + + scrollTimer.interval = 500 * Math.max(0.1, (1-Math.max(internal.autoScrollThreshold - listItem.y, listItem.y - listView.height + internal.autoScrollThreshold + listItem.height) / internal.autoScrollThreshold)); + scrollTimer.running = (listItem.y < internal.autoScrollThreshold || + listItem.y > listView.height - internal.autoScrollThreshold); + } + onReleased: { + listItem.y = internal.originalParent.mapFromItem(listItem, 0, 0).y; + listItem.parent = internal.originalParent; + dropAnimation.running = true; + scrollTimer.running = false; + } + onCanceled: released() + SequentialAnimation { + id: dropAnimation + YAnimator { + target: listItem + from: listItem.y + to: 0 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + PropertyAction { + target: listItem.parent + property: "z" + value: 0 + } + } + Timer { + id: scrollTimer + interval: 500 + repeat: true + onTriggered: { + if (listItem.y < internal.autoScrollThreshold) { + listView.contentY = Math.max(0, listView.contentY - Kirigami.Units.gridUnit) + } else { + listView.contentY = Math.min(listView.contentHeight - listView.height, listView.contentY + Kirigami.Units.gridUnit) + } + internal.arrangeItem(); + } + } +} diff --git a/src/controls/private/DefaultListItemBackground.qml b/src/controls/private/DefaultListItemBackground.qml --- a/src/controls/private/DefaultListItemBackground.qml +++ b/src/controls/private/DefaultListItemBackground.qml @@ -38,22 +38,15 @@ ColorAnimation { duration: Units.longDuration } } - readonly property bool _firstElement: typeof(index) !== "undefined" && index == 0 + readonly property bool __separatorVisible: listItem.separatorVisible - on_FirstElementChanged: { - if (_firstElement) { + on__SeparatorVisibleChanged: { + if (__separatorVisible) { var newObject = Qt.createQmlObject('import QtQuick 2.0; import org.kde.kirigami 2.4; Separator {anchors {left: parent.left; right: parent.right; bottom: parent.top} visible: listItem.separatorVisible}', background); + newObject = Qt.createQmlObject('import QtQuick 2.0; import org.kde.kirigami 2.4; Separator {anchors {left: parent.left; right: parent.right; bottom: parent.bottom} visible: listItem.separatorVisible}', + background); } } - - Separator { - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - visible: listItem.separatorVisible - } } diff --git a/src/controls/templates/SwipeListItem.qml b/src/controls/templates/SwipeListItem.qml --- a/src/controls/templates/SwipeListItem.qml +++ b/src/controls/templates/SwipeListItem.qml @@ -149,7 +149,15 @@ //TODO: a global "open" state enabled: background.x !== 0 property bool indicateActiveFocus: listItem.pressed || Settings.tabletMode || listItem.activeFocus || (view ? view.activeFocus : false) - property Flickable view: listItem.ListView.view || listItem.parent.ListView.view + property Flickable view: listItem.ListView.view || (listItem.parent ? (listItem.parent.ListView.view || listItem.parent) : null) + onViewChanged: { + if (view && Settings.tabletMode && !behindItem.view.parent.parent._swipeFilter) { + var component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml")); + behindItem.view.parent.parent._swipeFilter = component.createObject(behindItem.view.parent.parent); + print("SSS"+behindItem.view.parent.parent._swipeFilter+internal.swipeFilterItem+" "+(behindItem.view && behindItem.view.parent && behindItem.view.parent.parent)) + } + } + anchors { fill: parent } @@ -339,17 +347,13 @@ } Component.onCompleted: { //this will happen only once - if (Settings.tabletMode && !swipeFilterConnection.swipeFilterItem) { - var component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml")); - behindItem.view.parent.parent._swipeFilter = component.createObject(behindItem.view.parent.parent); - } listItem.contentItemChanged(); } Connections { target: Settings onTabletModeChanged: { if (Settings.tabletMode) { - if (!swipeFilterConnection.swipeFilterItem) { + if (!internal.swipeFilterItem) { var component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml")); listItem.ListView.view.parent.parent._swipeFilter = component.createObject(listItem.ListView.view.parent.parent); } @@ -363,20 +367,27 @@ } } } + QtObject { + id: internal + readonly property QtObject swipeFilterItem: (behindItem.view && behindItem.view.parent && behindItem.view.parent.parent) ? behindItem.view.parent.parent._swipeFilter : null + + readonly property bool edgeEnabled: swipeFilterItem ? swipeFilterItem.currentItem === listItem || swipeFilterItem.currentItem === listItem.parent : false + } + Connections { id: swipeFilterConnection - readonly property QtObject swipeFilterItem: (behindItem.view && behindItem.view && behindItem.view.parent && behindItem.view.parent.parent) ? behindItem.view.parent.parent._swipeFilter : null - readonly property bool enabled: swipeFilterItem ? swipeFilterItem.currentItem === listItem : false - target: enabled ? swipeFilterItem : null - onPeekChanged: listItem.background.x = -(listItem.background.width - listItem.background.height) * swipeFilterItem.peek + + target: internal.edgeEnabled ? internal.swipeFilterItem : null + onPeekChanged: listItem.background.x = -(listItem.background.width - listItem.background.height) * internal.swipeFilterItem.peek onCurrentItemChanged: { - if (!enabled) { + if (!internal.edgeEnabled) { positionAnimation.to = 0; positionAnimation.from = background.x; positionAnimation.running = true; } } } + //END signal handlers Accessible.role: Accessible.ListItem diff --git a/src/delegaterecycler.h b/src/delegaterecycler.h --- a/src/delegaterecycler.h +++ b/src/delegaterecycler.h @@ -72,9 +72,15 @@ Q_SIGNALS: void sourceComponentChanged(); +private Q_SLOTS: + void syncIndex(); + void syncModel(); + void syncModelData(); + private: QPointer m_sourceComponent; QPointer m_item; + QObject *m_propertiesTracker = nullptr; bool m_updatingSize = false; }; diff --git a/src/delegaterecycler.cpp b/src/delegaterecycler.cpp --- a/src/delegaterecycler.cpp +++ b/src/delegaterecycler.cpp @@ -115,6 +115,24 @@ } } +void DelegateRecycler::syncIndex() +{ + QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); + ctx->setContextProperty(QStringLiteral("index"), m_propertiesTracker->property("trackedIndex")); +} + +void DelegateRecycler::syncModel() +{ + QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); + ctx->setContextProperty(QStringLiteral("model"), m_propertiesTracker->property("trackedModel")); +} + +void DelegateRecycler::syncModelData() +{ + QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); + ctx->setContextProperty(QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData")); +} + QQmlComponent *DelegateRecycler::sourceComponent() const { return m_sourceComponent; @@ -129,6 +147,18 @@ if (m_sourceComponent == component) { return; } + + if (!m_propertiesTracker) { + QQmlComponent *propertiesTrackerComponent = new QQmlComponent(qmlEngine(this), this); + + propertiesTrackerComponent->setData(QByteArrayLiteral("import QtQuick 2.3\nQtObject{property int trackedIndex: index; property var trackedModel: typeof model != 'undefined' ? model : null; property var trackedModelData: typeof modelData != 'undefined' ? modelData : null}"), QUrl()); + m_propertiesTracker = propertiesTrackerComponent->create(QQmlEngine::contextForObject(this)); + + connect(m_propertiesTracker, SIGNAL(trackedIndexChanged()), this, SLOT(syncIndex())); + connect(m_propertiesTracker, SIGNAL(trackedModelChanged()), this, SLOT(syncModel())); + connect(m_propertiesTracker, SIGNAL(trackedModelDataChanged()), this, SLOT(syncModelData())); + } + if (m_sourceComponent) { if (m_item) { disconnect(m_item.data(), &QQuickItem::implicitWidthChanged, this, &DelegateRecycler::updateHints); @@ -156,24 +186,21 @@ } } - QQmlContext *myCtx = QQmlEngine::contextForObject(this); - ctx->setContextProperty(QStringLiteral("model"), myCtx->contextProperty(QStringLiteral("model"))); - ctx->setContextProperty(QStringLiteral("modelData"), myCtx->contextProperty(QStringLiteral("modelData"))); - ctx->setContextProperty(QStringLiteral("index"), myCtx->contextProperty(QStringLiteral("index"))); + ctx->setContextProperty(QStringLiteral("model"), m_propertiesTracker->property("trackedModel")); + ctx->setContextProperty(QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData")); + ctx->setContextProperty(QStringLiteral("index"), m_propertiesTracker->property("trackedIndex")); QObject * obj = component->create(ctx); m_item = qobject_cast(obj); if (!m_item) { obj->deleteLater(); } } else { - QQmlContext *myCtx = QQmlEngine::contextForObject(this); QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext(); - QObject *model = myCtx->contextProperty(QStringLiteral("model")).value(); - ctx->setContextProperty(QStringLiteral("model"), QVariant::fromValue(model)); - ctx->setContextProperty(QStringLiteral("modelData"), myCtx->contextProperty(QStringLiteral("modelData"))); - ctx->setContextProperty(QStringLiteral("index"), myCtx->contextProperty(QStringLiteral("index"))); + ctx->setContextProperty(QStringLiteral("model"), m_propertiesTracker->property("trackedModel")); + ctx->setContextProperty(QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData")); + ctx->setContextProperty(QStringLiteral("index"), m_propertiesTracker->property("trackedIndex")); } if (m_item) { @@ -194,7 +221,7 @@ void DelegateRecycler::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { - if (m_item && newGeometry != oldGeometry) { + if (m_item && newGeometry.size() != oldGeometry.size()) { updateSize(true); } QQuickItem::geometryChanged(newGeometry, oldGeometry); diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -166,9 +166,11 @@ qmlRegisterType(componentUrl(QStringLiteral("CardsLayout.qml")), uri, 2, 4, "CardsLayout"); qmlRegisterType(componentUrl(QStringLiteral("InlineMessage.qml")), uri, 2, 4, "InlineMessage"); qmlRegisterUncreatableType(uri, 2, 4, "MessageType", "Cannot create objects of type MessageType"); - qmlRegisterType(uri, 2, 4, "DelegateRecycler"); + //2.5 + qmlRegisterType(componentUrl(QStringLiteral("ListItemDragHandle.qml")), uri, 2, 5, "ListItemDragHandle"); + qmlProtectModule(uri, 2); } diff --git a/src/qmldir b/src/qmldir --- a/src/qmldir +++ b/src/qmldir @@ -42,3 +42,5 @@ CardsListView 2.4 CardsListView.qml CardsGridView 2.4 CardsGridView.qml InlineMessage 2.4 InlineMessage.qml +ListItemDragHandle 2.5 ListItemDragHandle.qml + diff --git a/src/styles/Material/SwipeListItem.qml b/src/styles/Material/SwipeListItem.qml --- a/src/styles/Material/SwipeListItem.qml +++ b/src/styles/Material/SwipeListItem.qml @@ -60,7 +60,6 @@ } } background: DefaultListItemBackground { - clip: true //TODO: this will have to reuse QQC2.1 Ripple Rectangle { id: ripple