mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 15:03:09 +00:00
Harden gaming mode and fix startup crash
Strut windows committed zero height to layer-shell during early init, causing a protocol error and session exit. Wrap height and exclusionZone in Math.max(1, ...) for top bar and dock. Persisted gamingModeEnabled=true with hardcoded gameCenterOpen=false hid panels with no overlay visible. Initialize gameCenterOpen from the stored setting. Fix HUD visibility regression from an emergency visible:true override. Various null-guard and positioning fixes from static analysis across the gaming shell QML.
This commit is contained in:
parent
50ea5976ce
commit
70fa861b30
18 changed files with 129 additions and 52 deletions
7
Nebra.code-workspace
Normal file
7
Nebra.code-workspace
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -86,13 +86,15 @@ QQuickItem *AppletHost::fullRepresentationFor(const QString &pluginId)
|
|||
|
||||
m_containment->addApplet(applet);
|
||||
auto *item = PlasmaQuick::AppletQuickItem::itemForApplet(applet);
|
||||
m_items.insert(pluginId, item);
|
||||
|
||||
if (!item) {
|
||||
qWarning() << "AppletHost: no AppletQuickItem for" << pluginId;
|
||||
applet->deleteLater();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_items.insert(pluginId, item);
|
||||
|
||||
item->setPreloadFullRepresentation(true);
|
||||
|
||||
auto *fullRepItem = item->fullRepresentationItem();
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ Item {
|
|||
shrinkSize: 0
|
||||
|
||||
onClicked: {
|
||||
if (!root.taskModel) return;
|
||||
root.taskModel.requestActivate(root.taskModel.makeModelIndex(taskDelegate.index));
|
||||
}
|
||||
|
||||
|
|
@ -180,18 +181,27 @@ Item {
|
|||
Controls.MenuItem {
|
||||
text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize")
|
||||
icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"
|
||||
onTriggered: root.taskModel.requestToggleMinimized(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||
onTriggered: {
|
||||
if (!root.taskModel) return;
|
||||
root.taskModel.requestToggleMinimized(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||
}
|
||||
}
|
||||
Controls.MenuItem {
|
||||
text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize")
|
||||
icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize"
|
||||
onTriggered: root.taskModel.requestToggleMaximized(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||
onTriggered: {
|
||||
if (!root.taskModel) return;
|
||||
root.taskModel.requestToggleMaximized(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||
}
|
||||
}
|
||||
Controls.MenuSeparator {}
|
||||
Controls.MenuItem {
|
||||
text: i18n("Close")
|
||||
icon.name: "window-close"
|
||||
onTriggered: root.taskModel.requestClose(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||
onTriggered: {
|
||||
if (!root.taskModel) return;
|
||||
root.taskModel.requestClose(root.taskModel.makeModelIndex(taskDelegate.index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -706,12 +706,16 @@ MouseArea {
|
|||
x: {
|
||||
if (!targetDelegate) return 0
|
||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
||||
return Math.max(0, Math.min(Screen.width - width, delegateGlobal.x + (targetDelegate.width - width) / 2))
|
||||
var win = targetDelegate.Window.window
|
||||
var scrW = win && win.screen ? win.screen.width : Screen.width
|
||||
return Math.max(0, Math.min(scrW - width, delegateGlobal.x + (targetDelegate.width - width) / 2))
|
||||
}
|
||||
y: {
|
||||
if (!targetDelegate) return 0
|
||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
||||
return Math.max(0, Math.min(Screen.height - height, delegateGlobal.y - height - Kirigami.Units.smallSpacing))
|
||||
var win = targetDelegate.Window.window
|
||||
var scrH = win && win.screen ? win.screen.height : Screen.height
|
||||
return Math.max(0, Math.min(scrH - height, delegateGlobal.y - height - Kirigami.Units.smallSpacing))
|
||||
}
|
||||
|
||||
onShowingChanged: {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ Window {
|
|||
text: i18n("Exit Gaming Mode")
|
||||
display: QQC2.AbstractButton.TextBesideIcon
|
||||
Keys.onReturnPressed: clicked()
|
||||
Keys.onEnterPressed: clicked()
|
||||
onClicked: root.requestExitGamingMode()
|
||||
}
|
||||
}
|
||||
|
|
@ -183,7 +184,10 @@ Window {
|
|||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Leave")
|
||||
onTriggered: ShellSettings.Settings.gamingModeEnabled = false
|
||||
onTriggered: {
|
||||
ShellSettings.Settings.gamingModeEnabled = false
|
||||
theExitDialog.close()
|
||||
}
|
||||
}
|
||||
]
|
||||
onClosed: exitGamingDialog.active = false
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ QQC2.ItemDelegate {
|
|||
|
||||
onClicked: {
|
||||
if (!application) return
|
||||
if (application.icon !== "" && !application.running) {
|
||||
if (application.icon && typeof application.icon === "string" && application.icon.length > 0 && !application.running) {
|
||||
MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(
|
||||
Plasmoid.screen,
|
||||
application.icon,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@ Window {
|
|||
|
||||
signal openRequested()
|
||||
|
||||
width: Kirigami.Units.gridUnit * 4
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
// Guard against startup timing where Kirigami units may briefly be 0/NaN.
|
||||
// LayerShell surfaces must never be committed with zero size.
|
||||
readonly property real safeGridUnit: ((Kirigami.Units.gridUnit || 0) > 0) ? Kirigami.Units.gridUnit : 16
|
||||
width: safeGridUnit * 4
|
||||
height: safeGridUnit * 2
|
||||
color: "transparent"
|
||||
flags: Qt.FramelessWindowHint
|
||||
|
||||
|
|
@ -25,7 +28,11 @@ Window {
|
|||
LayerShell.Window.exclusionZone: 0
|
||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||
|
||||
opacity: visible ? 1 : 0
|
||||
// Driven by the Loader in folio/qml/main.qml — set false to fade out
|
||||
// before the Loader destroys the window.
|
||||
property bool showing: true
|
||||
|
||||
opacity: showing ? 1 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ Item {
|
|||
id: resumeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
z: -1
|
||||
// Leave room for the close button
|
||||
onClicked: {
|
||||
taskList.currentIndex = index
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ ContainmentItem {
|
|||
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
|
||||
// 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
|
||||
|
||||
Timer {
|
||||
|
|
@ -52,6 +53,7 @@ ContainmentItem {
|
|||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.gameCenterOpen = ShellSettings.Settings.gamingModeEnabled
|
||||
folio.FolioSettings.load();
|
||||
folio.FavouritesModel.load();
|
||||
folio.PageListModel.load();
|
||||
|
|
@ -698,12 +700,13 @@ ContainmentItem {
|
|||
|
||||
// Small persistent button at the top-right corner of the screen that lets
|
||||
// the user return to the Game Center after launching a game.
|
||||
// Wrapped in a Loader so the LayerShell Window is destroyed (not merely
|
||||
// hidden) when not needed — hiding a LayerShell window with AnchorTop|Right
|
||||
// but no fixed height causes a Wayland protocol error (height=0).
|
||||
// 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.
|
||||
Loader {
|
||||
active: ShellSettings.Settings.gamingModeEnabled && !root.gameCenterOpen
|
||||
active: ShellSettings.Settings.gamingModeEnabled
|
||||
sourceComponent: GamingHUD {
|
||||
visible: showing
|
||||
showing: !root.gameCenterOpen
|
||||
onOpenRequested: root.gameCenterOpen = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ Item {
|
|||
context: Qt.ApplicationShortcut
|
||||
onActivated: {
|
||||
if (drawer.actionDrawer.intendedToBeVisible) {
|
||||
drawer.actionDrawer.intendedToBeVisible = false;
|
||||
drawer.actionDrawer.close();
|
||||
} else {
|
||||
drawer.actionDrawer.openToPinnedMode = false;
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ ContainmentItem {
|
|||
|
||||
function onGamingModeEnabledChanged() {
|
||||
root.setWindowProperties();
|
||||
MobileShellState.ShellDBusClient.panelState = ShellSettings.Settings.gamingModeEnabled ? "hidden" : "default";
|
||||
MobileShellState.ShellDBusClient.panelState = ShellSettings.Settings.gamingModeEnabled ? "hidden" : (fullscreen ? "hidden" : "default");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,13 +150,13 @@ ContainmentItem {
|
|||
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
|
||||
color: "transparent"
|
||||
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
||||
height: root.panelHeight
|
||||
height: Math.max(1, root.panelHeight)
|
||||
width: 1
|
||||
|
||||
LayerShell.Window.scope: "topbar-space"
|
||||
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
||||
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
||||
LayerShell.Window.exclusionZone: root.panelHeight
|
||||
LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight)
|
||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ ContainmentItem {
|
|||
|
||||
function onGamingModeEnabledChanged() {
|
||||
root.setWindowProperties();
|
||||
navigationPanel.offset = ShellSettings.Settings.gamingModeEnabled ? root.navigationPanelHeight : 0;
|
||||
navigationPanel.offset = ShellSettings.Settings.gamingModeEnabled ? MobileShell.Constants.navigationPanelThickness : 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,13 +166,13 @@ ContainmentItem {
|
|||
color: "transparent"
|
||||
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
||||
// height is set by layer-shell anchoring; provide a fallback.
|
||||
height: Kirigami.Units.gridUnit * 3
|
||||
height: Math.max(1, MobileShell.Constants.navigationPanelThickness)
|
||||
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
|
||||
|
||||
LayerShell.Window.scope: "dock-space"
|
||||
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
||||
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
||||
LayerShell.Window.exclusionZone: Kirigami.Units.gridUnit * 3
|
||||
LayerShell.Window.exclusionZone: Math.max(1, MobileShell.Constants.navigationPanelThickness)
|
||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ Loader {
|
|||
|
||||
property var currentWindow
|
||||
|
||||
// Window that needs geometry clamping after un-maximize in convergence
|
||||
// mode. Set in onMaximizedChanged and consumed by the timer below.
|
||||
property var pendingConstrainWindow: null
|
||||
// Windows awaiting geometry clamping after un-maximize in convergence
|
||||
// mode. Using an array so concurrent un-maximizes are not lost.
|
||||
property var pendingConstrainWindows: []
|
||||
|
||||
// After a window is un-maximized in convergence mode, the dockSpaceReserver
|
||||
// LayerShell surface needs one Wayland roundtrip to (re)commit its exclusive
|
||||
|
|
@ -24,27 +24,30 @@ Loader {
|
|||
id: constrainAfterRestoreTimer
|
||||
interval: 200
|
||||
onTriggered: {
|
||||
const window = root.pendingConstrainWindow
|
||||
root.pendingConstrainWindow = null
|
||||
if (!window || window.deleted || !window.normalWindow) return
|
||||
if (!ShellSettings.Settings.convergenceModeEnabled) return
|
||||
const windows = root.pendingConstrainWindows.slice()
|
||||
root.pendingConstrainWindows = []
|
||||
for (const window of windows) {
|
||||
if (!window || window.deleted || !window.normalWindow) continue
|
||||
if (!ShellSettings.Settings.convergenceModeEnabled) continue
|
||||
if (ShellSettings.Settings.gamingModeEnabled) continue
|
||||
|
||||
const output = window.output
|
||||
const desktop = window.desktops[0]
|
||||
if (!output) return
|
||||
if (!desktop) return
|
||||
const output = window.output
|
||||
const desktop = window.desktops[0]
|
||||
if (!output) continue
|
||||
if (!desktop) continue
|
||||
|
||||
const maxRect = KWinComponents.Workspace.clientArea(
|
||||
KWinComponents.Workspace.MaximizeArea, output, desktop)
|
||||
const geo = window.frameGeometry
|
||||
const maxBottom = maxRect.y + maxRect.height
|
||||
const maxRect = KWinComponents.Workspace.clientArea(
|
||||
KWinComponents.Workspace.MaximizeArea, output, desktop)
|
||||
const geo = window.frameGeometry
|
||||
const maxBottom = maxRect.y + maxRect.height
|
||||
|
||||
if (geo.y + geo.height > maxBottom) {
|
||||
// Clip the bottom edge to MaximizeArea; preserve top position
|
||||
// and width. Ensure height is at least 100px to avoid
|
||||
// pathological cases where the window starts above maxRect.
|
||||
const newH = Math.max(100, maxBottom - geo.y)
|
||||
window.frameGeometry = Qt.rect(geo.x, geo.y, geo.width, newH)
|
||||
if (geo.y + geo.height > maxBottom) {
|
||||
// Clip the bottom edge to MaximizeArea; preserve top position
|
||||
// and width. Ensure height is at least 100px to avoid
|
||||
// pathological cases where the window starts above maxRect.
|
||||
const newH = Math.max(100, maxBottom - geo.y)
|
||||
window.frameGeometry = Qt.rect(geo.x, geo.y, geo.width, newH)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +100,9 @@ Loader {
|
|||
target: currentWindow
|
||||
|
||||
function onFullScreenChanged() {
|
||||
if (!currentWindow) {
|
||||
return;
|
||||
}
|
||||
currentWindow.interactiveMoveResizeFinished.connect((currentWindow) => {
|
||||
root.run(currentWindow);
|
||||
});
|
||||
|
|
@ -104,6 +110,9 @@ Loader {
|
|||
}
|
||||
|
||||
function onMaximizedChanged() {
|
||||
if (!currentWindow) {
|
||||
return;
|
||||
}
|
||||
if (!currentWindow.maximizable) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -116,7 +125,7 @@ Loader {
|
|||
// zone is re-committed over a Wayland roundtrip.
|
||||
if (ShellSettings.Settings.convergenceModeEnabled
|
||||
&& ShellSettings.Settings.autoHidePanelsEnabled) {
|
||||
root.pendingConstrainWindow = currentWindow
|
||||
root.pendingConstrainWindows.push(currentWindow)
|
||||
constrainAfterRestoreTimer.restart()
|
||||
}
|
||||
}
|
||||
|
|
@ -150,6 +159,9 @@ Loader {
|
|||
target: KWinComponents.Workspace
|
||||
|
||||
function onWindowAdded(window) {
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
if (window.normalWindow) {
|
||||
window.interactiveMoveResizeFinished.connect((window) => {
|
||||
root.run(window);
|
||||
|
|
@ -159,6 +171,9 @@ Loader {
|
|||
}
|
||||
|
||||
function onWindowActivated(window) {
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
if (window.normalWindow) {
|
||||
currentWindow = window;
|
||||
window.interactiveMoveResizeFinished.connect((window) => {
|
||||
|
|
|
|||
|
|
@ -3,4 +3,14 @@
|
|||
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_org.kde.plasma.quicksetting.gaming.pot
|
||||
set -e
|
||||
: "${XGETTEXT:?XGETTEXT is not set}"
|
||||
: "${podir:?podir is not set}"
|
||||
|
||||
mapfile -t files < <(find . \( -name '*.js' -o -name '*.qml' -o -name '*.cpp' \) -print)
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
echo "Messages.sh: no input files found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$XGETTEXT" "${files[@]}" -o "$podir/plasma_org.kde.plasma.quicksetting.gaming.pot"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ QS.QuickSetting {
|
|||
|
||||
function requestDisable() {
|
||||
confirmDisableDialog.active = true;
|
||||
confirmDisableDialog.item.open();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
|
|
@ -33,6 +32,7 @@ QS.QuickSetting {
|
|||
Loader {
|
||||
id: confirmDisableDialog
|
||||
active: false
|
||||
onLoaded: item.open()
|
||||
|
||||
sourceComponent: Kirigami.PromptDialog {
|
||||
id: theConfirmDialog
|
||||
|
|
@ -46,7 +46,10 @@ QS.QuickSetting {
|
|||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Leave")
|
||||
onTriggered: ShellSettings.Settings.gamingModeEnabled = false
|
||||
onTriggered: {
|
||||
ShellSettings.Settings.gamingModeEnabled = false
|
||||
theConfirmDialog.close()
|
||||
}
|
||||
}
|
||||
]
|
||||
onClosed: confirmDisableDialog.active = false
|
||||
|
|
|
|||
|
|
@ -2,4 +2,14 @@
|
|||
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
$XGETTEXT *.json contents/ui/*.qml -o $podir/plasma_mobile_qt.pot
|
||||
set -e
|
||||
: "${XGETTEXT:?XGETTEXT is not set}"
|
||||
: "${podir:?podir is not set}"
|
||||
|
||||
shopt -s nullglob
|
||||
files=(*.json contents/ui/*.qml)
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
echo "Messages.sh: no input files found" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$XGETTEXT" "${files[@]}" -o "$podir/plasma_mobile_qt.pot"
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
|||
QS.QuickSetting {
|
||||
text: i18n("Launch Hint")
|
||||
icon: "dialog-information"
|
||||
status: ""
|
||||
enabled: ShellSettings.Settings.gamingDismissHintEnabled
|
||||
status: ShellSettings.Settings.gamingDismissHintEnabled ? i18n("On") : i18n("Off")
|
||||
enabled: true
|
||||
available: ShellSettings.Settings.gamingModeEnabled
|
||||
|
||||
function toggle() {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2026 Marco Allegretti.
|
||||
SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
SPDX-License-Identifier: EUPL-1.2
|
||||
|
|
|
|||
Loading…
Reference in a new issue