mirror of
https://github.com/lutris/lutris.git
synced 2026-06-17 10:19:58 -04:00
Promote DisplayManager to an ABC and lazy-init the singleton
Convert the unused stub DisplayManager class in lutris/util/display.py into an abstract base class declaring the five-method interface, and make MutterDisplayManager and LegacyDisplayManager inherit from it. This collapses the get_display_manager() return type from a three-way union to just DisplayManager. Replace the eager DISPLAY_MANAGER module-level singleton with a @cache_single-decorated get_display_manager() function. This breaks a latent import cycle (xrandr.py and displayconfig.py now top-level import the ABC; display.py no longer constructs the singleton at import time) and defers backend selection until first use. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -28,10 +28,10 @@ from lutris.runners import import_runner, is_valid_runner_name
|
||||
from lutris.runners.runner import Runner, kill_processes
|
||||
from lutris.util import busy, discord, extract, jobs, linux, strings, system, xdgshortcuts
|
||||
from lutris.util.display import (
|
||||
DISPLAY_MANAGER,
|
||||
SCREEN_SAVER_INHIBITOR,
|
||||
disable_compositing,
|
||||
enable_compositing,
|
||||
get_display_manager,
|
||||
is_display_x11,
|
||||
)
|
||||
from lutris.util.graphics.xrandr import turn_off_except
|
||||
@@ -560,7 +560,7 @@ class Game:
|
||||
return True
|
||||
|
||||
def restrict_to_display(self, display: str) -> bool:
|
||||
outputs = DISPLAY_MANAGER.get_config()
|
||||
outputs = get_display_manager().get_config()
|
||||
if display == "primary":
|
||||
display = ""
|
||||
for output in outputs:
|
||||
@@ -746,7 +746,7 @@ class Game:
|
||||
system.set_keyboard_layout("us")
|
||||
|
||||
# Display control
|
||||
self.original_outputs = DISPLAY_MANAGER.get_config()
|
||||
self.original_outputs = get_display_manager().get_config()
|
||||
|
||||
if self.runner.system_config.get("disable_compositor"):
|
||||
self.set_desktop_compositing(False)
|
||||
@@ -759,7 +759,7 @@ class Game:
|
||||
|
||||
resolution: str = self.runner.system_config.get("resolution")
|
||||
if resolution != "off":
|
||||
DISPLAY_MANAGER.set_resolution(resolution)
|
||||
get_display_manager().set_resolution(resolution)
|
||||
time.sleep(3)
|
||||
self.resolution_changed = True
|
||||
|
||||
@@ -1094,7 +1094,7 @@ class Game:
|
||||
|
||||
if self.resolution_changed or self.runner.system_config.get("reset_desktop"):
|
||||
if self.original_outputs:
|
||||
DISPLAY_MANAGER.set_resolution(self.original_outputs)
|
||||
get_display_manager().set_resolution(self.original_outputs)
|
||||
|
||||
if self.compositor_disabled:
|
||||
self.set_desktop_compositing(True)
|
||||
|
||||
@@ -17,7 +17,7 @@ from lutris.installer.installer import LutrisInstaller
|
||||
from lutris.runners import NonInstallableRunnerError, RunnerInstallationError, steam, wine
|
||||
from lutris.services.lutris import download_lutris_media
|
||||
from lutris.util import system
|
||||
from lutris.util.display import DISPLAY_MANAGER
|
||||
from lutris.util.display import get_display_manager
|
||||
from lutris.util.jobs import AsyncCall
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.strings import unpack_dependencies
|
||||
@@ -82,7 +82,7 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
|
||||
self.user_inputs = []
|
||||
self.current_command = 0 # Current installer command when iterating through them
|
||||
self.runners_to_install = []
|
||||
self.current_resolution = DISPLAY_MANAGER.get_current_resolution()
|
||||
self.current_resolution = get_display_manager().get_current_resolution()
|
||||
self.installer = LutrisInstaller(installer, self, service=self.service, appid=_appid)
|
||||
|
||||
if not self.installer.script:
|
||||
|
||||
@@ -10,7 +10,9 @@ from lutris.util import display, extract, system
|
||||
|
||||
def get_resolutions():
|
||||
try:
|
||||
screen_resolutions = [(resolution, resolution) for resolution in display.DISPLAY_MANAGER.get_resolutions()]
|
||||
screen_resolutions = [
|
||||
(resolution, resolution) for resolution in display.get_display_manager().get_resolutions()
|
||||
]
|
||||
except OSError:
|
||||
screen_resolutions = []
|
||||
screen_resolutions.insert(0, (_("Desktop resolution"), "desktop"))
|
||||
@@ -123,7 +125,7 @@ class atari800(Runner):
|
||||
resolution = self.runner_config.get("resolution")
|
||||
if resolution:
|
||||
if resolution == "desktop":
|
||||
width, height = display.DISPLAY_MANAGER.get_current_resolution()
|
||||
width, height = display.get_display_manager().get_current_resolution()
|
||||
else:
|
||||
width, height = resolution.split("x")
|
||||
arguments += ["-fs-width", "%s" % width, "-fs-height", "%s" % height]
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
from lutris import settings
|
||||
from lutris.runners.runner import Runner
|
||||
from lutris.util import system
|
||||
from lutris.util.display import DISPLAY_MANAGER
|
||||
from lutris.util.display import get_display_manager
|
||||
|
||||
AMIGAS: dict[str, Any] = {
|
||||
"A500": {
|
||||
@@ -426,7 +426,7 @@ class fsuae(Runner):
|
||||
params.append(param % option_value)
|
||||
|
||||
if self.runner_config.get("gfx_fullscreen_amiga"):
|
||||
width = DISPLAY_MANAGER.get_current_resolution()[0]
|
||||
width = get_display_manager().get_current_resolution()[0]
|
||||
params.append("--fullscreen")
|
||||
# params.append("--fullscreen_mode=fullscreen-window")
|
||||
params.append("--fullscreen_mode=fullscreen")
|
||||
|
||||
@@ -7,7 +7,7 @@ from lutris.exceptions import MissingGameExecutableError
|
||||
# Lutris Modules
|
||||
from lutris.runners.runner import Runner
|
||||
from lutris.util import system
|
||||
from lutris.util.display import DISPLAY_MANAGER
|
||||
from lutris.util.display import get_display_manager
|
||||
from lutris.util.joypad import get_controller_mappings
|
||||
from lutris.util.log import logger
|
||||
|
||||
@@ -464,7 +464,7 @@ class mednafen(Runner):
|
||||
scaler = self.runner_config.get("scaler") or DEFAULT_MEDNAFEN_SCALER
|
||||
sound_device = self.runner_config.get("sound_device")
|
||||
|
||||
xres, yres = DISPLAY_MANAGER.get_current_resolution()
|
||||
xres, yres = get_display_manager().get_current_resolution()
|
||||
options = [
|
||||
"-fs",
|
||||
fullscreen,
|
||||
|
||||
@@ -37,7 +37,7 @@ from lutris.runners.commands.wine import ( # noqa: F401 pylint: disable=unused-
|
||||
)
|
||||
from lutris.runners.runner import Runner
|
||||
from lutris.util import system
|
||||
from lutris.util.display import DISPLAY_MANAGER, get_default_dpi, is_display_x11
|
||||
from lutris.util.display import get_default_dpi, get_display_manager, is_display_x11
|
||||
from lutris.util.graphics import drivers, vkquery
|
||||
from lutris.util.linux import LINUX_SYSTEM
|
||||
from lutris.util.log import logger
|
||||
@@ -538,7 +538,7 @@ class wine(Runner):
|
||||
"visible": _is_pre_proton,
|
||||
"conditional_on": "Desktop",
|
||||
"advanced": True,
|
||||
"choices": DISPLAY_MANAGER.get_resolutions,
|
||||
"choices": get_display_manager().get_resolutions,
|
||||
"help": _("The size of the virtual desktop in pixels."),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -101,7 +101,7 @@ class zdoom(Runner):
|
||||
resolution = self.runner_config.get("resolution")
|
||||
if resolution:
|
||||
if resolution == "desktop":
|
||||
width, height = display.DISPLAY_MANAGER.get_current_resolution()
|
||||
width, height = display.get_display_manager().get_current_resolution()
|
||||
else:
|
||||
width, height = resolution.split("x")
|
||||
command.append("-width")
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from lutris import runners
|
||||
from lutris.util import linux, system
|
||||
from lutris.util.display import DISPLAY_MANAGER, SCREEN_SAVER_INHIBITOR, is_compositing_enabled, is_display_x11
|
||||
from lutris.util.display import SCREEN_SAVER_INHIBITOR, get_display_manager, is_compositing_enabled, is_display_x11
|
||||
from lutris.util.graphics.gpu import get_gpus
|
||||
from lutris.util.sniper import get_sniper_run_command
|
||||
|
||||
@@ -20,7 +20,7 @@ def get_resolution_choices() -> list[tuple[str, str]]:
|
||||
"""Return list of available resolutions as label, value tuples
|
||||
suitable for inclusion in drop-downs.
|
||||
"""
|
||||
resolutions = DISPLAY_MANAGER.get_resolutions()
|
||||
resolutions = get_display_manager().get_resolutions()
|
||||
resolution_choices = list(zip(resolutions, resolutions))
|
||||
resolution_choices.insert(0, (_("Keep current"), "off"))
|
||||
return resolution_choices
|
||||
@@ -61,7 +61,7 @@ def get_gpu_list() -> list[tuple[str, str]]:
|
||||
|
||||
def get_output_choices() -> list[tuple[str, str]]:
|
||||
"""Return list of outputs for drop-downs"""
|
||||
displays = DISPLAY_MANAGER.get_display_names()
|
||||
displays = get_display_manager().get_display_names()
|
||||
output_choices = list(zip(displays, displays))
|
||||
output_choices.insert(0, (_("Off"), "off"))
|
||||
output_choices.insert(1, (_("Primary"), "primary"))
|
||||
@@ -73,7 +73,7 @@ def get_output_list() -> list[tuple[str, str]]:
|
||||
This is used to indicate to SDL 1.2 which monitor to use.
|
||||
"""
|
||||
choices = [(_("Off"), "off")]
|
||||
displays = DISPLAY_MANAGER.get_display_names()
|
||||
displays = get_display_manager().get_display_names()
|
||||
for index, output in enumerate(displays):
|
||||
# Display name can't be used because they might not be in the right order
|
||||
# Using DISPLAYS to get the number of connected monitors
|
||||
@@ -293,7 +293,7 @@ system_options: list[dict[str, Any]] = [ # pylint: disable=invalid-name
|
||||
"option": "gamescope_output_res",
|
||||
"type": "choice_with_entry",
|
||||
"label": _("Output Resolution"),
|
||||
"choices": DISPLAY_MANAGER.get_resolutions,
|
||||
"choices": get_display_manager().get_resolutions,
|
||||
"advanced": True,
|
||||
"conditional_on": "gamescope",
|
||||
"condition": system.can_find_executable("gamescope"),
|
||||
@@ -309,7 +309,7 @@ system_options: list[dict[str, Any]] = [ # pylint: disable=invalid-name
|
||||
"option": "gamescope_game_res",
|
||||
"type": "choice_with_entry",
|
||||
"label": _("Game Resolution"),
|
||||
"choices": DISPLAY_MANAGER.get_resolutions,
|
||||
"choices": get_display_manager().get_resolutions,
|
||||
"conditional_on": "gamescope",
|
||||
"condition": system.can_find_executable("gamescope"),
|
||||
"help": _("Set the maximum resolution used by the game.\n\n<b>Custom Resolutions:</b> (width)x(height)"),
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# isort:skip_file
|
||||
import abc
|
||||
import enum
|
||||
import os
|
||||
import subprocess
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
# GnomeDesktop 3.0 requires GTK 3 and cannot coexist with GTK 4.
|
||||
# The GnomeDesktop-based DisplayManager is no longer available;
|
||||
# we fall through to MutterDisplayManager or LegacyDisplayManager instead.
|
||||
# GnomeDesktop 3.0 requires GTK 3 and cannot coexist with GTK 4. The constants are
|
||||
# kept (always falsy) for callers that branched on them before the GTK 4 port.
|
||||
LIB_GNOME_DESKTOP_AVAILABLE = False
|
||||
GnomeDesktop = None
|
||||
|
||||
@@ -26,8 +25,6 @@ from gi.repository import Gdk, GLib, Gio, Gtk
|
||||
|
||||
from lutris.util import cache_single
|
||||
from lutris.settings import DEFAULT_RESOLUTION_HEIGHT, DEFAULT_RESOLUTION_WIDTH
|
||||
from lutris.util.graphics.displayconfig import MutterDisplayManager
|
||||
from lutris.util.graphics.xrandr import LegacyDisplayManager, change_resolution, get_outputs, Output
|
||||
from lutris.util.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -57,51 +54,76 @@ def is_display_x11() -> bool:
|
||||
return "x11" in type(display).__name__.casefold()
|
||||
|
||||
|
||||
class DisplayManager:
|
||||
"""Get display and resolution using XRandR (GnomeDesktop no longer available with GTK 4)"""
|
||||
class DisplayManager(abc.ABC):
|
||||
"""Abstract interface implemented by the supported display backends:
|
||||
MutterDisplayManager (DBus, Wayland-aware) and LegacyDisplayManager (xrandr, X11).
|
||||
|
||||
@staticmethod
|
||||
def get_display_names() -> list[str]:
|
||||
"""Return names of connected displays"""
|
||||
return [output.name for output in get_outputs()]
|
||||
Default implementations of get_display_names and get_current_resolution use
|
||||
GDK; the concrete backends override both to stay self-consistent with the
|
||||
same data source they use for the abstract methods below."""
|
||||
|
||||
@staticmethod
|
||||
def get_resolutions() -> list[str]:
|
||||
"""Return available resolutions"""
|
||||
resolutions = []
|
||||
for output in get_outputs():
|
||||
resolutions.append(output.mode)
|
||||
if not resolutions:
|
||||
logger.error("Failed to generate resolution list")
|
||||
return ["%sx%s" % (DEFAULT_RESOLUTION_WIDTH, DEFAULT_RESOLUTION_HEIGHT)]
|
||||
return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)
|
||||
def get_display_names(self) -> list[str]:
|
||||
"""Return connector names of connected displays."""
|
||||
display = Gdk.Display.get_default()
|
||||
if not display:
|
||||
return []
|
||||
monitors = display.get_monitors()
|
||||
names: list[str] = []
|
||||
for i in range(monitors.get_n_items()):
|
||||
monitor = monitors.get_item(i)
|
||||
if monitor and (connector := monitor.get_connector()):
|
||||
names.append(connector)
|
||||
return names
|
||||
|
||||
@staticmethod
|
||||
def get_current_resolution() -> tuple[str, str]:
|
||||
"""Return the current resolution for the primary display"""
|
||||
outputs = get_outputs()
|
||||
primary = next((o for o in outputs if o.primary), None) or (outputs[0] if outputs else None)
|
||||
if primary and primary.mode:
|
||||
parts = primary.mode.split("x")
|
||||
if len(parts) == 2:
|
||||
return parts[0], parts[1]
|
||||
@abc.abstractmethod
|
||||
def get_resolutions(self) -> list[str]:
|
||||
"""Return available resolutions, formatted "WIDTHxHEIGHT"."""
|
||||
|
||||
def get_current_resolution(self) -> tuple[str, str]:
|
||||
"""Return the current resolution (width, height) of the primary display.
|
||||
|
||||
BUG: under fractional desktop scaling this reports the wrong number.
|
||||
GDK 4.10 exposes only the integer scale_factor (Gdk.Monitor.get_scale,
|
||||
a float, was added in GDK 4.14). For 125% scaling on a 1080p panel,
|
||||
get_geometry returns a 1536x864 logical size and get_scale_factor
|
||||
returns 1, so width*scale_factor = 1536x864 instead of the physical
|
||||
1920x1080. KDE/Wayland exposes the same bug from the other side via
|
||||
XWayland's upscaled virtual canvas. Both concrete subclasses override
|
||||
this with backend-specific logic that recovers the physical mode
|
||||
(xrandr uses XWayland's EDID preferred_mode; Mutter pulls the current
|
||||
mode list from DBus)."""
|
||||
display = Gdk.Display.get_default()
|
||||
if display:
|
||||
monitors = display.get_monitors()
|
||||
if monitors.get_n_items() > 0:
|
||||
monitor = monitors.get_item(0)
|
||||
if monitor:
|
||||
geom = monitor.get_geometry()
|
||||
scale = monitor.get_scale_factor()
|
||||
return str(geom.width * scale), str(geom.height * scale)
|
||||
return str(DEFAULT_RESOLUTION_WIDTH), str(DEFAULT_RESOLUTION_HEIGHT)
|
||||
|
||||
@staticmethod
|
||||
def set_resolution(resolution: str | Iterable[Output]) -> None:
|
||||
"""Set the resolution of one or more displays."""
|
||||
return change_resolution(resolution)
|
||||
@abc.abstractmethod
|
||||
def set_resolution(self, resolution: Any) -> None:
|
||||
"""Apply a resolution string to the primary display, or a backend-specific
|
||||
config list (as returned by get_config) to all displays."""
|
||||
|
||||
@staticmethod
|
||||
def get_config() -> list[Output]:
|
||||
"""Return the current display configuration."""
|
||||
return get_outputs()
|
||||
@abc.abstractmethod
|
||||
def get_config(self) -> list[Any]:
|
||||
"""Return the current display configuration; can be passed back to set_resolution."""
|
||||
|
||||
|
||||
def get_display_manager() -> MutterDisplayManager | DisplayManager | LegacyDisplayManager:
|
||||
"""Return the appropriate display manager instance.
|
||||
Defaults to Mutter if available. This is the only one to support Wayland.
|
||||
@cache_single
|
||||
def get_display_manager() -> DisplayManager:
|
||||
"""Return the appropriate display manager instance, lazily constructed and
|
||||
cached. Defaults to Mutter if available — this is the only backend that
|
||||
supports Wayland.
|
||||
"""
|
||||
# Imported here to avoid a circular import: the concrete classes inherit from
|
||||
# DisplayManager defined above, so they import this module at their top level.
|
||||
from lutris.util.graphics.displayconfig import MutterDisplayManager
|
||||
from lutris.util.graphics.xrandr import LegacyDisplayManager
|
||||
|
||||
if DBUS_AVAILABLE:
|
||||
try:
|
||||
return MutterDisplayManager()
|
||||
@@ -115,9 +137,6 @@ def get_display_manager() -> MutterDisplayManager | DisplayManager | LegacyDispl
|
||||
return LegacyDisplayManager()
|
||||
|
||||
|
||||
DISPLAY_MANAGER = get_display_manager()
|
||||
|
||||
|
||||
class DesktopEnvironment(enum.Enum):
|
||||
"""Enum of desktop environments."""
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
import dbus
|
||||
|
||||
from lutris.settings import DEFAULT_RESOLUTION_HEIGHT, DEFAULT_RESOLUTION_WIDTH
|
||||
from lutris.util.display import DisplayManager
|
||||
from lutris.util.log import logger
|
||||
|
||||
DisplayConfig = namedtuple("DisplayConfig", ("monitors", "name", "position", "transform", "primary", "scale"))
|
||||
@@ -621,7 +622,7 @@ class MutterDisplayConfig:
|
||||
self.interface.ApplyMonitorsConfig(self.current_state.serial, self.TEMPORARY_METHOD, monitors_config, {})
|
||||
|
||||
|
||||
class MutterDisplayManager:
|
||||
class MutterDisplayManager(DisplayManager):
|
||||
"""Manage displays using the DBus Mutter interface"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -7,6 +7,7 @@ from collections.abc import Iterable
|
||||
|
||||
from lutris.settings import DEFAULT_RESOLUTION_HEIGHT, DEFAULT_RESOLUTION_WIDTH
|
||||
from lutris.util import cache_single
|
||||
from lutris.util.display import DisplayManager
|
||||
from lutris.util.linux import LINUX_SYSTEM
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.system import read_process_output
|
||||
@@ -201,23 +202,20 @@ def change_resolution(resolution: str | Iterable[Output]) -> None:
|
||||
xrandr.communicate()
|
||||
|
||||
|
||||
class LegacyDisplayManager: # pylint: disable=too-few-public-methods
|
||||
class LegacyDisplayManager(DisplayManager):
|
||||
"""Legacy XrandR based display manager.
|
||||
Does not work on Wayland.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_display_names() -> list[str]:
|
||||
def get_display_names(self) -> list[str]:
|
||||
"""Return output names from XrandR"""
|
||||
return [output.name for output in get_outputs()]
|
||||
|
||||
@staticmethod
|
||||
def get_resolutions() -> list[str]:
|
||||
def get_resolutions(self) -> list[str]:
|
||||
"""Return available resolutions"""
|
||||
return get_resolutions()
|
||||
|
||||
@staticmethod
|
||||
def get_current_resolution() -> tuple[str, str]:
|
||||
def get_current_resolution(self) -> tuple[str, str]:
|
||||
"""Return the current resolution for the desktop"""
|
||||
outputs = get_outputs()
|
||||
if not outputs:
|
||||
@@ -235,12 +233,10 @@ class LegacyDisplayManager: # pylint: disable=too-few-public-methods
|
||||
mode = primary.preferred_mode
|
||||
return tuple(mode.split("x"))
|
||||
|
||||
@staticmethod
|
||||
def set_resolution(resolution: str | Iterable[Output]) -> None:
|
||||
def set_resolution(self, resolution: str | Iterable[Output]) -> None:
|
||||
"""Change the current resolution"""
|
||||
change_resolution(resolution)
|
||||
|
||||
@staticmethod
|
||||
def get_config() -> list[Output]:
|
||||
def get_config(self) -> list[Output]:
|
||||
"""Return the current display configuration"""
|
||||
return get_outputs()
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
from lutris.settings import get_lutris_directory_settings, set_lutris_directory_settings
|
||||
from lutris.util import joypad, system
|
||||
from lutris.util.display import DISPLAY_MANAGER
|
||||
from lutris.util.display import get_display_manager
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.wine.registry import WineRegistry
|
||||
from lutris.util.xdgshortcuts import get_xdg_entry
|
||||
@@ -258,7 +258,7 @@ class WinePrefixManager:
|
||||
path = self.hkcu_prefix + "/Software/Wine/Explorer"
|
||||
if enabled:
|
||||
self.set_registry_key(path, "Desktop", "WineDesktop")
|
||||
default_resolution = "x".join(DISPLAY_MANAGER.get_current_resolution())
|
||||
default_resolution = "x".join(get_display_manager().get_current_resolution())
|
||||
logger.debug(
|
||||
"Enabling wine virtual desktop with default resolution of %s",
|
||||
default_resolution,
|
||||
|
||||
Reference in New Issue
Block a user