Suppress shell chrome in gaming mode

Hide the navigation panel, status bar, and app drawer while
gamingModeEnabled is active. The KWin convergent-windows script
skips its window policy so game windows are not forcibly tiled
or maximized.

The Home button in gaming mode re-opens the Game Center overlay
rather than the app drawer. A configurable hint nudges the user
toward the HUD button after launching a game.
This commit is contained in:
Marco Allegretti 2026-04-19 13:51:08 +02:00
parent d901815c9d
commit b0739dd9a7
4 changed files with 134 additions and 6 deletions

View file

@ -25,12 +25,32 @@ 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 Folio.HomeScreen folio: root.plasmoid
// Tracks whether the Game Center grid is visible within gaming mode.
// Starts true when gaming mode turns on; set to false by a game launch.
property bool gameCenterOpen: false
property bool showGameCenterHint: false
Timer {
id: gameCenterHintTimer
interval: 2600
onTriggered: root.showGameCenterHint = false
}
Connections {
target: ShellSettings.Settings
function onGamingModeEnabledChanged() {
root.gameCenterOpen = ShellSettings.Settings.gamingModeEnabled
}
}
Component.onCompleted: {
folio.FolioSettings.load();
folio.FavouritesModel.load();
@ -89,6 +109,12 @@ ContainmentItem {
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.
@ -186,7 +212,7 @@ ContainmentItem {
// task panel containment; this window only provides the visible dock.
Window {
id: dockOverlay
visible: ShellSettings.Settings.convergenceModeEnabled
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
color: "transparent"
width: Screen.width
height: Kirigami.Units.gridUnit * 3
@ -286,6 +312,7 @@ ContainmentItem {
Window {
id: drawerOverlay
visible: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& folio.HomeScreenState.appDrawerOpenProgress > 0
color: "transparent"
width: Screen.width
@ -645,6 +672,68 @@ ContainmentItem {
}
}
// 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()
}
}
onVisibleChanged: {
if (!visible) {
folio.ApplicationListSearchModel.categoryFilter = ""
}
}
}
// Small persistent button at the top-right corner of the screen that lets
// the user return to the Game Center after launching a game.
GamingHUD {
visible: ShellSettings.Settings.gamingModeEnabled && !root.gameCenterOpen
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 {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
}
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

View file

@ -34,11 +34,17 @@ ContainmentItem {
// Whether the startup feedback is showing
readonly property bool showingStartupFeedback: MobileShellState.ShellDBusObject.startupFeedbackModel.activeWindowIsStartupFeedback
readonly property bool gamingMode: ShellSettings.Settings.gamingModeEnabled
// Whether an app is maximized and showing (does not include startup feedback)
readonly property bool showingApp: windowMaximizedTracker.showingWindow && !showingStartupFeedback
// Whether the currently showing app is in "fullscreen"
readonly property bool fullscreen: {
if (gamingMode) {
return true;
}
// In convergence mode the status bar is always visible, like a desktop panel.
if (ShellSettings.Settings.convergenceModeEnabled) {
return false;
@ -69,7 +75,7 @@ ContainmentItem {
}
}
readonly property real panelHeight: MobileShell.Constants.topPanelHeight
readonly property real panelHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight
onPanelHeightChanged: setWindowProperties()
function setWindowProperties() {
@ -123,6 +129,11 @@ ContainmentItem {
function onConvergenceModeEnabledChanged() {
root.setWindowProperties();
}
function onGamingModeEnabledChanged() {
root.setWindowProperties();
MobileShellState.ShellDBusClient.panelState = ShellSettings.Settings.gamingModeEnabled ? "hidden" : "default";
}
}
Component.onCompleted: {
@ -136,7 +147,7 @@ ContainmentItem {
// MaximizeArea by the panel height.
Window {
id: topBarSpaceReserver
visible: ShellSettings.Settings.convergenceModeEnabled
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
height: root.panelHeight
@ -152,6 +163,7 @@ ContainmentItem {
// Visual panel component
StatusPanel {
id: statusPanel
visible: !ShellSettings.Settings.gamingModeEnabled
anchors.fill: parent
containmentItem: root
}

View file

@ -38,12 +38,14 @@ ContainmentItem {
readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height)
readonly property real navigationPanelHeight: MobileShell.Constants.navigationPanelThickness
readonly property bool gamingMode: ShellSettings.Settings.gamingModeEnabled
readonly property real navigationPanelHeight: gamingMode ? 0 : MobileShell.Constants.navigationPanelThickness
onNavigationPanelHeightChanged: setWindowProperties()
readonly property real intendedWindowThickness: navigationPanelHeight
readonly property real intendedWindowLength: inLandscape ? Screen.height : Screen.width
readonly property real intendedWindowOffset: inLandscape ? MobileShell.Constants.topPanelHeight : 0; // offset for top panel
readonly property real intendedWindowOffset: (inLandscape && !gamingMode) ? MobileShell.Constants.topPanelHeight : 0; // offset for top panel
readonly property int intendedWindowLocation: inLandscape ? PlasmaCore.Types.RightEdge : PlasmaCore.Types.BottomEdge
onIntendedWindowLengthChanged: maximizeTimer.restart() // ensure it always takes up the full length of the screen
@ -136,6 +138,11 @@ ContainmentItem {
function onConvergenceModeEnabledChanged() {
root.setWindowProperties();
}
function onGamingModeEnabledChanged() {
root.setWindowProperties();
navigationPanel.offset = ShellSettings.Settings.gamingModeEnabled ? root.navigationPanelHeight : 0;
}
}
Component.onCompleted: setWindowProperties();
@ -153,6 +160,7 @@ ContainmentItem {
Window {
id: dockSpaceReserver
visible: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& !(ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow)
color: "transparent"
@ -177,7 +185,9 @@ ContainmentItem {
return (windowMaximizedTracker.showingWindow || isCurrentWindowFullscreen) && !showingStartupFeedback
}
readonly property alias isCurrentWindowFullscreen: windowMaximizedTracker.isCurrentWindowFullscreen
readonly property bool fullscreen: isCurrentWindowFullscreen || (ShellSettings.Settings.autoHidePanelsEnabled && opaqueBar)
readonly property bool fullscreen: ShellSettings.Settings.gamingModeEnabled
|| isCurrentWindowFullscreen
|| (ShellSettings.Settings.autoHidePanelsEnabled && opaqueBar)
WindowPlugin.WindowMaximizedTracker {
id: windowMaximizedTracker
@ -205,6 +215,7 @@ ContainmentItem {
Item {
id: navigationPanel
visible: !ShellSettings.Settings.gamingModeEnabled
anchors.fill: parent
property real offset: 0

View file

@ -60,6 +60,12 @@ Loader {
return;
}
if (ShellSettings.Settings.gamingModeEnabled) {
window.noBorder = true;
window.setMaximize(true, true);
return;
}
if (ShellSettings.Settings.convergenceModeEnabled) {
window.noBorder = false;
} else {
@ -128,6 +134,16 @@ Loader {
}
}
}
function onGamingModeEnabledChanged() {
const windows = KWinComponents.Workspace.windows;
for (let i = 0; i < windows.length; i++) {
if (windows[i].normalWindow) {
root.run(windows[i]);
}
}
}
}
Connections {