diff --git a/chameleon/CMakeLists.txt b/chameleon/CMakeLists.txt
index e7b6ca9f9..fd2ad28e9 100644
--- a/chameleon/CMakeLists.txt
+++ b/chameleon/CMakeLists.txt
@@ -24,6 +24,7 @@ set(QML_FILES
Frame.qml
GroupBox.qml
ItemDelegate.qml
+ ListView.qml
MenuItem.qml
Menu.qml
MenuSeparator.qml
diff --git a/chameleon/ListView.qml b/chameleon/ListView.qml
new file mode 100644
index 000000000..43fe1d9c0
--- /dev/null
+++ b/chameleon/ListView.qml
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import org.deepin.dtk 1.0 as D
+
+D.ListView {
+
+}
diff --git a/qt6/src/dtkdeclarative_qml.qrc b/qt6/src/dtkdeclarative_qml.qrc
index fb83f85c9..899a09ac3 100644
--- a/qt6/src/dtkdeclarative_qml.qrc
+++ b/qt6/src/dtkdeclarative_qml.qrc
@@ -63,6 +63,7 @@
qml/private/ButtonPanel.qml
qml/ActionButton.qml
qml/ItemDelegate.qml
+ qml/ListView.qml
qml/CheckDelegate.qml
qml/TipsSlider.qml
qml/SliderTipItem.qml
diff --git a/qt6/src/qml.cmake b/qt6/src/qml.cmake
index 998efbbfe..0a40268ad 100644
--- a/qt6/src/qml.cmake
+++ b/qt6/src/qml.cmake
@@ -52,6 +52,7 @@ set(QML_DTK_CONTROLS
"qml/ComboBox.qml"
"qml/ActionButton.qml"
"qml/ItemDelegate.qml"
+ "qml/ListView.qml"
"qml/CheckDelegate.qml"
"qml/TipsSlider.qml"
"qml/SliderTipItem.qml"
@@ -98,6 +99,7 @@ set(QML_DTK_CONTROLS
"qml/PlaceholderText.qml"
"qml/ControlGroup.qml"
"qml/ControlGroupItem.qml"
+ "qml/DragItemsImage.qml"
)
foreach(QML_FILE ${QML_DTK_CONTROLS})
diff --git a/qt6/src/qml/CheckDelegate.qml b/qt6/src/qml/CheckDelegate.qml
index 8db95ae2c..ec1cb3801 100644
--- a/qt6/src/qml/CheckDelegate.qml
+++ b/qt6/src/qml/CheckDelegate.qml
@@ -12,6 +12,8 @@ T.CheckDelegate {
id: control
property Component content
property D.Palette backgroundColor: DS.Style.itemDelegate.checkBackgroundColor
+ property string indicatorIcon: control.checkState === Qt.Unchecked ? "item_unchecked" : "item_checked"
+ property bool indicatorVisible: control.checked
implicitWidth: DS.Style.control.implicitWidth(control)
implicitHeight: DS.Style.control.implicitHeight(control)
@@ -26,15 +28,22 @@ T.CheckDelegate {
indicator: Loader {
x: control.mirrored ? control.leftPadding : control.width - width - control.rightPadding
y: control.topPadding + (control.availableHeight - height) / 2
- active: control.checked
+ active: indicatorVisible
sourceComponent: D.DciIcon {
palette: control.D.DTK.makeIconPalette(control.palette)
mode: control.D.ColorSelector.controlState
theme: control.D.ColorSelector.controlTheme
- name: "menu_select"
+ name: indicatorIcon
sourceSize: Qt.size(DS.Style.itemDelegate.checkIndicatorIconSize, DS.Style.itemDelegate.checkIndicatorIconSize)
fallbackToQIcon: false
+ onNameChanged: {
+ play(D.DTK.NormalState);
+ }
+ Component.onCompleted: {
+ if (indicatorVisible)
+ play(D.DTK.NormalState);
+ }
}
}
@@ -58,12 +67,12 @@ T.CheckDelegate {
}
}
- background: Item {
+ background: Control {
implicitWidth: DS.Style.itemDelegate.width
implicitHeight: DS.Style.itemDelegate.height
Rectangle {
anchors.fill: parent
- visible: !checked
+ visible: !checked && !control.ListView.view
color: control.D.ColorSelector.backgroundColor
radius: DS.Style.control.radius
}
@@ -74,4 +83,12 @@ T.CheckDelegate {
radius: DS.Style.control.radius
}
}
+
+ onHoveredChanged: {
+ if (checked || !ListView.view)
+ return
+
+ if (ListView.view)
+ ListView.view.setHoverItem(control.hovered ? control : null)
+ }
}
diff --git a/qt6/src/qml/DragItemsImage.qml b/qt6/src/qml/DragItemsImage.qml
new file mode 100644
index 000000000..934b362b0
--- /dev/null
+++ b/qt6/src/qml/DragItemsImage.qml
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.15
+
+Item {
+ id: control
+ property list- items
+
+ signal aboutToGrabToImage(Item item)
+ signal grabToImageFinished(Item item)
+
+ onItemsChanged: function updateGeomotry() {
+ var w = 0
+ var h = 0
+ for (var i = 0; i < control.items.length; ++i) {
+ let item = control.items[i]
+ w = Math.max(w, item.width)
+ h = Math.max(h, item.height)
+ }
+ control.width = w + 30
+ control.height = h * 2 + 30
+ }
+
+ function addItem(item) {
+ if (control.items.include(item))
+ return
+
+ items.push(item)
+ }
+
+ function removeItem(item) {
+ if (!control.items.include(item))
+ return
+
+ var index = control.items.indexOf(item);
+ if (index !== -1) {
+ control.items.splice(index, 1);
+ }
+ }
+
+ Repeater {
+ model: control.items.length
+ delegate: Image {
+ id: img
+ anchors.centerIn: parent
+ antialiasing: true
+ rotation: index ? (index % 2 === 0 ? 10 : -10) : 0
+ opacity: (1 - index * 0.2)
+ z: -index
+
+ Component.onCompleted: {
+ let item = control.items[index]
+ if (!item)
+ return
+
+ // item.dragActive = true
+ aboutToGrabToImage(item)
+ item.grabToImage(function(result) {
+ img.source = result.url
+ grabToImageFinished(item)
+ })
+ }
+ }
+ }
+
+ RoundButton {
+ id: number
+ checked: true
+ anchors.right: dragItem.right
+ anchors.top: dragItem.top
+ anchors.margins: 8
+ implicitWidth: 24
+ implicitHeight: 24
+ text: control.items.length > 1 ? control.items.length : ""
+ opacity: control.items.length > 1 ? 1 : 0
+ }
+}
diff --git a/qt6/src/qml/ItemDelegate.qml b/qt6/src/qml/ItemDelegate.qml
index 0a2acca4d..f8c5a205c 100644
--- a/qt6/src/qml/ItemDelegate.qml
+++ b/qt6/src/qml/ItemDelegate.qml
@@ -1,12 +1,13 @@
-// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd.
+// SPDX-FileCopyrightText: 2022-2024 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
-import QtQuick 2.11
+import QtQuick
import QtQuick.Templates as T
import QtQuick.Layouts 1.11
import org.deepin.dtk 1.0 as D
import org.deepin.dtk.style 1.0 as DS
+import org.deepin.dtk.private 1.0 as P
T.ItemDelegate {
id: control
@@ -17,6 +18,12 @@ T.ItemDelegate {
property Component content
property D.Palette checkedTextColor: DS.Style.checkedButton.text
property int corners: D.RoundRectangle.TopLeftCorner | D.RoundRectangle.TopRightCorner | D.RoundRectangle.BottomLeftCorner | D.RoundRectangle.BottomRightCorner
+ property bool dragActive: false
+ // drag
+ property bool enableDrag: false
+ Drag.mimeData: { "text/plain": control.text }
+ Drag.dragType: Drag.Automatic
+
function getCornersForBackground(index, count) {
if (count <= 1)
return D.RoundRectangle.TopLeftCorner | D.RoundRectangle.TopRightCorner | D.RoundRectangle.BottomLeftCorner | D.RoundRectangle.BottomRightCorner
@@ -35,7 +42,7 @@ T.ItemDelegate {
spacing: DS.Style.control.spacing
checkable: true
autoExclusive: true
- palette.windowText: checked && !control.cascadeSelected ? D.ColorSelector.checkedTextColor : undefined
+ palette.windowText: checked && !control.cascadeSelected && control.backgroundVisible && !dragActive? D.ColorSelector.checkedTextColor : undefined
D.DciIcon.mode: D.ColorSelector.controlState
D.DciIcon.theme: D.ColorSelector.controlTheme
@@ -83,12 +90,13 @@ T.ItemDelegate {
}
background: Item {
+ visible: backgroundVisible
implicitWidth: DS.Style.itemDelegate.width
implicitHeight: DS.Style.itemDelegate.height
Loader {
anchors.fill: parent
- active: checked && !control.cascadeSelected
+ active: checked && !control.cascadeSelected && !dragActive
sourceComponent: HighlightPanel {}
}
@@ -104,12 +112,57 @@ T.ItemDelegate {
Loader {
anchors.fill: parent
- active: !checked && control.backgroundVisible
+ active: !control.ListView.view && !checked && control.backgroundVisible
sourceComponent: D.RoundRectangle {
color: DS.Style.itemDelegate.normalColor
radius: DS.Style.control.radius
corners: control.corners
}
}
+
+ Loader {
+ anchors.fill: parent
+ active: dragActive
+ sourceComponent: Rectangle {
+ border.color: Qt.rgba(0, 0, 0, 0.09)
+ radius: DS.Style.control.radius
+ }
+ }
+ }
+
+ DragHandler {
+ id: dragHandler
+ enabled: enableDrag
+ onActiveChanged: {
+ if (active) {
+ let dragItem = control.ListView.view.dragItem
+ if (!dragItem)
+ return
+
+ let md = JSON.stringify(dragItem.Drag.mimeData)
+
+ if (md.length > 2) // '{}'
+ control.Drag.mimeData = dragItem.Drag.mimeData
+
+ dragItem.grabToImage(function(result) {
+ control.Drag.imageSource = result.url;
+ control.Drag.hotSpot = Qt.point(dragItem.width / 2, dragItem.height / 2)
+ control.Drag.active = true
+ })
+ }
+ }
+ }
+
+ onHoveredChanged: {
+ if (checked || control.cascadeSelected || !backgroundVisible || dragActive)
+ return
+
+ if (ListView.view)
+ ListView.view.setHoverItem(control.hovered ? control : null)
+ }
+
+ onCheckedChanged: {
+ if (ListView.view)
+ ListView.view.updateCheckedItems()
}
}
diff --git a/qt6/src/qml/ListView.qml b/qt6/src/qml/ListView.qml
new file mode 100644
index 000000000..562839d36
--- /dev/null
+++ b/qt6/src/qml/ListView.qml
@@ -0,0 +1,194 @@
+// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Templates as T
+import org.deepin.dtk 1.0 as D
+import org.deepin.dtk.style 1.0 as DS
+import org.deepin.dtk.private 1.0 as P
+
+ListView {
+ id: control
+ property int duration: 100
+ property bool bgVisible: false
+ property Item hoveredItem
+ property list
- checkedItems
+ property alias dragItem: dragItem
+
+ function setHoverItem(item) {
+ if (item) {
+ hoveredItem = item
+ // if hover from null to item do not show animation
+ bgVisible = (background.x <= 0 && background.y <= 0) ? false : hoveredItem.hovered
+ background.x = hoveredItem.x - contentX
+ background.y = hoveredItem.y - contentY
+ background.width = hoveredItem.implicitWidth
+ background.height = hoveredItem.implicitHeight
+ bgVisible = hoveredItem.hovered
+ control.state = bgVisible ? "Visible" : "Invisible"
+ } else {
+ hideTimer.start()
+ }
+ }
+
+ function updateCheckedItems() {
+ var items = []
+ for (var i = 0; i < count; ++i) {
+ let item = itemAtIndex(i)
+ if (item && item.checked) {
+ items.push(item)
+ }
+ }
+
+ checkedItems = items
+ }
+
+ DragItemsImage {
+ id: dragItem
+ items: checkedItems
+ visible: Drag.active
+ // Drag.mimeData: {"text/plain": ""}
+
+ onAboutToGrabToImage: function(item) {
+ item.dragActive = true
+ }
+ onGrabToImageFinished: function(item) {
+ item.dragActive = false
+ }
+ }
+
+ onContentXChanged: {
+ if (hoveredItem && hoveredItem.hovered && background)
+ background.x = hoveredItem.x - contentX
+ }
+
+ onContentYChanged: {
+ if (hoveredItem && hoveredItem.hovered && background)
+ background.y = hoveredItem.y - contentY
+ }
+
+ // ItemDelegate hover item0 ==> item1, add timer ignore [item0.unhovered]
+ // item0.hovered, item0.unhovered, item1.hovered
+ Timer {
+ id: hideTimer
+ interval: control.duration
+ onTriggered: {
+ bgVisible = hoveredItem ? hoveredItem.hovered : false
+ control.state = bgVisible ? "Visible" : "Invisible"
+ }
+ }
+
+ // visible animation
+ states: [
+ State{
+ name: "Visible"
+ PropertyChanges { target: background; opacity: 1.0 }
+ PropertyChanges { target: background; scale: 1.0 }
+ PropertyChanges { target: background; visible: true }
+ },
+ State{
+ name:"Invisible"
+ PropertyChanges { target: background; opacity: 0.0 }
+ PropertyChanges { target: background; scale: 0.5 }
+ PropertyChanges { target: background; visible: false }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ from: "Visible"
+ to: "Invisible"
+
+ SequentialAnimation {
+ ParallelAnimation {
+ NumberAnimation {
+ target: background
+ property: "opacity"
+ duration: control.duration
+ easing.type: Easing.InOutQuad
+ }
+ NumberAnimation {
+ target: background
+ property: "scale"
+ duration: control.duration
+ easing.type: Easing.InOutQuad
+ }
+ }
+ NumberAnimation {
+ target: background
+ property: "visible"
+ duration: 0
+ }
+ }
+ },
+ Transition {
+ from: "Invisible"
+ to: "Visible"
+ SequentialAnimation {
+ NumberAnimation {
+ target: background
+ property: "visible"
+ duration: 0
+ }
+ ParallelAnimation {
+ NumberAnimation {
+ target: background
+ property: "opacity"
+ duration: control.duration
+ easing.type: Easing.InOutQuad
+ }
+ NumberAnimation {
+ target: background
+ property: "scale"
+ duration: control.duration
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+ }
+ ]
+
+ Item {
+ id: background
+ implicitWidth: DS.Style.itemDelegate.width
+ implicitHeight: DS.Style.itemDelegate.height
+ z: -1
+
+ Loader {
+ anchors.fill: parent
+ active: hoveredItem
+ sourceComponent: P.ButtonPanel {
+ button: hoveredItem
+ outsideBorderColor: null
+ visible: hoveredItem && !hoveredItem.checked && hoveredItem.enabled
+ }
+ }
+
+ onVisibleChanged: {
+ if (!visible) {
+ // ensure do not show animation when hoveredItem from null to item
+ background.x = -1
+ background.y = -1
+ }
+ }
+
+ // move and resize animation
+ Behavior on x {
+ enabled: bgVisible
+ NumberAnimation { duration: control.duration }
+ }
+ Behavior on y {
+ enabled: bgVisible
+ NumberAnimation { duration: control.duration }
+ }
+ Behavior on width {
+ enabled: bgVisible
+ NumberAnimation { duration: control.duration }
+ }
+ Behavior on height {
+ enabled: bgVisible
+ NumberAnimation { duration: control.duration }
+ }
+ }
+}
diff --git a/src/dtkdeclarative_assets.qrc b/src/dtkdeclarative_assets.qrc
index e4d3ec914..689488046 100644
--- a/src/dtkdeclarative_assets.qrc
+++ b/src/dtkdeclarative_assets.qrc
@@ -89,6 +89,8 @@
icons/bloom/list_delete.dci
icons/bloom/menu_arrow.dci
icons/bloom/menu_select.dci
+ icons/bloom/item_checked.dci
+ icons/bloom/item_unchecked.dci
icons/bloom/window_sidebar.dci
icons/bloom/action_add.dci
icons/bloom/radio_checked.dci
diff --git a/src/icons/bloom/item_checked.dci b/src/icons/bloom/item_checked.dci
new file mode 100644
index 000000000..e38bb1031
Binary files /dev/null and b/src/icons/bloom/item_checked.dci differ
diff --git a/src/icons/bloom/item_unchecked.dci b/src/icons/bloom/item_unchecked.dci
new file mode 100644
index 000000000..4da8717c0
Binary files /dev/null and b/src/icons/bloom/item_unchecked.dci differ