// SPDX-FileCopyrightText: 2023 Devin Lin // SPDX-License-Identifier: LGPL-2.0-or-later import QtQuick import QtQuick.Layouts import QtQuick.Effects import QtQuick.Controls as QQC2 import org.kde.kirigami 2.20 as Kirigami import org.kde.plasma.private.mobileshell as MobileShell import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio import "./private" import "./delegate" Folio.DelegateTouchArea { id: root property Folio.HomeScreen folio property var homeScreen // the position on the screen for animations to start from property real folderPositionX property real folderPositionY property Folio.FolioApplicationFolder folder: folio.HomeScreenState.currentFolder MobileShell.HapticsEffect { id: haptics } onClicked: close(); function close() { folio.HomeScreenState.closeFolder(); } Keys.onPressed: (event) => { switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: // Close view root.close(); event.accepted = true; break; case Qt.Key_Up: case Qt.Key_Down: case Qt.Key_Left: case Qt.Key_Right: // Keyboard focus on first item if (delegateRepeater.count > 0) { delegateRepeater.itemAt(0).keyboardFocus(); event.accepted = true; } break; } } Connections { target: folio.HomeScreenState function onFolderAboutToOpen(x, y) { root.folderPositionX = x - folio.HomeScreenState.viewLeftPadding; root.folderPositionY = y - folio.HomeScreenState.viewTopPadding; // Focus view when opened root.forceActiveFocus(); } } FolderViewTitle { id: titleText folio: root.folio width: root.width // have to use y instead of anchors to avoid animations y: Math.round(((root.height / 2) - (folderBackground.height / 2)) * 0.9 - height) anchors.left: parent.left anchors.right: parent.right folder: root.folder opacity: (root.opacity === 1) ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } } function updateContentWidth() { let margin = folderBackground.margin; folio.HomeScreenState.folderPageContentWidth = (folderBackground.width - margin * 2); } function updateContentHeight() { let margin = folderBackground.margin; folio.HomeScreenState.folderPageContentHeight = (folderBackground.height - margin * 2); } function keyboardNavigateForDelegate(key, column, row, page) { let dx = 0; let dy = 0; switch (key) { case Qt.Key_Up: { dy = -1; break; } case Qt.Key_Down: { dy = 1; break; } case Qt.Key_Left: { dx = -1; break; } case Qt.Key_Right: { dx = 1; break; } default: return; } let x = column + dx; let y = row + dy; // Loop in direction to find next delegate while (x >= 0 && x < folio.HomeScreenState.folderGridLength && y >= 0 && y < folio.HomeScreenState.folderGridLength) { // Find delegate at x, y for (let i = 0; i < delegateRepeater.count; ++i) { let cDelegate = delegateRepeater.itemAt(i); if (cDelegate.columnValue === x && cDelegate.rowValue === y && cDelegate.pageValue === page) { // Delegate matches, focus on it and return cDelegate.keyboardFocus(); return; } } x += dx; y += dy; } // Behavior if no delegate is found to navigate to switch (key) { case Qt.Key_Up: case Qt.Key_Down: break; case Qt.Key_Left: { // Go to the left page if a delegate exists there let cDelegate = delegateRepeater.itemAt((page - 1) * folio.HomeScreenState.folderGridLength * folio.HomeScreenState.folderGridLength); if (cDelegate) { cDelegate.keyboardFocus(); folio.HomeScreenState.goToFolderPage(page - 1, false); } break; } case Qt.Key_Right: { // Go to the right page if a delegate exists there let cDelegate = delegateRepeater.itemAt((page + 1) * folio.HomeScreenState.folderGridLength * folio.HomeScreenState.folderGridLength); if (cDelegate) { cDelegate.keyboardFocus(); folio.HomeScreenState.goToFolderPage(page + 1, false); } break; } default: return; } } Connections { target: folio.HomeScreenState function onPageCellWidthChanged() { root.updateContentWidth(); root.updateContentHeight(); } function onPageCellHeightChanged() { root.updateContentWidth(); root.updateContentHeight(); } } Rectangle { id: folderBackground color: Qt.rgba(255, 255, 255, 0.3) radius: Kirigami.Units.gridUnit readonly property int gridLength: folio.HomeScreenState.folderGridLength readonly property int margin: Kirigami.Units.largeSpacing readonly property int maxLength: Math.min(root.width - Kirigami.Units.gridUnit * 4, root.height - Kirigami.Units.gridUnit * 2) readonly property int pageSize: Math.min(maxLength, (folio.FolioSettings.delegateIconSize + Kirigami.Units.gridUnit * 3) * gridLength + Kirigami.Units.gridUnit * 2) width: pageSize - margin * 2 height: pageSize QQC2.PageIndicator { visible: count > 1 Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: Kirigami.Theme.Complementary anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom currentIndex: folio.HomeScreenState.currentFolderPage count: folio.HomeScreenState.currentFolder ? folio.HomeScreenState.currentFolder.applications.numberOfPages : 0 } onWidthChanged: { folio.HomeScreenState.folderPageWidth = width; root.updateContentWidth(); root.updateContentHeight(); } onHeightChanged: { folio.HomeScreenState.folderPageHeight = height; root.updateContentWidth(); root.updateContentHeight(); } x: { const folderPos = root.folderPositionX; const centerX = (root.width / 2) - (width / 2); return Math.round(folderPos + (centerX - folderPos) * folio.HomeScreenState.folderOpenProgress); } y: { const folderPos = root.folderPositionY; const centerY = (root.height / 2) - (height / 2); return Math.round(folderPos + (centerY - folderPos) * folio.HomeScreenState.folderOpenProgress); } transform: [ Scale { origin.x: 0 origin.y: 0 xScale: { const iconSize = folio.FolioSettings.delegateIconSize; const fullWidth = folderBackground.width; const candidate = iconSize + (fullWidth - iconSize) * folio.HomeScreenState.folderOpenProgress; return Math.max(0, Math.min(1, candidate / fullWidth)); } yScale: { const iconSize = folio.FolioSettings.delegateIconSize; const fullHeight = folderBackground.height; const candidate = iconSize + (fullHeight - iconSize) * folio.HomeScreenState.folderOpenProgress; return Math.max(0, Math.min(1, candidate / fullHeight)); } } ] MouseArea { id: captureTouches anchors.fill: parent // clip the pages layer.enabled: true Item { id: contentContainer x: folio.HomeScreenState.folderViewX Repeater { id: delegateRepeater model: root.folder ? root.folder.applications : [] delegate: Item { id: delegate readonly property var delegateModel: model.delegate readonly property int index: model.index readonly property int cellWidth: folio.HomeScreenState.folderPageContentWidth / folderBackground.gridLength readonly property int cellHeight: folio.HomeScreenState.folderPageContentHeight / folderBackground.gridLength readonly property bool outsideView: { const appPosition = x + folio.HomeScreenState.folderViewX; return (appPosition <= 0 || appPosition >= folderBackground.width); } readonly property var dragState: folio.HomeScreenState.dragState readonly property bool isDropPositionThis: dragState.candidateDropPosition.location === Folio.DelegateDragPosition.Folder && dragState.candidateDropPosition.folderPosition === index // get the index position value so we can animate them property double columnValue: model.columnIndex property double rowValue: model.rowIndex property double pageValue: model.pageIndex Behavior on columnValue { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } } Behavior on rowValue { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } } Behavior on pageValue { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } } // multiply the index values by the cell size to get the actual position readonly property int columnPosition: cellWidth * columnValue readonly property int rowPosition: cellHeight * rowValue x: folderBackground.margin + (pageValue * folderBackground.width) + columnPosition y: folderBackground.margin + rowPosition implicitWidth: cellWidth implicitHeight: cellHeight width: cellWidth height: cellHeight // Implement keyboard arrow navigation Keys.onPressed: (event) => { switch (event.key) { case Qt.Key_Up: case Qt.Key_Down: case Qt.Key_Left: case Qt.Key_Right: event.accepted = true; break; default: return; } root.keyboardNavigateForDelegate(event.key, columnValue, rowValue, pageValue); } function keyboardFocus() { if (delegateLoader.item) { delegateLoader.item.keyboardFocus(); } } Loader { id: delegateLoader anchors.fill: parent sourceComponent: { if (delegate.delegateModel && delegate.delegateModel.type === Folio.FolioDelegate.Application) { return appComponent; } else { return noneComponent; } } } Component { id: noneComponent Item {} } Component { id: appComponent AppDelegate { id: appDelegate folio: root.folio application: delegate.delegateModel.application // do not show if the drop animation is running to this delegate visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis) enabled: !delegate.outsideView // don't show label in drag and drop mode labelOpacity: delegate.opacity onPressAndHold: { // prevent editing if lock layout is enabled if (folio.FolioSettings.lockLayout) return; let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.delegateModel, appDelegate.delegateItem); folio.HomeScreenState.startDelegateFolderDrag( mappedCoords.x, mappedCoords.y, appDelegate.pressPosition.x, appDelegate.pressPosition.y, root.folder, delegate.index ); contextMenu.open(); haptics.buttonVibrate(); } onPressAndHoldReleased: { // cancel the event if the delegate is not dragged if (folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) { homeScreen.cancelDelegateDrag(); } } onRightMousePress: { contextMenu.open(); } ContextMenuLoader { id: contextMenu // close menu when drag starts Connections { target: folio.HomeScreenState function onDelegateDragStarted() { contextMenu.close(); } } actions: [ Kirigami.Action { icon.name: "emblem-favorite" text: i18n("Remove") enabled: !folio.FolioSettings.lockLayout onTriggered: root.folder.removeDelegate(delegate.index) } ] } } } } } } } } }