mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
414 lines
16 KiB
QML
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 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)
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|