shift-shell/containments/homescreens/folio/qml/main.qml

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1643 lines
71 KiB
QML
Raw Normal View History

// 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
2026-05-19 07:13:00 +00:00
import QtQuick.Shapes 1.8
import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid 2.0
2019-09-04 16:39:31 +00:00
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.plasma.private.mobileshell.gamingshellplugin as GamingShell
2020-07-22 15:17:10 +00:00
import org.kde.layershell 1.0 as LayerShell
import org.kde.plasma.private.sessions 2.0
import org.kde.coreaddons as KCoreAddons
import org.kde.kcmutils as KCM
import org.kde.kirigamiaddons.components as KirigamiAddonsComponents
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import "./gaming"
import "./private"
ContainmentItem {
id: root
property var folio: root.plasmoid
readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
// Tracks whether the Game Center grid is visible within gaming mode.
// If gaming mode is already enabled at startup, open it immediately so
// the user is never left without controls.
property bool gameCenterOpen: ShellSettings.Settings.gamingModeEnabled
property bool showGameCenterHint: false
// State saved when gaming mode activates, restored when it deactivates
property string _savedPowerProfile: ""
property bool _savedDnd: false
property bool _savedDynamicTiling: false
property bool _gamingSessionActive: false
function _applyGamingModeState(enabled) {
root.gameCenterOpen = enabled
GamingShell.GamepadManager.active = enabled
if (enabled === root._gamingSessionActive) {
return
}
if (enabled) {
// Save current state and apply gaming optimizations
root._savedDnd = MobileShellState.ShellDBusClient.doNotDisturb
root._savedDynamicTiling = ShellSettings.Settings.dynamicTilingEnabled
root._gamingSessionActive = true
MobileShellState.ShellDBusClient.doNotDisturb = true
ShellSettings.Settings.dynamicTilingEnabled = false
if (MobileShellState.ShellDBusClient.isActionDrawerOpen) {
MobileShellState.ShellDBusClient.closeActionDrawer()
}
folio.HomeScreenState.closeFolder()
folio.HomeScreenState.closeSearchWidget()
folio.HomeScreenState.closeAppDrawer()
if (folio.HomeScreenState.viewState === Folio.HomeScreenState.SettingsView) {
folio.HomeScreenState.closeSettingsView()
}
if (GamingShell.PowerProfileControl.available) {
root._savedPowerProfile = GamingShell.PowerProfileControl.activeProfile
GamingShell.PowerProfileControl.activeProfile = "performance"
}
GamingShell.GameModeControl.requestStart()
} else {
// Restore previous state
root._gamingSessionActive = false
MobileShellState.ShellDBusClient.doNotDisturb = root._savedDnd
ShellSettings.Settings.dynamicTilingEnabled = root._savedDynamicTiling
if (GamingShell.PowerProfileControl.available && root._savedPowerProfile.length > 0) {
GamingShell.PowerProfileControl.activeProfile = root._savedPowerProfile
}
GamingShell.GameModeControl.requestEnd()
}
}
Timer {
id: gameCenterHintTimer
interval: 2600
onTriggered: root.showGameCenterHint = false
}
Connections {
target: ShellSettings.Settings
function onGamingModeEnabledChanged() {
root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled)
}
}
// Gamepad Guide button toggles Game Center overlay
Connections {
target: GamingShell.GamepadManager
enabled: ShellSettings.Settings.gamingModeEnabled
function onButtonPressed(button, gamepadIndex) {
if (button === GamingShell.GamepadManager.ButtonGuide) {
root.gameCenterOpen = !root.gameCenterOpen
}
}
}
2019-09-04 16:39:31 +00:00
Component.onCompleted: {
root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled)
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
2023-11-17 06:40:07 +00:00
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
)
)
2023-11-15 16:42:43 +00:00
}
WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
screenGeometry: Plasmoid.containment.screenGeometry
}
// In gaming mode, reopen Game Center when the last window goes away
// so the user is never stranded on a bare wallpaper.
Connections {
target: windowMaximizedTracker
enabled: ShellSettings.Settings.gamingModeEnabled
function onShowingWindowChanged() {
if (!windowMaximizedTracker.showingWindow && !root.gameCenterOpen) {
root.gameCenterOpen = true
}
}
}
// 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.gamingModeEnabled) {
// In gaming mode Home/Menu should reopen the Game Center overlay.
root.gameCenterOpen = true;
return;
}
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
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
// Convergence: no scrim (popup has own background); mobile: dark scrim
color: ShellSettings.Settings.convergenceModeEnabled
? "transparent"
: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.46)
opacity: folio.HomeScreenState.appDrawerOpenProgress
}
Rectangle {
id: searchWidgetBackground
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.30)
opacity: folio.HomeScreenState.searchWidgetOpenProgress
}
Rectangle {
id: settingsViewBackground
anchors.fill: parent
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: MobileShell.SurfaceColors.withAlpha(MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.18, 0.10), 0.30)
opacity: folio.HomeScreenState.settingsOpenProgress
2019-09-04 16:39:31 +00:00
}
// Unified convergence chrome — renders the visible top bar, workspace
// frame, and dock in one mapped surface so they appear together.
// Invisible reserver surfaces in the panel/taskpanel containments still
// provide the exclusive zones that shrink KWin's MaximizeArea.
Window {
id: convergenceChrome
readonly property bool active: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& !folio.overviewActive
visible: active
color: "transparent"
width: Screen.width
height: Screen.height
LayerShell.Window.scope: "convergence-chrome"
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
// 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
property bool inputRegionInitialized: false
readonly property real topBarHeight: MobileShell.Constants.topPanelHeight
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real topBarHitHeight: topBarHeight + frameThickness
readonly property real frameRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, Math.max(0, Math.min(workAreaWidth, workAreaHeight) / 2))
readonly property real workAreaX: frameThickness
readonly property real workAreaY: topBarHitHeight
readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)
readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2)
readonly property real leftEdgeHotzoneWidth: Math.max(frameThickness, Math.round(Kirigami.Units.gridUnit * 0.7))
readonly property real rightEdgeHotzoneWidth: leftEdgeHotzoneWidth
readonly property real leftLauncherWidth: Math.min(Kirigami.Units.gridUnit * 22, width * 0.42)
readonly property real leftLauncherHeight: Math.min(Kirigami.Units.gridUnit * 16, workAreaHeight * 0.66)
readonly property bool leftLauncherEnabled: root.folio.HomeScreenState.appDrawerOpenProgress <= 0
readonly property real layoutMenuWidth: Math.min(Kirigami.Units.gridUnit * 16, width * 0.34)
readonly property int layoutMenuWindowCount: Math.max(0, ShellSettings.Settings.dynamicTilingLayoutWindowCount)
readonly property bool layoutMenuEnabled: ShellSettings.Settings.dynamicTilingEnabled
&& layoutMenuWindowCount >= 2
&& root.folio.HomeScreenState.appDrawerOpenProgress <= 0
readonly property real leftFrameBulgeIdleDepth: Math.max(frameThickness * 0.45, Kirigami.Units.gridUnit * 0.16)
readonly property real leftFrameBulgeHoverDepth: 0
property real leftFrameBulgeDepth: !leftLauncherEnabled || leftLauncherOpen || leftEdgeHovered
? leftFrameBulgeHoverDepth
: leftFrameBulgeIdleDepth
property real rightFrameBulgeDepth: !layoutMenuEnabled || layoutMenuOpen || rightEdgeHovered
? leftFrameBulgeHoverDepth
: leftFrameBulgeIdleDepth
// Long, thin thickening of the lower-left workspace wall. Vertical
// tangents at all three anchors keep the curve smooth as it blends
// into the straight wall above and below.
readonly property real leftFrameBulgeEffectiveDepth: Math.max(leftFrameBulgeDepth, 0.01)
readonly property real leftFrameBulgeApexX: workAreaX + leftFrameBulgeEffectiveDepth
readonly property real leftFrameBulgeHalfLength: Kirigami.Units.gridUnit * 7.5
readonly property real leftFrameBulgeApexY: workAreaY + workAreaHeight * 0.7
readonly property real leftFrameBulgeEdgeTopY: leftFrameBulgeApexY - leftFrameBulgeHalfLength
readonly property real leftFrameBulgeEdgeBottomY: leftFrameBulgeApexY + leftFrameBulgeHalfLength
// Bezier control-handle length along the vertical tangent at each
// anchor. ~0.55 of the half-length gives a clean, taut oval profile.
readonly property real leftFrameBulgeTangent: leftFrameBulgeHalfLength * 0.55
readonly property real rightFrameBulgeEffectiveDepth: Math.max(rightFrameBulgeDepth, 0.01)
readonly property real rightFrameBulgeApexX: workAreaX + workAreaWidth - rightFrameBulgeEffectiveDepth
readonly property real rightFrameBulgeHalfLength: leftFrameBulgeHalfLength
readonly property real rightFrameBulgeApexY: leftFrameBulgeApexY
readonly property real rightFrameBulgeEdgeTopY: rightFrameBulgeApexY - rightFrameBulgeHalfLength
readonly property real rightFrameBulgeEdgeBottomY: rightFrameBulgeApexY + rightFrameBulgeHalfLength
readonly property real rightFrameBulgeTangent: rightFrameBulgeHalfLength * 0.55
readonly property color chromeColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.32, 0.18)
readonly property color edgeColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
readonly property int dockAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
// 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: MobileShell.Constants.convergenceDockRevealHeight
// True once the hover-reveal timer fires; cleared on hover-exit.
property bool hoverRevealing: false
property bool leftEdgeHovered: false
property bool leftLauncherHovered: false
property bool leftLauncherOpen: false
property bool rightEdgeHovered: false
property bool layoutMenuHovered: false
property bool layoutMenuOpen: false
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
function updateInputRegion() {
const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)
const leftEdgeRegion = Qt.rect(0, topBarHitHeight, leftEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight))
const rightEdgeRegion = Qt.rect(width - rightEdgeHotzoneWidth, topBarHitHeight, rightEdgeHotzoneWidth, Math.max(0, height - topBarHitHeight - dockHeight))
const leftLauncherRegion = Qt.rect(0,
Math.max(0, height - dockHeight - leftLauncherHeight),
leftLauncherWidth,
leftLauncherHeight)
const layoutMenuRegion = Qt.rect(rightLayoutMenu.x,
rightLayoutMenu.y,
rightLayoutMenu.width,
rightLayoutMenu.height)
let regions = [topBarRegion, leftEdgeRegion]
if (layoutMenuEnabled) {
regions.push(rightEdgeRegion)
}
if (shouldHide && dockOffset >= dockHeight) {
regions.push(Qt.rect(0, height - revealStripHeight, width, revealStripHeight))
} else {
regions.push(Qt.rect(0, height - dockHeight, width, dockHeight))
}
if (leftLauncherOpen) {
regions.push(leftLauncherRegion)
}
if (layoutMenuEnabled && layoutMenuOpen) {
regions.push(layoutMenuRegion)
}
MobileShell.ShellUtil.setInputRegions(convergenceChrome, regions)
}
function launchStorageId(storageId) {
if (!storageId || String(storageId).length === 0) {
return
}
var normalizedId = String(storageId)
if (!normalizedId.endsWith(".desktop")) {
normalizedId += ".desktop"
}
MobileShell.AppLaunch.launchOrActivateApp(normalizedId)
}
function refreshLeftLauncherVisibility() {
if (!leftLauncherEnabled) {
leftLauncherCloseTimer.stop()
leftEdgeHovered = false
leftLauncherHovered = false
leftLauncherOpen = false
inputRegionTimer.restart()
return
}
if (leftEdgeHovered || leftLauncherHovered) {
leftLauncherCloseTimer.stop()
leftLauncherOpen = true
} else {
leftLauncherCloseTimer.restart()
}
inputRegionTimer.restart()
}
function refreshLayoutMenuVisibility() {
if (!layoutMenuEnabled) {
layoutMenuCloseTimer.stop()
rightEdgeHovered = false
layoutMenuHovered = false
layoutMenuOpen = false
inputRegionTimer.restart()
return
}
if (rightEdgeHovered || layoutMenuHovered) {
layoutMenuCloseTimer.stop()
layoutMenuOpen = true
} else {
layoutMenuCloseTimer.restart()
}
inputRegionTimer.restart()
}
onActiveChanged: {
hoverRevealTimer.stop()
hoverRevealing = false
inputRegionInitialized = false
dockOffset = shouldHide ? dockHeight : 0
inputRegionTimer.restart()
}
onShouldHideChanged: {
if (shouldHide) {
dockOffset = dockHeight
} else {
dockOffset = 0
}
inputRegionTimer.restart()
}
onLeftLauncherEnabledChanged: refreshLeftLauncherVisibility()
onLayoutMenuEnabledChanged: refreshLayoutMenuVisibility()
// 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: {
inputRegionTimer.restart()
}
onWidthChanged: inputRegionTimer.restart()
onHeightChanged: inputRegionTimer.restart()
onFrameSwapped: {
if (!inputRegionInitialized) {
inputRegionInitialized = true
inputRegionTimer.restart()
}
}
Timer {
id: inputRegionTimer
interval: 0
repeat: false
onTriggered: convergenceChrome.updateInputRegion()
}
// Delay reveal briefly so a quick edge graze does not pop the
// dock up mid-interaction with the underlying application.
Timer {
id: hoverRevealTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: convergenceChrome.hoverRevealing = true
}
Timer {
id: leftLauncherCloseTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: {
if (!convergenceChrome.leftEdgeHovered
&& !convergenceChrome.leftLauncherHovered
&& convergenceChrome.leftLauncherOpen) {
convergenceChrome.leftLauncherOpen = false
inputRegionTimer.restart()
}
}
}
Timer {
id: layoutMenuCloseTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: {
if (!convergenceChrome.rightEdgeHovered
&& !convergenceChrome.layoutMenuHovered
&& convergenceChrome.layoutMenuOpen) {
convergenceChrome.layoutMenuOpen = false
inputRegionTimer.restart()
}
}
}
Behavior on dockOffset {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: convergenceChrome.dockAnimationDuration
}
}
Behavior on leftFrameBulgeDepth {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: root.shortAnimationDuration
}
}
Behavior on rightFrameBulgeDepth {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: root.shortAnimationDuration
}
}
Rectangle {
id: topBarSurface
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: convergenceChrome.topBarHeight
color: convergenceChrome.chromeColor
MobileShell.StatusBar {
anchors.fill: parent
showSecondRow: false
showTime: true
backgroundColor: "transparent"
}
}
Shape {
id: workspaceFrame
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: convergenceChrome.chromeColor
fillRule: ShapePath.OddEvenFill
strokeWidth: 0
startX: 0
startY: convergenceChrome.topBarHeight
PathLine { x: convergenceChrome.width; y: convergenceChrome.topBarHeight }
PathLine { x: convergenceChrome.width; y: convergenceChrome.height - convergenceChrome.dockHeight }
PathLine { x: 0; y: convergenceChrome.height - convergenceChrome.dockHeight }
PathLine { x: 0; y: convergenceChrome.topBarHeight }
PathMove { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.rightFrameBulgeEdgeTopY }
PathCubic {
x: convergenceChrome.rightFrameBulgeApexX
y: convergenceChrome.rightFrameBulgeApexY
control1X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control1Y: convergenceChrome.rightFrameBulgeEdgeTopY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.rightFrameBulgeApexX
control2Y: convergenceChrome.rightFrameBulgeApexY - convergenceChrome.rightFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.rightFrameBulgeEdgeBottomY
control1X: convergenceChrome.rightFrameBulgeApexX
control1Y: convergenceChrome.rightFrameBulgeApexY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control2Y: convergenceChrome.rightFrameBulgeEdgeBottomY - convergenceChrome.rightFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight }
PathArc { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.leftFrameBulgeEdgeBottomY }
PathCubic {
x: convergenceChrome.leftFrameBulgeApexX
y: convergenceChrome.leftFrameBulgeApexY
control1X: convergenceChrome.workAreaX
control1Y: convergenceChrome.leftFrameBulgeEdgeBottomY - convergenceChrome.leftFrameBulgeTangent
control2X: convergenceChrome.leftFrameBulgeApexX
control2Y: convergenceChrome.leftFrameBulgeApexY + convergenceChrome.leftFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX
y: convergenceChrome.leftFrameBulgeEdgeTopY
control1X: convergenceChrome.leftFrameBulgeApexX
control1Y: convergenceChrome.leftFrameBulgeApexY - convergenceChrome.leftFrameBulgeTangent
control2X: convergenceChrome.workAreaX
control2Y: convergenceChrome.leftFrameBulgeEdgeTopY + convergenceChrome.leftFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
}
}
Shape {
id: workspaceFrameBorder
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: convergenceChrome.edgeColor
strokeWidth: 0.85
joinStyle: ShapePath.RoundJoin
startX: convergenceChrome.workAreaX + convergenceChrome.frameRadius
startY: convergenceChrome.workAreaY
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathQuad {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.workAreaY + convergenceChrome.frameRadius
controlX: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
controlY: convergenceChrome.workAreaY
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.rightFrameBulgeEdgeTopY }
PathCubic {
x: convergenceChrome.rightFrameBulgeApexX
y: convergenceChrome.rightFrameBulgeApexY
control1X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control1Y: convergenceChrome.rightFrameBulgeEdgeTopY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.rightFrameBulgeApexX
control2Y: convergenceChrome.rightFrameBulgeApexY - convergenceChrome.rightFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
y: convergenceChrome.rightFrameBulgeEdgeBottomY
control1X: convergenceChrome.rightFrameBulgeApexX
control1Y: convergenceChrome.rightFrameBulgeApexY + convergenceChrome.rightFrameBulgeTangent
control2X: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
control2Y: convergenceChrome.rightFrameBulgeEdgeBottomY - convergenceChrome.rightFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius }
PathQuad {
x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius
y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight
controlX: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth
controlY: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight
}
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight }
PathQuad {
x: convergenceChrome.workAreaX
y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius
controlX: convergenceChrome.workAreaX
controlY: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight
}
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.leftFrameBulgeEdgeBottomY }
PathCubic {
x: convergenceChrome.leftFrameBulgeApexX
y: convergenceChrome.leftFrameBulgeApexY
control1X: convergenceChrome.workAreaX
control1Y: convergenceChrome.leftFrameBulgeEdgeBottomY - convergenceChrome.leftFrameBulgeTangent
control2X: convergenceChrome.leftFrameBulgeApexX
control2Y: convergenceChrome.leftFrameBulgeApexY + convergenceChrome.leftFrameBulgeTangent
}
PathCubic {
x: convergenceChrome.workAreaX
y: convergenceChrome.leftFrameBulgeEdgeTopY
control1X: convergenceChrome.leftFrameBulgeApexX
control1Y: convergenceChrome.leftFrameBulgeApexY - convergenceChrome.leftFrameBulgeTangent
control2X: convergenceChrome.workAreaX
control2Y: convergenceChrome.leftFrameBulgeEdgeTopY + convergenceChrome.leftFrameBulgeTangent
}
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius }
PathQuad {
x: convergenceChrome.workAreaX + convergenceChrome.frameRadius
y: convergenceChrome.workAreaY
controlX: convergenceChrome.workAreaX
controlY: convergenceChrome.workAreaY
}
}
}
Rectangle {
id: dockSurface
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: convergenceChrome.dockHeight
color: "transparent"
HoverHandler {
id: dockHoverHandler
onHoveredChanged: {
if (hovered) {
hoverRevealTimer.start()
} else {
hoverRevealTimer.stop()
convergenceChrome.hoverRevealing = false
}
}
}
Rectangle {
anchors.fill: parent
visible: !convergenceChrome.shouldHide || convergenceChrome.dockOffset < convergenceChrome.dockHeight
color: convergenceChrome.chromeColor
}
FavouritesBar {
id: dockOverlayBar
anchors.fill: parent
folio: root.folio
maskManager: root.maskManager
homeScreen: folioHomeScreen
suppressRunningTasks: runningAppsPanel.visible
transform: Translate { y: convergenceChrome.dockOffset }
// Dock is an opaque panel — use Window colorset so all content
// (labels, hover highlights, icon tints) follows the system theme
// instead of the containment's Complementary wallpaper context.
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
}
Item {
id: leftEdgeStrip
anchors.left: parent.left
anchors.top: topBarSurface.bottom
anchors.bottom: dockSurface.top
width: convergenceChrome.leftEdgeHotzoneWidth
MouseArea {
id: leftEdgeHoverArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
enabled: convergenceChrome.leftLauncherEnabled
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: {
convergenceChrome.leftEdgeHovered = containsMouse
convergenceChrome.refreshLeftLauncherVisibility()
}
}
}
Item {
id: rightEdgeStrip
anchors.right: parent.right
anchors.top: topBarSurface.bottom
anchors.bottom: dockSurface.top
width: convergenceChrome.layoutMenuEnabled ? convergenceChrome.rightEdgeHotzoneWidth : 0
MouseArea {
id: rightEdgeHoverArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
enabled: convergenceChrome.layoutMenuEnabled
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: {
convergenceChrome.rightEdgeHovered = containsMouse
convergenceChrome.refreshLayoutMenuVisibility()
}
}
}
DynamicTilingLayoutMenu {
id: rightLayoutMenu
width: convergenceChrome.layoutMenuWidth
height: preferredHeight
x: convergenceChrome.width - width
y: convergenceChrome.height - convergenceChrome.dockHeight - height
visible: convergenceChrome.layoutMenuOpen
opacity: convergenceChrome.layoutMenuOpen ? 1 : 0
maxHeight: convergenceChrome.workAreaHeight * 0.5
windowCount: convergenceChrome.layoutMenuWindowCount
currentMode: ShellSettings.Settings.dynamicTilingLayoutMode
surfaceColor: convergenceChrome.chromeColor
animationDuration: root.shortAnimationDuration
HoverHandler {
enabled: convergenceChrome.layoutMenuOpen
onHoveredChanged: {
convergenceChrome.layoutMenuHovered = hovered
convergenceChrome.refreshLayoutMenuVisibility()
}
}
transform: Translate {
y: convergenceChrome.layoutMenuOpen ? 0 : Kirigami.Units.gridUnit
x: convergenceChrome.layoutMenuOpen ? 0 : rightLayoutMenu.width - convergenceChrome.rightEdgeHotzoneWidth
}
onLayoutModeRequested: (mode) => {
if (ShellSettings.Settings.requestDynamicTilingLayoutMode !== undefined) {
ShellSettings.Settings.requestDynamicTilingLayoutMode(mode)
}
}
onDismissRequested: {
convergenceChrome.layoutMenuOpen = false
inputRegionTimer.restart()
}
}
Item {
id: leftEdgeLauncher
width: convergenceChrome.leftLauncherWidth
height: convergenceChrome.leftLauncherHeight
x: 0
y: convergenceChrome.height - convergenceChrome.dockHeight - height
visible: convergenceChrome.leftLauncherOpen
opacity: convergenceChrome.leftLauncherOpen ? 1 : 0
clip: true
transform: Translate {
y: convergenceChrome.leftLauncherOpen ? 0 : Kirigami.Units.gridUnit
x: convergenceChrome.leftLauncherOpen ? 0 : -leftEdgeLauncher.width + convergenceChrome.leftEdgeHotzoneWidth
}
Behavior on opacity {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.EffectsFast
duration: root.shortAnimationDuration
}
}
readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, height * 0.24)
HoverHandler {
enabled: convergenceChrome.leftLauncherOpen
onHoveredChanged: {
convergenceChrome.leftLauncherHovered = hovered
convergenceChrome.refreshLeftLauncherVisibility()
}
}
Shape {
id: leftLauncherSurface
anchors.fill: parent
ShapePath {
fillColor: convergenceChrome.chromeColor
strokeWidth: 0
startX: 0
startY: 0
PathLine { x: leftEdgeLauncher.width - leftEdgeLauncher.cornerRadius; y: 0 }
PathArc {
x: leftEdgeLauncher.width
y: leftEdgeLauncher.cornerRadius
radiusX: leftEdgeLauncher.cornerRadius
radiusY: leftEdgeLauncher.cornerRadius
}
PathLine { x: leftEdgeLauncher.width; y: leftEdgeLauncher.height }
PathLine { x: 0; y: leftEdgeLauncher.height }
PathLine { x: 0; y: 0 }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.gridUnit * 0.65
ColumnLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.Label {
Layout.fillWidth: true
text: i18n("Recently Used")
font.weight: Font.Medium
elide: Text.ElideRight
}
ListView {
id: recentAppsList
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 6.8
clip: true
interactive: false
boundsBehavior: Flickable.StopAtBounds
model: folio.RecentApplicationsModel
delegate: MouseArea {
required property int index
required property var model
readonly property var delegateObject: model.delegate
readonly property var application: delegateObject ? delegateObject.application : null
readonly property bool validEntry: index < 5 && application !== null
width: recentAppsList.width
height: validEntry ? Kirigami.Units.gridUnit * 1.35 : 0
enabled: validEntry
hoverEnabled: validEntry
cursorShape: validEntry ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (application) {
convergenceChrome.launchStorageId(application.storageId)
}
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: parent.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
: "transparent"
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Kirigami.Icon {
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Layout.preferredWidth
source: application ? application.icon : ""
}
PlasmaComponents.Label {
Layout.fillWidth: true
text: application ? application.name : ""
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.Label {
Layout.fillWidth: true
text: i18n("Most Used")
font.weight: Font.Medium
elide: Text.ElideRight
}
ListView {
id: favouritesQuickList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
interactive: false
boundsBehavior: Flickable.StopAtBounds
model: folio.MostUsedApplicationsModel
delegate: MouseArea {
required property int index
required property var model
readonly property var delegateObject: model.delegate
readonly property var application: delegateObject ? delegateObject.application : null
readonly property bool validEntry: index < 6 && application !== null
width: favouritesQuickList.width
height: validEntry ? Kirigami.Units.gridUnit * 1.35 : 0
enabled: validEntry
hoverEnabled: validEntry
cursorShape: validEntry ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (application) {
convergenceChrome.launchStorageId(application.storageId)
}
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: parent.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
: "transparent"
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Kirigami.Icon {
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Layout.preferredWidth
source: application ? application.icon : ""
}
PlasmaComponents.Label {
Layout.fillWidth: true
text: application ? application.name : ""
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}
}
}
}
// 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
&& !ShellSettings.Settings.gamingModeEnabled
&& 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 dockHeight: MobileShell.Constants.convergenceDockHeight
2026-05-23 13:58:56 +00:00
readonly property real sideInset: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real connectedPanelGap: 0
2026-05-19 07:13:25 +00:00
readonly property real popupTopY: MobileShell.Constants.topPanelHeight
+ MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real popupBottomY: parent.height
- dockHeight
- MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real popupHeight: Math.max(0, popupBottomY - popupTopY)
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)
2026-05-23 13:58:56 +00:00
x: sideInset
y: (opacity > 0)
2026-05-19 07:13:25 +00:00
? popupTopY + 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() {
folio.ApplicationListSearchModel.categoryFilter = ""
overlayDrawer.forceActiveFocus()
}
}
}
CategoryPanel {
id: categoryPanel
folio: root.folio
width: Kirigami.Units.gridUnit * 9
height: overlayDrawer.popupHeight
2026-05-23 13:58:56 +00:00
x: overlayDrawer.x + overlayDrawer.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
opacity: overlayDrawer.opacity
onCategorySelected: (catId) => {
folio.ApplicationListSearchModel.categoryFilter = catId
overlayDrawerHeader.clearSearchText()
}
}
Rectangle {
id: powerPanel
// Width: just enough for one icon button + side margins
readonly property real tileSize: Kirigami.Units.iconSizes.medium + 2 * Kirigami.Units.largeSpacing
width: tileSize
height: overlayDrawer.popupHeight
x: runningAppsPanel.visible
2026-05-23 13:58:56 +00:00
? runningAppsPanel.x + runningAppsPanel.width + overlayDrawer.connectedPanelGap
: categoryPanel.x + categoryPanel.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
opacity: overlayDrawer.opacity
2026-05-23 13:58:56 +00:00
radius: 0
color: "transparent"
MouseArea {
anchors.fill: parent
}
KCoreAddons.KUser {
id: kuser
}
SessionManagement {
id: powerSession
}
// Close button anchored to top — smaller than power icons
Rectangle {
id: closeButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
height: Kirigami.Units.iconSizes.smallMedium + 2 * Kirigami.Units.smallSpacing
radius: Kirigami.Units.cornerRadius
color: closeArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: closeArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.smallMedium
height: width
source: "window-close-symbolic"
active: closeArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PlasmaComponents.ToolTip {
text: i18n("Close")
visible: closeArea.containsMouse
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: folio.HomeScreenState.closeAppDrawer()
}
}
// Separator below close button
Rectangle {
anchors.top: closeButton.bottom
anchors.topMargin: Kirigami.Units.smallSpacing
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
height: 1
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.15)
}
// Power buttons centred vertically in the panel
Column {
id: powerColumn
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Rectangle {
width: parent.width
height: width
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"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-lock-screen"
active: lockArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PlasmaComponents.ToolTip {
text: i18n("Lock Screen")
visible: lockArea.containsMouse
}
MouseArea {
id: lockArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerSession.lock()
folio.HomeScreenState.closeAppDrawer()
}
}
}
Rectangle {
width: parent.width
height: width
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"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-reboot"
active: rebootArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PlasmaComponents.ToolTip {
text: i18n("Restart")
visible: rebootArea.containsMouse
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
folio.HomeScreenState.closeAppDrawer()
powerSession.requestReboot()
}
}
}
Rectangle {
width: parent.width
height: width
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"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: "system-shutdown"
active: shutdownArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PlasmaComponents.ToolTip {
text: i18n("Shut Down")
visible: shutdownArea.containsMouse
}
MouseArea {
id: shutdownArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
folio.HomeScreenState.closeAppDrawer()
powerSession.requestShutdown()
}
}
}
}
// Separator above user avatar
Rectangle {
anchors.bottom: userSection.top
anchors.bottomMargin: Kirigami.Units.smallSpacing
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.rightMargin: Kirigami.Units.smallSpacing
height: 1
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.15)
}
// User avatar anchored to bottom — tooltip shows name, click opens user settings
Rectangle {
id: userSection
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Kirigami.Units.smallSpacing
height: width
radius: Kirigami.Units.cornerRadius
color: userArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: userArea.containsMouse
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
: "transparent"
KirigamiAddonsComponents.Avatar {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.medium
height: width
source: kuser.faceIconUrl
name: kuser.fullName || kuser.loginName
}
PlasmaComponents.ToolTip {
text: kuser.fullName || kuser.loginName
visible: userArea.containsMouse
}
MouseArea {
id: userArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
KCM.KCMLauncher.openSystemSettings("kcm_users")
folio.HomeScreenState.closeAppDrawer()
}
}
}
}
RunningAppsPanel {
id: runningAppsPanel
folio: root.folio
2026-05-23 13:58:56 +00:00
x: categoryPanel.x + categoryPanel.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
2026-05-23 13:58:56 +00:00
width: Math.max(0, parent.width - x - powerPanel.width - overlayDrawer.sideInset - overlayDrawer.connectedPanelGap)
height: overlayDrawer.popupHeight
opacity: overlayDrawer.opacity
visible: hasTasks && opacity > 0
onTaskActivated: folio.HomeScreenState.closeAppDrawer()
}
Shape {
id: drawerSurface
x: overlayDrawer.x
y: overlayDrawer.y
width: bodyWidth + cornerRadius
height: overlayDrawer.popupHeight
opacity: overlayDrawer.opacity
visible: opacity > 0 && bodyWidth > 0 && height > 0
z: -1
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
readonly property real bodyWidth: Math.max(0, powerPanel.x + powerPanel.width - overlayDrawer.x)
readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius,
Math.max(0.01, Math.min(bodyWidth, height) / 2))
ShapePath {
id: drawerSurfacePath
readonly property real cornerRadius: drawerSurface.cornerRadius
fillColor: MobileShell.SurfaceColors.accentSurface(Kirigami.Theme.backgroundColor, 0.32, 0.18)
strokeWidth: 0
startX: 0
startY: 0
PathLine { x: drawerSurface.bodyWidth + drawerSurfacePath.cornerRadius; y: 0 }
PathArc { x: drawerSurface.bodyWidth; y: drawerSurfacePath.cornerRadius; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }
PathLine { x: drawerSurface.bodyWidth; y: drawerSurface.height - drawerSurfacePath.cornerRadius }
PathArc { x: drawerSurface.bodyWidth + drawerSurfacePath.cornerRadius; y: drawerSurface.height; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }
PathLine { x: 0; y: drawerSurface.height }
PathLine { x: 0; y: 0 }
}
}
}
// Game Center overlay — full-screen grid of games shown when gaming mode
// is active. Sits at LayerTop so it covers running application windows
// without going above system notifications.
GameCenterOverlay {
id: gameCenterOverlay
folio: root.folio
visible: ShellSettings.Settings.gamingModeEnabled && root.gameCenterOpen
onGameStarted: root.gameCenterOpen = false
onDismissRequested: {
root.gameCenterOpen = false
if (ShellSettings.Settings.gamingDismissHintEnabled) {
root.showGameCenterHint = true
gameCenterHintTimer.restart()
}
}
}
// Small persistent button at the top-right corner of the screen that lets
// the user return to the Game Center after launching a game.
// Keep the Loader active for the full duration of gaming mode so the
// opacity Behavior in GamingHUD can animate both fade-in and fade-out.
//
// Hide the HUD while a game window covers the screen. A mapped LayerShell
// surface prevents KWin from using DRM direct scanout for the fullscreen
// game window. Setting showing=false triggers the opacity fade-out and then
// sets visible=false, which unmaps the Wayland surface and lets KWin bypass
// the compositor render loop entirely for the game frame.
Loader {
active: ShellSettings.Settings.gamingModeEnabled
sourceComponent: GamingHUD {
visible: showing
showing: !root.gameCenterOpen && !windowMaximizedTracker.showingWindow
onOpenRequested: root.gameCenterOpen = true
}
}
Rectangle {
id: gameCenterHint
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Kirigami.Units.gridUnit * 2
visible: root.showGameCenterHint && ShellSettings.Settings.gamingDismissHintEnabled
opacity: visible ? 1 : 0
z: 2000
radius: Kirigami.Units.cornerRadius
color: Qt.rgba(0, 0, 0, 0.65)
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.2)
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
implicitWidth: hintText.implicitWidth + Kirigami.Units.gridUnit * 2
implicitHeight: hintText.implicitHeight + Kirigami.Units.largeSpacing
PlasmaComponents.Label {
id: hintText
anchors.centerIn: parent
text: i18n("Gaming mode is still on. Use Home or the gamepad icon to reopen Game Center.")
color: "white"
wrapMode: Text.WordWrap
width: Math.min(root.width * 0.8, Kirigami.Units.gridUnit * 30)
horizontalAlignment: Text.AlignHCenter
}
}
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
topMargin: horizontal ? folioHomeScreen.topMargin : 0
bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin
leftMargin: horizontal ? folioHomeScreen.leftMargin : 0
rightMargin: horizontal ? folioHomeScreen.rightMargin : 0
onClosed: {
wallpaperSelectorLoader.active = false;
}
onWallpaperSettingsRequested: {
close();
folioHomeScreen.openConfigure();
}
}
}
}