shift-shell/containments/homescreens/folio/qml/FolderView.qml
Marco Allegretti a37734b74a Move homescreens to shared motion
Use Motion wrappers and state layers across Folio and Halcyon delegates, drawers, folders, resize handles, and dock feedback. Also align Folio edit/drop highlights with theme colors instead of fixed white overlays.
2026-05-21 11:13:36 +02:00

417 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
readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
readonly property int longAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
readonly property color folderFeedbackColor: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.24)
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 {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
}
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: root.folderFeedbackColor
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 { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
Behavior on rowValue { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
Behavior on pageValue { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
// 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)
}
]
}
}
}
}
}
}
}
}
}