Fixed more issues with Cinnamon window resizing and popups.
This commit is contained in:
@@ -5,11 +5,13 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Collection, Iterable, Sequence
|
||||
from collections.abc import Collection, Iterable, Mapping, Sequence
|
||||
|
||||
from .desktop_identity import (
|
||||
APPLICATION_NAME,
|
||||
@@ -44,12 +46,15 @@ _X_PROPERTY_CHANGE_MASK = 1 << 22
|
||||
_X_SUBSTRUCTURE_NOTIFY_MASK = 1 << 19
|
||||
_X_SUBSTRUCTURE_REDIRECT_MASK = 1 << 20
|
||||
_X_WM_STATE_WITHDRAWN = 0
|
||||
_X_WM_STATE_NORMAL = 1
|
||||
_X_WM_STATE_ICONIC = 3
|
||||
_NET_WM_STATE_REMOVE = 0
|
||||
_NET_WM_STATE_SOURCE_APPLICATION = 1
|
||||
_NET_WM_BYPASS_COMPOSITOR_PREFER_COMPOSITING = 2
|
||||
WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS = 1.0
|
||||
GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS = 0.25
|
||||
GAMESCOPE_HOST_FOCUS_STATE_ENV = "PTLINUX_GAMESCOPE_HOST_FOCUS_STATE"
|
||||
GAMESCOPE_HOST_FOCUS_STATE_MAX_AGE_SECONDS = 0.6
|
||||
_P_MIN_SIZE = 1 << 4
|
||||
_P_MAX_SIZE = 1 << 5
|
||||
_P_RESIZE_INC = 1 << 6
|
||||
@@ -64,6 +69,14 @@ _MOTIF_DECOR_MAXIMIZE = 1 << 6
|
||||
_X_ERROR_HANDLER = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
||||
|
||||
|
||||
def _process_exit_status(returncode: int) -> int:
|
||||
"""Convert Popen's negative signal code before passing it through SystemExit."""
|
||||
|
||||
if returncode < 0:
|
||||
return 128 + min(-returncode, 127)
|
||||
return min(returncode, 255)
|
||||
|
||||
|
||||
def _ignore_x11_error(_display: object, _event: object) -> int:
|
||||
"""Keep a stale X11 window race from terminating the mapper process."""
|
||||
return 0
|
||||
@@ -555,21 +568,14 @@ class _X11:
|
||||
and updated_normal_hints != normal_hints
|
||||
)
|
||||
motif_changed = motif_hints is not None and updated_motif_hints != motif_hints
|
||||
allowed_actions = self._long_property(window, b"_NET_WM_ALLOWED_ACTIONS", 64)
|
||||
allowed_actions_with_scaling = self._allowed_actions_with_scaling(
|
||||
allowed_actions,
|
||||
)
|
||||
allowed_actions_changed = (
|
||||
allowed_actions is not None
|
||||
and allowed_actions_with_scaling is not None
|
||||
and allowed_actions_with_scaling != allowed_actions
|
||||
)
|
||||
if (
|
||||
not normal_changed
|
||||
and not motif_changed
|
||||
and not allowed_actions_changed
|
||||
):
|
||||
return not confirm or self.window_allows_scaling(window)
|
||||
if not normal_changed and not motif_changed:
|
||||
if not confirm:
|
||||
return True
|
||||
# ``_NET_WM_ALLOWED_ACTIONS`` belongs to the window manager. Wait for
|
||||
# its answer even when the client hints were already resizable; Muffin
|
||||
# can publish the property shortly after the SDL window is discovered.
|
||||
self.library.XSync(self.display, 0)
|
||||
return self._wait_for_scaling_actions(window)
|
||||
|
||||
confirmed = False
|
||||
try:
|
||||
@@ -587,13 +593,6 @@ class _X11:
|
||||
b"_MOTIF_WM_HINTS",
|
||||
updated_motif_hints,
|
||||
)
|
||||
if allowed_actions_changed and allowed_actions_with_scaling is not None:
|
||||
self._replace_long_property(
|
||||
window,
|
||||
b"_NET_WM_ALLOWED_ACTIONS",
|
||||
b"_ATOM",
|
||||
allowed_actions_with_scaling,
|
||||
)
|
||||
if not confirm:
|
||||
self.library.XFlush(self.display)
|
||||
confirmed = True
|
||||
@@ -624,16 +623,6 @@ class _X11:
|
||||
)
|
||||
except Exception as exc:
|
||||
restore_errors.append(exc)
|
||||
if allowed_actions_changed and allowed_actions is not None:
|
||||
try:
|
||||
self._replace_long_property(
|
||||
window,
|
||||
b"_NET_WM_ALLOWED_ACTIONS",
|
||||
b"_ATOM",
|
||||
allowed_actions,
|
||||
)
|
||||
except Exception as exc:
|
||||
restore_errors.append(exc)
|
||||
try:
|
||||
self.library.XSync(self.display, 0)
|
||||
except Exception as exc:
|
||||
@@ -665,36 +654,6 @@ class _X11:
|
||||
vertical,
|
||||
}.issubset(actions)
|
||||
|
||||
def _allowed_actions_with_scaling(
|
||||
self,
|
||||
actions: tuple[int, ...] | None,
|
||||
) -> tuple[int, ...] | None:
|
||||
if actions is None:
|
||||
return None
|
||||
|
||||
resize = self._intern_atom(
|
||||
b"_NET_WM_ACTION_RESIZE",
|
||||
only_if_exists=True,
|
||||
)
|
||||
horizontal = self._intern_atom(
|
||||
b"_NET_WM_ACTION_MAXIMIZE_HORZ",
|
||||
only_if_exists=True,
|
||||
)
|
||||
vertical = self._intern_atom(
|
||||
b"_NET_WM_ACTION_MAXIMIZE_VERT",
|
||||
only_if_exists=True,
|
||||
)
|
||||
if resize == 0 or horizontal == 0 or vertical == 0:
|
||||
return actions
|
||||
|
||||
updated_actions = list(actions)
|
||||
known_actions = set(actions)
|
||||
for atom in (resize, horizontal, vertical):
|
||||
if atom not in known_actions:
|
||||
updated_actions.append(atom)
|
||||
known_actions.add(atom)
|
||||
return tuple(updated_actions)
|
||||
|
||||
def _wait_for_scaling_actions(self, window: int) -> bool:
|
||||
deadline = time.monotonic() + WINDOW_ACTION_CONFIRM_TIMEOUT_SECONDS
|
||||
while not self.window_allows_scaling(window):
|
||||
@@ -1004,6 +963,14 @@ class _X11:
|
||||
active = self._long_property(root, b"_NET_ACTIVE_WINDOW", 1)
|
||||
return bool(active and active[0] == window)
|
||||
|
||||
def minimize_window(self, window: int) -> bool:
|
||||
"""Ask the host window manager to keep a remapped client minimized."""
|
||||
|
||||
screen = self.library.XDefaultScreen(self.display)
|
||||
accepted = self.library.XIconifyWindow(self.display, window, screen) != 0
|
||||
self.library.XSync(self.display, 0)
|
||||
return accepted
|
||||
|
||||
def activate_window(self, window: int) -> bool:
|
||||
"""Ask the window manager to restore and activate one client."""
|
||||
|
||||
@@ -1174,6 +1141,14 @@ class _X11:
|
||||
self.library.XOpenDisplay.restype = ctypes.c_void_p
|
||||
self.library.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
|
||||
self.library.XDefaultRootWindow.restype = ctypes.c_ulong
|
||||
self.library.XDefaultScreen.argtypes = [ctypes.c_void_p]
|
||||
self.library.XDefaultScreen.restype = ctypes.c_int
|
||||
self.library.XIconifyWindow.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_ulong,
|
||||
ctypes.c_int,
|
||||
]
|
||||
self.library.XIconifyWindow.restype = ctypes.c_int
|
||||
self.library.XQueryTree.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_ulong,
|
||||
@@ -1457,6 +1432,8 @@ def _repair_game_window_focus(mapper: _X11, window: int) -> bool:
|
||||
def _repair_gamescope_game_window_focus(mapper: _X11, window: int) -> bool:
|
||||
"""Restore nested focus without asking the desktop to activate Gamescope."""
|
||||
|
||||
if not _gamescope_host_focus_is_allowed():
|
||||
return False
|
||||
try:
|
||||
if mapper.window_state(window) == _X_WM_STATE_ICONIC:
|
||||
return False
|
||||
@@ -1477,6 +1454,31 @@ def _repair_gamescope_game_window_focus(mapper: _X11, window: int) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _gamescope_host_focus_is_allowed(
|
||||
*,
|
||||
environment: Mapping[str, str] | None = None,
|
||||
now: float | None = None,
|
||||
) -> bool:
|
||||
"""Honor the outer X11 window's current focus without trusting stale state."""
|
||||
|
||||
env = os.environ if environment is None else environment
|
||||
state_path = env.get(GAMESCOPE_HOST_FOCUS_STATE_ENV, "").strip()
|
||||
if not state_path:
|
||||
# Direct invocations and native Wayland Gamescope have no X11 host-state
|
||||
# bridge. Retain their existing nested focus behavior.
|
||||
return True
|
||||
try:
|
||||
fields = Path(state_path).read_text(encoding="ascii").split()
|
||||
if len(fields) != 2 or fields[0] != "1":
|
||||
return False
|
||||
published_at = float(fields[1])
|
||||
except (OSError, UnicodeError, ValueError):
|
||||
return False
|
||||
observed_at = time.monotonic() if now is None else now
|
||||
age = observed_at - published_at
|
||||
return 0.0 <= age <= GAMESCOPE_HOST_FOCUS_STATE_MAX_AGE_SECONDS
|
||||
|
||||
|
||||
def _resolve_gamescope_game_window(
|
||||
mapper: _X11,
|
||||
window: int,
|
||||
@@ -1851,7 +1853,8 @@ def run_game_window(
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
_focus_game_window(mapper, game_window)
|
||||
if _gamescope_host_focus_is_allowed():
|
||||
_focus_game_window(mapper, game_window)
|
||||
print(
|
||||
"Game presentation is isolated; Gamescope handles "
|
||||
"aspect-preserving outer-window resizing.",
|
||||
@@ -1944,13 +1947,15 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
if command and command[0] == "--":
|
||||
command = command[1:]
|
||||
try:
|
||||
return run_game_window(
|
||||
command,
|
||||
arguments.window_title,
|
||||
borderless=arguments.borderless,
|
||||
allow_window_resizing=arguments.allow_window_resizing,
|
||||
gamescope_scaling=arguments.gamescope_scaling,
|
||||
suppress_wm_ping=arguments.suppress_wm_ping,
|
||||
return _process_exit_status(
|
||||
run_game_window(
|
||||
command,
|
||||
arguments.window_title,
|
||||
borderless=arguments.borderless,
|
||||
allow_window_resizing=arguments.allow_window_resizing,
|
||||
gamescope_scaling=arguments.gamescope_scaling,
|
||||
suppress_wm_ping=arguments.suppress_wm_ping,
|
||||
)
|
||||
)
|
||||
except (GameboxRuntimeError, OSError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user