// SPDX-FileCopyrightText: 2023 Devin Lin // SPDX-License-Identifier: LGPL-2.0-or-later import QtQuick import QtQuick.Window import QtQuick.Layouts import QtQuick.Effects import QtQuick.Controls as QQC2 import org.kde.kirigami as Kirigami import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import QtQuick.Templates as T import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio import "./delegate" import "./settings" Item { id: root property Folio.HomeScreen folio property MobileShell.MaskManager maskManager property Folio.HomeScreenState homeScreenState: folio.HomeScreenState property real topMargin: 0 property real bottomMargin: 0 property real leftMargin: 0 property real rightMargin: 0 property bool interactive: true readonly property color favouritesBarScrimColor: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.14) // non-widget drop animation readonly property bool dropAnimationRunning: delegateDragItem.dropAnimationRunning || widgetDragItem.dropAnimationRunning // widget that is currently being dragged (or dropped) readonly property Folio.FolioWidget currentlyDraggedWidget: widgetDragItem.widget // how much to scale out in the settings mode readonly property real settingsModeHomeScreenScale: 0.8 onTopMarginChanged: folio.HomeScreenState.viewTopPadding = root.topMargin onBottomMarginChanged: folio.HomeScreenState.viewBottomPadding = root.bottomMargin onLeftMarginChanged: folio.HomeScreenState.viewLeftPadding = root.leftMargin onRightMarginChanged: folio.HomeScreenState.viewRightPadding = root.rightMargin onWidthChanged: folio.HomeScreenState.viewWidth = width onHeightChanged: folio.HomeScreenState.viewHeight = height signal wallpaperSelectorTriggered() // called by any delegates when starting drag // returns the mapped coordinates to be used in the home screen state function prepareStartDelegateDrag(delegate, item, skipSwipeThreshold) { // If the user is prompted with a context menu, they may want to let go, and so we keep the detect swipe threshold. // Otherwise, we want to skip detecting a swipe because we know we immediately go into delegate dragging. if (skipSwipeThreshold) { swipeArea.setSkipSwipeThreshold(true); } if (delegate) { delegateDragItem.delegate = delegate; } return root.mapFromItem(item, 0, 0); } function cancelDelegateDrag() { homeScreenState.cancelDelegateDrag(); } // sets the coordinates for the folder opening/closing animation function prepareFolderOpen(item) { return root.mapFromItem(item, 0, 0); } function openConfigure() { Plasmoid.internalAction("configure").trigger(); } function showDesktopContextMenu() { desktopContextMenu.popup(); } PC3.Menu { id: desktopContextMenu popupType: T.Popup.Window PC3.MenuItem { text: i18n("Desktop and Wallpaper...") icon.name: "preferences-desktop-wallpaper" onTriggered: root.wallpaperSelectorTriggered() } PC3.MenuItem { text: i18n("Add Widgets...") icon.name: "widget-alternatives" onTriggered: { folio.HomeScreenState.openSettingsView(); settings.openWidgetsBrowser(); } } PC3.MenuSeparator {} PC3.MenuItem { text: i18n("Configure Desktop...") icon.name: "settings-configure" onTriggered: root.openConfigure() } } Keys.onPressed: (event) => { // The root is focused when we aren't in key navigation mode // Begin key navigation when arrow keys are pressed if (event.key === Qt.Key_Up || event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { homeScreenPages.forceActiveFocus(); event.accepted = true; } } // determine how tall an app label is, for delegate measurements DelegateLabel { id: appLabelMetrics text: "M\nM" visible: false onHeightChanged: folio.HomeScreenState.pageDelegateLabelHeight = appLabelMetrics.height Component.onCompleted: { folio.HomeScreenState.pageDelegateLabelWidth = Kirigami.Units.smallSpacing; } } // a way of stopping focus FocusScope { id: noFocus } // Listen to view changes Connections { target: folio.HomeScreenState function onViewStateChanged() { if (folio.HomeScreenState.viewState === Folio.HomeScreenState.PageView) { // Focus root when on page view root.forceActiveFocus(); } } } // Drag and drop area DelegateDropArea { id: dropArea folio: root.folio anchors.fill: parent // Area that can be swiped MobileShell.SwipeArea { id: swipeArea anchors.fill: parent interactive: root.interactive && settings.homeScreenInteractive && !dropArea.containsDrag && (appDrawer.flickable.atYBeginning || // there are cases where contentY > 0 but atYBeginning is true appDrawer.flickable.contentY <= 10 || // disable the swipe area when we are swiping in the app drawer, and not in drag-and-drop folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate || folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate || folio.HomeScreenState.swipeState === Folio.HomeScreenState.SwipingAppDrawerGrid || folio.HomeScreenState.viewState !== Folio.HomeScreenState.AppDrawerView) onSwipeStarted: (currentPos, startPos) => { const deltaX = currentPos.x - startPos.x; const deltaY = currentPos.y - startPos.y; homeScreenState.swipeStarted(deltaX, deltaY); } onSwipeEnded: { homeScreenState.swipeEnded(); } onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => { // cancel swipe when settings component is opening to prevent conflicts if (folio.HomeScreenState.settingsOpenProgress && folio.HomeScreenState.viewState !== Folio.HomeScreenState.SettingsView) { homeScreenState.swipeCancelled(); return; } homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY); } onTouchpadScrollStarted: { if (!ShellSettings.Settings.convergenceModeEnabled) { root.homeScreenState.swipeStarted(0, 0); } } onTouchpadScrollEnded: { if (!ShellSettings.Settings.convergenceModeEnabled) { root.homeScreenState.swipeEnded(); } } onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => { if (!ShellSettings.Settings.convergenceModeEnabled) { root.homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY); } } onPressedChanged: { if (pressed) { // ensures that components like the widget settings overlay close when swiping noFocus.forceActiveFocus(); } } SettingsComponent { id: settings folio: root.folio homeScreen: root settingsModeHomeScreenScale: root.settingsModeHomeScreenScale // Don't anchor, since we set y width: parent.width height: parent.height opacity: folio.HomeScreenState.settingsOpenProgress visible: opacity > 0 z: 1 bottomMargin: root.bottomMargin leftMargin: root.leftMargin rightMargin: root.rightMargin } Item { id: mainHomeScreen anchors.fill: parent // we stop showing halfway through the animation (skip in convergence — desktop stays visible) opacity: ShellSettings.Settings.convergenceModeEnabled ? 1 : 1 - Math.max(homeScreenState.appDrawerOpenProgress, homeScreenState.searchWidgetOpenProgress, homeScreenState.folderOpenProgress) * 2 visible: opacity > 0 // prevent handlers from picking up events transform: [ Scale { property real scaleFactor: ShellSettings.Settings.convergenceModeEnabled ? 0 : Math.max(homeScreenState.appDrawerOpenProgress, homeScreenState.searchWidgetOpenProgress) origin.x: mainHomeScreen.width / 2 origin.y: mainHomeScreen.height / 2 yScale: 1 - (scaleFactor * 2) * 0.1 xScale: 1 - (scaleFactor * 2) * 0.1 } ] HomeScreenPages { id: homeScreenPages folio: root.folio maskManager: root.maskManager homeScreen: root anchors.topMargin: root.topMargin anchors.leftMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? 0 : root.leftMargin anchors.rightMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? 0 : root.rightMargin anchors.bottomMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? 0 : root.bottomMargin // update the model with page dimensions onWidthChanged: { homeScreenState.pageWidth = homeScreenPages.width; } onHeightChanged: { homeScreenState.pageHeight = homeScreenPages.height; } // Keyboard navigation from pages Keys.onPressed: (event) => { switch (event.key) { case Qt.Key_Up: // Open search widget when going up folio.HomeScreenState.openSearchWidget(); event.accepted = true; break; case Qt.Key_Down: // Focus on favorites bar or app drawer, depending on its physical location if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom) { favouritesBar.forceActiveFocus(); } else { folio.HomeScreenState.openAppDrawer(); } event.accepted = true; break; case Qt.Key_Left: if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left) { // If favorites bar is on the left, navigate to it favouritesBar.forceActiveFocus(); } else { // Otherwise go to page on the left folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage - 1, false); homeScreenPages.focusCurrentPageForKeyboardNav(); } event.accepted = true; break; case Qt.Key_Right: if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right) { // If favorites bar is on the right, navigate to it favouritesBar.forceActiveFocus(); } else { // Otherwise go to page on the right folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage + 1, false); homeScreenPages.focusCurrentPageForKeyboardNav(); } event.accepted = true; break; default: break; } } transform: [ Scale { // animation when settings opens property real scaleFactor: 1 - folio.HomeScreenState.settingsOpenProgress * (1 - settingsModeHomeScreenScale) origin.x: root.leftMargin + (root.width - root.rightMargin - root.leftMargin) / 2 origin.y: root.height * settingsModeHomeScreenScale / 2 xScale: scaleFactor yScale: scaleFactor } ] states: [ State { name: "bottom" when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom AnchorChanges { target: homeScreenPages anchors.top: parent.top anchors.bottom: favouritesBar.top anchors.left: parent.left anchors.right: parent.right } }, State { name: "left" when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left AnchorChanges { target: homeScreenPages anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: favouritesBar.right anchors.right: parent.right } }, State { name: "right" when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right AnchorChanges { target: homeScreenPages anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: favouritesBar.left } } ] } Rectangle { id: favouritesBarScrim color: root.favouritesBarScrimColor Component.onCompleted: maskManager.assignToMask(this) // don't show in settings mode opacity: 1 - folio.HomeScreenState.settingsOpenProgress visible: folio.FolioSettings.showFavouritesBarBackground && !ShellSettings.Settings.convergenceModeEnabled anchors.top: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? favouritesBar.top : parent.top anchors.bottom: parent.bottom anchors.left: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? favouritesBar.left : parent.left anchors.right: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? favouritesBar.right : parent.right // because of the scale animation, we need to extend the panel out a bit anchors.topMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? 0 : -Kirigami.Units.gridUnit * 5 anchors.bottomMargin: -Kirigami.Units.gridUnit * 5 anchors.leftMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right ? 0 : -Kirigami.Units.gridUnit * 5 anchors.rightMargin: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left ? 0 : -Kirigami.Units.gridUnit * 5 } FavouritesBar { id: favouritesBar folio: root.folio maskManager: root.maskManager homeScreen: root // don't show in settings mode opacity: 1 - folio.HomeScreenState.settingsOpenProgress // Hidden in convergence mode — the dock overlay window renders it instead visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled // one is ignored as anchors are set height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6 width: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6 anchors.topMargin: root.topMargin anchors.bottomMargin: ShellSettings.Settings.convergenceModeEnabled ? 0 : root.bottomMargin anchors.leftMargin: root.leftMargin anchors.rightMargin: root.rightMargin // Keyboard navigation on favorites bar Keys.onPressed: (event) => { switch (event.key) { case Qt.Key_Up: // Focus on homescreen pages or search widget depending on physical position if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom) { homeScreenPages.forceActiveFocus(); } else { folio.HomeScreenState.openSearchWidget(); } event.accepted = true; break; case Qt.Key_Down: // Open app drawer folio.HomeScreenState.openAppDrawer(); event.accepted = true; break; case Qt.Key_Left: if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left) { // Go to left page if mounted on the left folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage - 1, false); event.accepted = true; } break; case Qt.Key_Right: if (folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right) { // Go to right page if mounted on the right folio.HomeScreenState.goToPage(folio.HomeScreenState.currentPage + 1, false); event.accepted = true; } break; default: break; } } states: [ State { name: "bottom" when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom AnchorChanges { target: favouritesBar anchors.top: undefined anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right } PropertyChanges { target: favouritesBar height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6 } }, State { name: "left" when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Left AnchorChanges { target: favouritesBar anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: undefined } PropertyChanges { target: favouritesBar width: Kirigami.Units.gridUnit * 6 } }, State { name: "right" when: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Right AnchorChanges { target: favouritesBar anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: undefined anchors.right: parent.right } PropertyChanges { target: favouritesBar width: Kirigami.Units.gridUnit * 6 } } ] } Item { id: pageIndicatorWrapper property bool favouritesBarAtBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom // don't show in settings mode opacity: 1 - folio.HomeScreenState.settingsOpenProgress visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.bottom: favouritesBarAtBottom ? favouritesBar.top : parent.bottom anchors.topMargin: root.topMargin anchors.leftMargin: root.leftMargin anchors.rightMargin: root.rightMargin anchors.bottomMargin: favouritesBarAtBottom ? 0 : (root.bottomMargin + Kirigami.Units.largeSpacing) // show page indicator if there are multiple pages 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.currentPage count: folio.PageListModel.length TapHandler { enabled: !ShellSettings.Settings.convergenceModeEnabled onTapped: folio.HomeScreenState.openAppDrawer() } } // show arrow to open app drawer when there is 1 page (hidden in convergence mode) Kirigami.Icon { source: 'arrow-up' Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: Kirigami.Theme.Complementary isMask: true color: Kirigami.Theme.textColor implicitHeight: Kirigami.Units.iconSizes.small implicitWidth: Kirigami.Units.iconSizes.small visible: folio.PageListModel.length <= 1 && !ShellSettings.Settings.convergenceModeEnabled anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: Kirigami.Units.smallSpacing TapHandler { onTapped: folio.HomeScreenState.openAppDrawer() } } } } // folder view FolderView { id: folderView folio: root.folio anchors.fill: parent anchors.topMargin: root.topMargin anchors.leftMargin: root.leftMargin anchors.rightMargin: root.rightMargin anchors.bottomMargin: root.bottomMargin homeScreen: root opacity: homeScreenState.folderOpenProgress transform: Translate { y: folderView.opacity > 0 ? 0 : folderView.height } } // Click-to-dismiss overlay (mobile only; convergence uses layer-shell overlay) MouseArea { anchors.fill: parent visible: !ShellSettings.Settings.convergenceModeEnabled && homeScreenState.appDrawerOpenProgress > 0 onClicked: folio.HomeScreenState.closeAppDrawer() } // bottom app drawer (mobile only; convergence uses layer-shell overlay) AppDrawer { id: appDrawer folio: root.folio visible: !ShellSettings.Settings.convergenceModeEnabled width: parent.width height: parent.height homeScreen: root // we only start showing it halfway through opacity: homeScreenState.appDrawerOpenProgress < 0.5 ? 0 : (homeScreenState.appDrawerOpenProgress - 0.5) * 2 // position for animation property real animationY: (1 - homeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2) // Full-screen position x: 0 y: (opacity > 0) ? animationY : parent.height headerHeight: Math.round(Kirigami.Units.gridUnit * 4) headerItem: AppDrawerHeader { id: appDrawerHeader folio: root.folio onReleaseFocusRequested: appDrawer.forceActiveFocus() } // Account for panels topPadding: root.topMargin bottomPadding: root.bottomMargin leftPadding: root.leftMargin rightPadding: root.rightMargin // Forward keyboard text to the search bar Keys.onPressed: (event) => { if (event.text.trim().length > 0) { appDrawerHeader.addSearchText(event.text); appDrawerHeader.forceActiveFocus(); event.accepted = true; } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down) { appDrawerHeader.forceActiveFocus(); event.accepted = true; } } Connections { target: folio.HomeScreenState function onAppDrawerOpened() { appDrawer.forceActiveFocus(); } function onAppDrawerClosed() { // reset app drawer position when closed appDrawer.flickable.contentY = 0; } } } // search component MobileShell.KRunnerScreen { id: searchWidget anchors.fill: parent opacity: homeScreenState.searchWidgetOpenProgress visible: opacity > 0 transform: Translate { y: (1 - homeScreenState.searchWidgetOpenProgress) * (-Kirigami.Units.gridUnit * 2) } onVisibleChanged: { if (!visible) { // clear search bar when closed searchWidget.clearField(); } } // focus the search bar if it opens Connections { target: folio.HomeScreenState function onSearchWidgetOpenProgressChanged() { if (homeScreenState.searchWidgetOpenProgress === 1.0) { searchWidget.requestFocus(); } } } onRequestedClose: (triggeredByKeyEvent) => { homeScreenState.closeSearchWidget(); } anchors.topMargin: root.topMargin anchors.bottomMargin: root.bottomMargin anchors.leftMargin: root.leftMargin anchors.rightMargin: root.rightMargin } } } }