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