shift-shell/containments/homescreens/folio/qml/main.qml
Marco Allegretti 0b9ae2eca7 Narrow dock hit area to edge strip when hidden
The hidden dock's window still covered full height, blocking
toolbar buttons in maximized apps. Reduce the input region to
a thin strip at the screen edge while off-screen and restore
it on reveal. A debounce timer prevents an accidental edge
graze from reopening the dock.
2026-04-18 13:20:13 +02:00

624 lines
23 KiB
QML

// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.layershell 1.0 as LayerShell
import org.kde.plasma.private.sessions 2.0
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import "./private"
ContainmentItem {
id: root
property Folio.HomeScreen folio: root.plasmoid
Component.onCompleted: {
folio.FolioSettings.load();
folio.FavouritesModel.load();
folio.PageListModel.load();
}
property MobileShell.MaskManager maskManager: MobileShell.MaskManager {
height: root.height
width: root.width
}
property MobileShell.MaskManager frontMaskManager: MobileShell.MaskManager {
height: root.height
width: root.width
}
// wallpaper blur layer
MobileShell.BlurEffect {
id: wallpaperBlur
active: folio.FolioSettings.wallpaperBlurEffect > 0
anchors.fill: parent
sourceLayer: Plasmoid.wallpaperGraphicsObject
maskSourceLayer: folio.FolioSettings.wallpaperBlurEffect > 1 ? maskManager.maskLayer : null
fullBlur: Math.min(1,
Math.max(
1 - homeScreen.contentOpacity,
// Convergence: no blur for popup drawer
ShellSettings.Settings.convergenceModeEnabled ? 0 : folio.HomeScreenState.appDrawerOpenProgress * 2,
folio.HomeScreenState.searchWidgetOpenProgress * 1.5, // blur faster during swipe
folio.HomeScreenState.folderOpenProgress
)
)
}
WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
screenGeometry: Plasmoid.containment.screenGeometry
}
// Close app drawer when a new window appears
Connections {
target: WindowPlugin.WindowUtil
function onWindowCreated() {
if (folio.HomeScreenState.viewState === Folio.HomeScreenState.AppDrawerView) {
folio.HomeScreenState.closeAppDrawer();
}
}
}
function homeAction() {
const isInWindow = (!WindowPlugin.WindowUtil.isShowingDesktop && windowMaximizedTracker.showingWindow);
// Always close action drawer
if (MobileShellState.ShellDBusClient.isActionDrawerOpen) {
MobileShellState.ShellDBusClient.closeActionDrawer();
}
if (ShellSettings.Settings.convergenceModeEnabled) {
// Convergence: toggle the app drawer as a layer-shell overlay
// without disturbing open windows.
switch (folio.HomeScreenState.viewState) {
case Folio.HomeScreenState.AppDrawerView:
folio.HomeScreenState.closeAppDrawer();
break;
case Folio.HomeScreenState.FolderView:
folio.HomeScreenState.closeFolder();
break;
case Folio.HomeScreenState.SearchWidgetView:
folio.HomeScreenState.closeSearchWidget();
break;
case Folio.HomeScreenState.SettingsView:
folio.HomeScreenState.closeSettingsView();
break;
default:
folio.HomeScreenState.openAppDrawer();
break;
}
return;
}
if (isInWindow) {
folio.HomeScreenState.closeFolder();
folio.HomeScreenState.closeSearchWidget();
folio.HomeScreenState.closeAppDrawer();
folio.HomeScreenState.goToPage(0, false);
WindowPlugin.WindowUtil.minimizeAll();
// Always ensure settings view is closed
if (folio.HomeScreenState.viewState == Folio.HomeScreenState.SettingsView) {
folio.HomeScreenState.closeSettingsView();
}
} else { // If we are already on the homescreen
switch (folio.HomeScreenState.viewState) {
case Folio.HomeScreenState.PageView:
if (folio.HomeScreenState.currentPage === 0) {
folio.HomeScreenState.openAppDrawer();
} else {
folio.HomeScreenState.goToPage(0, false);
}
break;
case Folio.HomeScreenState.AppDrawerView:
folio.HomeScreenState.closeAppDrawer();
break;
case Folio.HomeScreenState.SearchWidgetView:
folio.HomeScreenState.closeSearchWidget();
break;
case Folio.HomeScreenState.FolderView:
folio.HomeScreenState.closeFolder();
break;
case Folio.HomeScreenState.SettingsView:
folio.HomeScreenState.closeSettingsView();
break;
}
}
}
Plasmoid.onActivated: homeAction()
Rectangle {
id: appDrawerBackground
anchors.fill: parent
// Convergence: no scrim (popup has own background); mobile: dark scrim
color: ShellSettings.Settings.convergenceModeEnabled
? "transparent"
: Qt.rgba(0, 0, 0, 0.6)
opacity: folio.HomeScreenState.appDrawerOpenProgress
}
Rectangle {
id: searchWidgetBackground
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.3)
opacity: folio.HomeScreenState.searchWidgetOpenProgress
}
Rectangle {
id: settingsViewBackground
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.3)
opacity: folio.HomeScreenState.settingsOpenProgress
}
// Dock overlay window — renders the favourites bar above application
// windows in convergence mode. LayerTop sits above normal windows but
// below LayerOverlay (notifications, volume OSD). The exclusive zone
// that reserves screen space is handled by the dockSpaceReserver in the
// task panel containment; this window only provides the visible dock.
Window {
id: dockOverlay
visible: ShellSettings.Settings.convergenceModeEnabled
color: "transparent"
width: Screen.width
height: Kirigami.Units.gridUnit * 3
LayerShell.Window.scope: "dock-overlay"
LayerShell.Window.layer: LayerShell.Window.LayerTop
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: -1
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// Auto-hide: slide dock content off-screen when a window is
// maximized. The reveal strip at the screen edge brings it back.
property real dockOffset: 0
readonly property real dockHeight: Kirigami.Units.gridUnit * 3
// Height of the input-receive strip kept at the screen edge when
// the dock is hidden. Matches the navigation panel convention.
readonly property real revealStripHeight: Kirigami.Units.gridUnit
// True once the hover-reveal timer fires; cleared on hover-exit.
property bool hoverRevealing: false
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing
onShouldHideChanged: {
if (shouldHide) {
dockOffset = dockHeight
} else {
dockOffset = 0
}
}
// Narrow the input region to a strip at the screen edge when hidden
// so that app controls near the bottom edge are not accidentally
// intercepted. Mirrors the same pattern used by NavigationPanel.
onDockOffsetChanged: {
if (dockOffset >= dockHeight) {
MobileShell.ShellUtil.setInputRegion(dockOverlay,
Qt.rect(0, dockOverlay.height - revealStripHeight,
dockOverlay.width, revealStripHeight))
} else if (dockOffset === 0) {
MobileShell.ShellUtil.setInputRegion(dockOverlay, Qt.rect(0, 0, 0, 0))
}
}
// Delay reveal by 300 ms so a quick edge graze does not pop the
// dock up mid-interaction with the underlying application.
Timer {
id: hoverRevealTimer
interval: 300
repeat: false
onTriggered: dockOverlay.hoverRevealing = true
}
HoverHandler {
id: dockHoverHandler
onHoveredChanged: {
if (hovered) {
hoverRevealTimer.start()
} else {
hoverRevealTimer.stop()
dockOverlay.hoverRevealing = false
}
}
}
Behavior on dockOffset {
NumberAnimation {
easing.type: dockOverlay.shouldHide ? Easing.InExpo : Easing.OutExpo
duration: Kirigami.Units.longDuration
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
transform: Translate { y: dockOverlay.dockOffset }
}
FavouritesBar {
id: dockOverlayBar
anchors.fill: parent
folio: root.folio
maskManager: root.maskManager
homeScreen: folioHomeScreen
transform: Translate { y: dockOverlay.dockOffset }
}
}
// App-drawer overlay — renders the popup drawer above application
// windows in convergence mode. Same pattern as the dock overlay:
// a fullscreen layer-shell surface at LayerTop so that it appears
// over normal windows without minimizing them.
Window {
id: drawerOverlay
visible: ShellSettings.Settings.convergenceModeEnabled
&& folio.HomeScreenState.appDrawerOpenProgress > 0
color: "transparent"
width: Screen.width
height: Screen.height
LayerShell.Window.scope: "drawer-overlay"
LayerShell.Window.layer: LayerShell.Window.LayerTop
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
| LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: -1
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// Click outside the popup to dismiss
MouseArea {
anchors.fill: parent
onClicked: folio.HomeScreenState.closeAppDrawer()
}
AppDrawer {
id: overlayDrawer
folio: root.folio
homeScreen: folioHomeScreen
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
readonly property real popupHeight: Math.min(Kirigami.Units.gridUnit * 32, parent.height * 0.7)
readonly property real dockHeight: Kirigami.Units.gridUnit * 3
width: popupWidth
height: popupHeight
opacity: folio.HomeScreenState.appDrawerOpenProgress < 0.5
? 0 : (folio.HomeScreenState.appDrawerOpenProgress - 0.5) * 2
property real animationY: (1 - folio.HomeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2)
x: Kirigami.Units.smallSpacing
y: (opacity > 0)
? parent.height - dockHeight - popupHeight - Kirigami.Units.smallSpacing + animationY
: parent.height
headerHeight: Math.round(Kirigami.Units.gridUnit * 3)
headerItem: AppDrawerHeader {
id: overlayDrawerHeader
folio: root.folio
onReleaseFocusRequested: overlayDrawer.forceActiveFocus()
}
Keys.onPressed: (event) => {
if (event.text.trim().length > 0) {
overlayDrawerHeader.addSearchText(event.text);
overlayDrawerHeader.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) {
overlayDrawerHeader.forceActiveFocus();
event.accepted = true;
}
}
Connections {
target: folio.HomeScreenState
function onAppDrawerOpened() {
overlayDrawer.forceActiveFocus();
}
}
}
Rectangle {
id: powerPanel
readonly property real buttonHeight: Kirigami.Units.gridUnit * 5
width: Kirigami.Units.gridUnit * 8
height: overlayDrawer.popupHeight
x: overlayDrawer.x + overlayDrawer.width + Kirigami.Units.smallSpacing
y: overlayDrawer.y
opacity: overlayDrawer.opacity
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 0
verticalOffset: 2
radius: 12
samples: 25
color: Qt.rgba(0, 0, 0, 0.4)
}
MouseArea {
anchors.fill: parent
}
SessionManagement {
id: powerSession
}
Column {
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: Kirigami.Units.smallSpacing
}
spacing: Kirigami.Units.smallSpacing
Rectangle {
width: parent.width
height: powerPanel.buttonHeight
radius: Kirigami.Units.cornerRadius
color: lockArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: lockArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Column {
anchors.centerIn: parent
spacing: Kirigami.Units.smallSpacing / 2
Kirigami.Icon {
anchors.horizontalCenter: parent.horizontalCenter
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
source: "system-lock-screen"
active: lockArea.containsMouse
}
PlasmaComponents.Label {
anchors.horizontalCenter: parent.horizontalCenter
font: Kirigami.Theme.smallFont
text: i18n("Lock Screen")
}
}
MouseArea {
id: lockArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerSession.lock()
folio.HomeScreenState.closeAppDrawer()
}
}
}
Rectangle {
width: parent.width
height: powerPanel.buttonHeight
radius: Kirigami.Units.cornerRadius
color: rebootArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: rebootArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Column {
anchors.centerIn: parent
spacing: Kirigami.Units.smallSpacing / 2
Kirigami.Icon {
anchors.horizontalCenter: parent.horizontalCenter
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
source: "system-reboot"
active: rebootArea.containsMouse
}
PlasmaComponents.Label {
anchors.horizontalCenter: parent.horizontalCenter
font: Kirigami.Theme.smallFont
text: i18n("Restart")
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
folio.HomeScreenState.closeAppDrawer()
powerSession.requestReboot()
}
}
}
Rectangle {
width: parent.width
height: powerPanel.buttonHeight
radius: Kirigami.Units.cornerRadius
color: shutdownArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: shutdownArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Column {
anchors.centerIn: parent
spacing: Kirigami.Units.smallSpacing / 2
Kirigami.Icon {
anchors.horizontalCenter: parent.horizontalCenter
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
source: "system-shutdown"
active: shutdownArea.containsMouse
}
PlasmaComponents.Label {
anchors.horizontalCenter: parent.horizontalCenter
font: Kirigami.Theme.smallFont
text: i18n("Shut Down")
}
}
MouseArea {
id: shutdownArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
folio.HomeScreenState.closeAppDrawer()
powerSession.requestShutdown()
}
}
}
}
}
}
MobileShell.HomeScreen {
id: homeScreen
anchors.fill: parent
plasmoidItem: root
onResetHomeScreenPosition: {
// NOTE: empty, because this is handled by homeAction()
}
onHomeTriggered: root.homeAction()
contentItem: Item {
// homescreen component
FolioHomeScreen {
id: folioHomeScreen
folio: root.folio
maskManager: root.maskManager
anchors.fill: parent
topMargin: homeScreen.topMargin
bottomMargin: homeScreen.bottomMargin
leftMargin: homeScreen.leftMargin
rightMargin: homeScreen.rightMargin
// Ensure is the focused item at start
Component.onCompleted: forceActiveFocus()
onWallpaperSelectorTriggered: wallpaperSelectorLoader.active = true
}
}
}
// top blur layer for items on top of the base homescreen
MobileShell.BlurEffect {
id: homescreenBlur
anchors.fill: parent
active: folio.FolioSettings.wallpaperBlurEffect > 1 && ((delegateDragItem.visible && folio.HomeScreenState.dragState.dropDelegate.type === Folio.FolioDelegate.Folder) || wallpaperSelectorLoader.active)
visible: active
fullBlur: 0
sourceLayer: homeScreenLayer
maskSourceLayer: frontMaskManager.maskLayer
// stacking both wallpaper and homescreen layers so we can blur them in one pass
Item {
id: homeScreenLayer
anchors.fill: parent
opacity: 0
// wallpaper blur
ShaderEffectSource {
anchors.fill: parent
textureSize: homescreenBlur.textureSize
sourceItem: Plasmoid.wallpaperGraphicsObject
hideSource: false
}
// homescreen blur
ShaderEffectSource {
anchors.fill: parent
textureSize: homescreenBlur.textureSize
sourceItem: homeScreen
hideSource: false
}
}
}
// drag and drop component
DelegateDragItem {
id: delegateDragItem
folio: root.folio
maskManager: root.frontMaskManager
}
// drag and drop for widgets
WidgetDragItem {
id: widgetDragItem
folio: root.folio
}
// loader for wallpaper selector
Loader {
id: wallpaperSelectorLoader
anchors.fill: parent
asynchronous: true
active: false
onLoaded: {
wallpaperSelectorLoader.item.open();
}
sourceComponent: MobileShell.WallpaperSelector {
maskManager: root.frontMaskManager
horizontal: root.width > root.height
edge: horizontal ? Qt.LeftEdge : Qt.BottomEdge
bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin
leftMargin: horizontal ? folioHomeScreen.leftMargin : 0
rightMargin: horizontal ? folioHomeScreen.rightMargin : 0
onClosed: {
wallpaperSelectorLoader.active = false;
}
onWallpaperSettingsRequested: {
close();
folioHomeScreen.openConfigure();
}
}
}
}