/* * Copyright (C) 2023, KylinSoft Co., Ltd. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ import QtQuick 2.15 import QtQml.Models 2.1 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.15 import org.ukui.menu.core 1.0 import org.ukui.menu.extension 1.0 import org.ukui.quick.platform 1.0 as Platform import org.ukui.quick.items 1.0 as UkuiItems import AppControls2 1.0 as AppControls2 GridView { id: favoriteView cellWidth: itemHeight + spacing; cellHeight: cellWidth signal openFolderSignal(string folderId, string folderName, int x, int y) signal contentShowFinished() property bool isContentShow: true property int exchangedStartIndex: 0 property int spacing: 4 property int itemHeight: 84 property int column: Math.floor(width / cellWidth) property alias viewModel: visualModel property string mergeToAppId: "" property bool isMergeToFolder: false property bool dragTypeIsMerge: false state: isContentShow ? "contentShow" : "contentHidden" states: [ State { name: "contentHidden" PropertyChanges { target: favoriteView; opacity: 0; scale: 0.95 } }, State { name: "contentShow" PropertyChanges { target: favoriteView; opacity: 1; scale: 1 } } ] transitions: [ Transition { to:"contentHidden" SequentialAnimation { PropertyAnimation { properties: "opacity, scale"; duration: 300; easing.type: Easing.InOutCubic } ScriptAction { script: favoriteView.visible = false } } }, Transition { to: "contentShow" PropertyAnimation { properties: "opacity, scale"; duration: 300; easing.type: Easing.InOutCubic } } ] // 按键导航处理(左右键可以循环) focus: true onActiveFocusChanged: currentIndex = 0 onCountChanged: currentIndex = 0 Keys.onRightPressed: { if(currentIndex === count - 1) { currentIndex = 0; return; } currentIndex++; } Keys.onLeftPressed: { if(currentIndex === 0) { currentIndex = count - 1; return; } currentIndex--; } Keys.onDownPressed: { if(currentIndex > count - 1 - column) { return; } currentIndex = currentIndex + column; } Keys.onUpPressed: { if(currentIndex < column) { return; } currentIndex = currentIndex - column; } ScrollBar.vertical: AppControls2.ScrollBar { id: scrollBar visible: viewMouseArea.containsMouse width: 14; height: favoriteView.height } displaced: Transition { NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad; duration: 200 } } model: DelegateModel { id: visualModel delegate: Item { id: container focus: true width: favoriteView.cellWidth height: favoriteView.cellHeight property int visualIndex: DelegateModel.itemsIndex Binding { target: iconItem; property: "visualIndex"; value: visualIndex } states: State { when: activeFocus PropertyChanges { target: iconItem alpha: 0.6 } } Keys.onReturnPressed: { var data = {"id": model.id}; send(data); } Timer { id: delegateDropTimer property int timeOutCount: 0 interval: 300 repeat: true onTriggered: { ++timeOutCount; if (timeOutCount == 1) { mergeToAppId = model.id; isMergeToFolder = (model.type === DataType.Folder); dragTypeIsMerge = true; } } onRunningChanged:timeOutCount = 0 } DropArea { id: delegateDropArea width: 48; height: 48 anchors.top: parent.top anchors.topMargin: 8 anchors.horizontalCenter: parent.horizontalCenter property string enterSourceId: "" onEntered: { if (drag.source.isFolder) { return; } enterSourceId = drag.source.sourceId; delegateDropTimer.running = true; } onExited: { if (delegateDropTimer.timeOutCount < 1) { dragTypeIsMerge = false; visualModel.items.move(drag.source.visualIndex, iconItem.visualIndex); } delegateDropTimer.running = false; enterSourceId = ""; } } UkuiItems.StyleBackground { id: iconItem height: favoriteView.itemHeight; width: height property bool hold: false property int visualIndex: 0 property string sourceId: "" property bool isFolder: model.type === DataType.Folder x: 0; y: 0 radius: 8 useStyleTransparency: false paletteRole: Platform.Theme.Text alpha: hold ? 0 : control.containsPress ? 0.15 : control.containsMouse ? 0.08 : 0 Behavior on scale { NumberAnimation { duration: 300; easing.type: Easing.InOutCubic } } UkuiItems.StyleBackground { width: 52 height: width radius: 14 anchors.top: parent.top anchors.margins: 6 anchors.horizontalCenter: parent.horizontalCenter paletteRole: Platform.Theme.Text useStyleTransparency: false alpha: (delegateDropArea.enterSourceId !== model.id) && delegateDropArea.containsDrag && dragTypeIsMerge ? 0.15 : 0 z: -1 } Loader { id: itemLoader anchors.centerIn: parent property int index: model.index property int type: model.type property string id: model.id property string name: model.name property string icon: model.icon sourceComponent: { if (type === DataType.Normal) { return appItemComponent; } if (type === DataType.Folder) { return folderItemComponent; } } } Component { id: appItemComponent Item { height: iconItem.height width: iconItem.width ToolTip.visible: iconText.truncated && control.containsMouse ToolTip.text: model.name ToolTip.delay: 500 function itemClicked(mouseButton) { if (mouseButton === Qt.RightButton) { visualModel.model.openMenu(index) } else { var data = {"id": model.id}; send(data); } } ColumnLayout { width: parent.width anchors.top: parent.top anchors.topMargin: 8 anchors.horizontalCenter: parent.horizontalCenter spacing: 6 Image { id: iconImage Layout.preferredHeight: 48 Layout.preferredWidth: 48 sourceSize.height: height sourceSize.width: width source: icon cache: false Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter } UkuiItems.StyleText { id: iconText Layout.fillWidth: true text: name elide: Text.ElideRight paletteRole: Platform.Theme.Text horizontalAlignment: Text.AlignHCenter } } Loader { id: tag visible: mainWindow.editMode anchors.top: parent.top anchors.right: parent.right anchors.rightMargin: 10 Component { id: editImage UkuiItems.Button { width: 28 height: 28 icon.width: 16 icon.height: 16 background.paletteRole: Platform.Theme.Light background.alpha: 1 activeFocusOnTab: false onClicked: { visualModel.model.removeAppFromFavorites(id); } background.radius: width / 2 icon.source: "ukui-cancel-star-symbolic" } } sourceComponent: mainWindow.editMode ? editImage : null } } } Component { id: folderItemComponent UkuiItems.StyleBackground { height: iconItem.height width: iconItem.width - 14 useStyleTransparency: false paletteRole: Platform.Theme.Light radius: 6 alpha: parent.containsPress ? 0.25 : parent.containsMouse ? 0.15 : 0.00 function itemClicked(mouseButton) { if (mouseButton === Qt.RightButton) { visualModel.model.openMenu(index); } else { var x = mapToGlobal(0,0).x; var y = mapToGlobal(0,0).y openFolderSignal(id, name, x, y); // 执行隐藏动画,并且当前图标消失且鼠标区域不可用 favoriteView.isContentShow = false; opacity = 0; control.enabled = false; control.hoverEnabled = false; } } Item { id: folderItem property bool isSelect: false anchors.horizontalCenter: parent.horizontalCenter height: 40; width: 40 anchors.top: parent.top anchors.topMargin: 12 UkuiItems.StyleBackground { anchors.fill: parent paletteRole: Palette.Text useStyleTransparency: false alpha: 0.25 radius: 24 visible: folderItem.isSelect } AppControls2.FolderIcon { id: folderIcon anchors.fill: parent rows: 2; columns: 2 spacing: 2; padding: 2 icons: icon radius: 4; alpha: folderItem.isSelect ? 0 : 0.25 anchors.centerIn: parent } } UkuiItems.StyleText { id: folderText anchors.bottom: parent.bottom anchors.bottomMargin: 14 width: parent.width horizontalAlignment: Text.AlignHCenter anchors.horizontalCenter: parent.horizontalCenter elide: Text.ElideRight text: name } } } MouseArea { id: control anchors.fill: parent hoverEnabled: true pressAndHoldInterval: 300 acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { itemLoader.item.itemClicked(mouse.button); } onPressAndHold: { if (mouse.button === Qt.LeftButton) { drag.target = iconItem; iconItem.hold = true; exchangedStartIndex = iconItem.visualIndex; iconItem.sourceId = model.id; } } onReleased: { iconItem.hold = false; drag.target = null; } } // folderFunction function resetOpacity() { itemLoader.item.opacity = 1; control.enabled = true; control.hoverEnabled = true; } Component.onCompleted: favoriteView.contentShowFinished.connect(resetOpacity) Component.onDestruction: favoriteView.contentShowFinished.disconnect(resetOpacity) onHoldChanged: { if (hold) { favoriteView.interactive = false; } else { favoriteView.interactive = true; if (dragTypeIsMerge && (model.id !== mergeToAppId) && (model.type === DataType.Normal)) { if (isMergeToFolder) { visualModel.model.addAppToFolder(model.id, mergeToAppId); } else { visualModel.model.addAppsToNewFolder(model.id, mergeToAppId); } } else if (exchangedStartIndex != iconItem.visualIndex) { visualModel.model.exchangedAppsOrder(exchangedStartIndex, iconItem.visualIndex); } } } Drag.active: control.drag.active Drag.source: iconItem Drag.hotSpot.x: iconItem.width / 2 Drag.hotSpot.y: iconItem.height / 2 Drag.onActiveChanged: { if (Drag.active) { iconItem.parent = favoriteView; } else { iconResetAnimation.start(); } } ParallelAnimation { id: iconResetAnimation NumberAnimation { target: iconItem property: "x" to: container.x easing.type: Easing.OutQuad duration: 300 } NumberAnimation { target: iconItem property: "y" to: container.y - favoriteView.contentY easing.type: Easing.OutQuad duration: 300 } onFinished: { iconItem.parent = container; iconItem.x = 0; iconItem.y = 0; } } } } } }