Files
CCMA/src/ccma/ui/monitors.py
T

118 lines
3.7 KiB
Python

from __future__ import annotations
import re
import tkinter as tk
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class MonitorBounds:
x: int
y: int
width: int
height: int
primary: bool = False
@property
def center(self) -> tuple[int, int]:
return self.x + self.width // 2, self.y + self.height // 2
def as_tuple(self) -> tuple[int, int, int, int]:
return self.x, self.y, self.width, self.height
def available_monitors(root: tk.Misc | None = None) -> list[MonitorBounds]:
try:
from screeninfo import get_monitors
monitors = [
MonitorBounds(
int(monitor.x),
int(monitor.y),
int(monitor.width),
int(monitor.height),
bool(getattr(monitor, "is_primary", False)),
)
for monitor in get_monitors()
]
if monitors:
return monitors
except Exception:
pass
if root is None:
return []
return [
MonitorBounds(
int(root.winfo_vrootx()),
int(root.winfo_vrooty()),
int(root.winfo_vrootwidth()),
int(root.winfo_vrootheight()),
True,
)
]
def preferred_monitor(
root: tk.Misc,
saved_bounds: tuple[int, int, int, int] | None = None,
) -> MonitorBounds:
monitors = available_monitors(root)
if saved_bounds:
for monitor in monitors:
if monitor.as_tuple() == saved_bounds:
return monitor
return next((monitor for monitor in monitors if monitor.primary), monitors[0])
def monitor_for_geometry(root: tk.Misc, geometry: str) -> MonitorBounds:
monitors = available_monitors(root)
parsed = parse_geometry(geometry)
if not parsed:
return next((monitor for monitor in monitors if monitor.primary), monitors[0])
width, height, x, y = parsed
center_x, center_y = x + width // 2, y + height // 2
containing = [
monitor
for monitor in monitors
if monitor.x <= center_x < monitor.x + monitor.width
and monitor.y <= center_y < monitor.y + monitor.height
]
if containing:
return containing[0]
return min(monitors, key=lambda monitor: _distance_squared(monitor.center, (center_x, center_y)))
def centered_geometry(width: int, height: int, monitor: MonitorBounds) -> str:
fitted_width = min(width, max(640, monitor.width - 80))
fitted_height = min(height, max(480, monitor.height - 80))
x = monitor.x + (monitor.width - fitted_width) // 2
y = monitor.y + (monitor.height - fitted_height) // 2
return format_geometry(fitted_width, fitted_height, x, y)
def ensure_visible_geometry(geometry: str, monitor: MonitorBounds) -> str:
parsed = parse_geometry(geometry)
if not parsed:
return centered_geometry(1500, 860, monitor)
width, height, x, y = parsed
width = min(width, max(640, monitor.width - 40))
height = min(height, max(480, monitor.height - 40))
x = min(max(x, monitor.x), monitor.x + monitor.width - width)
y = min(max(y, monitor.y), monitor.y + monitor.height - height)
return format_geometry(width, height, x, y)
def parse_geometry(geometry: str) -> tuple[int, int, int, int] | None:
match = re.fullmatch(r"(\d+)x(\d+)([+-]\d+)([+-]\d+)", geometry.strip())
if not match:
return None
return tuple(int(value) for value in match.groups()) # type: ignore[return-value]
def format_geometry(width: int, height: int, x: int, y: int) -> str:
return f"{width}x{height}{x:+d}{y:+d}"
def _distance_squared(left: tuple[int, int], right: tuple[int, int]) -> int:
return (left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2