shift-shell/containments/homescreens/folio/qml/FolderView.qml
Devin Lin a8b858dc3e Remove kirigami version in imports
This fixes potential issues stemming from pinning versions on Kirigami
qml api imports
2026-03-06 22:08:07 -05:00

414 lines
16 KiB
QML

// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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 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)
}
]
}
}
}
}
}
}
}
}
}