Window management
This commit is contained in:
@@ -71,6 +71,7 @@ public static class LinuxMauiAppBuilderExtensions
|
|||||||
builder.Services.TryAddSingleton<FolderPickerService>();
|
builder.Services.TryAddSingleton<FolderPickerService>();
|
||||||
builder.Services.TryAddSingleton<NotificationService>();
|
builder.Services.TryAddSingleton<NotificationService>();
|
||||||
builder.Services.TryAddSingleton<SystemTrayService>();
|
builder.Services.TryAddSingleton<SystemTrayService>();
|
||||||
|
builder.Services.TryAddSingleton(_ => MonitorService.Instance);
|
||||||
|
|
||||||
// Register GTK host service
|
// Register GTK host service
|
||||||
builder.Services.TryAddSingleton(_ => GtkHostService.Instance);
|
builder.Services.TryAddSingleton(_ => GtkHostService.Instance);
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ internal static partial class X11
|
|||||||
|
|
||||||
public const int ZPixmap = 2;
|
public const int ZPixmap = 2;
|
||||||
|
|
||||||
|
// Event types
|
||||||
|
public const int ClientMessage = 33;
|
||||||
|
|
||||||
|
// Event masks for XSendEvent
|
||||||
|
public const long SubstructureRedirectMask = 1L << 20;
|
||||||
|
public const long SubstructureNotifyMask = 1L << 19;
|
||||||
|
|
||||||
[LibraryImport(LibX11)]
|
[LibraryImport(LibX11)]
|
||||||
public static partial IntPtr XOpenDisplay(IntPtr displayName);
|
public static partial IntPtr XOpenDisplay(IntPtr displayName);
|
||||||
|
|
||||||
@@ -72,6 +79,9 @@ internal static partial class X11
|
|||||||
[LibraryImport(LibX11)]
|
[LibraryImport(LibX11)]
|
||||||
public static partial int XResizeWindow(IntPtr display, IntPtr window, uint width, uint height);
|
public static partial int XResizeWindow(IntPtr display, IntPtr window, uint width, uint height);
|
||||||
|
|
||||||
|
[LibraryImport(LibX11)]
|
||||||
|
public static partial int XIconifyWindow(IntPtr display, IntPtr window, int screen);
|
||||||
|
|
||||||
[LibraryImport(LibX11)]
|
[LibraryImport(LibX11)]
|
||||||
public static partial int XMoveResizeWindow(IntPtr display, IntPtr window, int x, int y, uint width, uint height);
|
public static partial int XMoveResizeWindow(IntPtr display, IntPtr window, int x, int y, uint width, uint height);
|
||||||
|
|
||||||
|
|||||||
139
Interop/XRandR.cs
Normal file
139
Interop/XRandR.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Platform.Linux.Interop;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// XRandR (X Resize and Rotate) extension interop for multi-monitor support.
|
||||||
|
/// </summary>
|
||||||
|
internal static partial class XRandR
|
||||||
|
{
|
||||||
|
private const string LibXrandr = "libXrandr.so.2";
|
||||||
|
|
||||||
|
// RROutput and RRCrtc are XIDs (unsigned long)
|
||||||
|
// RRMode is also an XID
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial IntPtr XRRGetScreenResources(IntPtr display, IntPtr window);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial IntPtr XRRGetScreenResourcesCurrent(IntPtr display, IntPtr window);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial void XRRFreeScreenResources(IntPtr resources);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial IntPtr XRRGetOutputInfo(IntPtr display, IntPtr resources, ulong output);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial void XRRFreeOutputInfo(IntPtr outputInfo);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial IntPtr XRRGetCrtcInfo(IntPtr display, IntPtr resources, ulong crtc);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial void XRRFreeCrtcInfo(IntPtr crtcInfo);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial int XRRQueryExtension(IntPtr display, out int eventBase, out int errorBase);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial int XRRQueryVersion(IntPtr display, out int major, out int minor);
|
||||||
|
|
||||||
|
[LibraryImport(LibXrandr)]
|
||||||
|
public static partial void XRRSelectInput(IntPtr display, IntPtr window, int mask);
|
||||||
|
|
||||||
|
// RRNotify mask values
|
||||||
|
public const int RRScreenChangeNotifyMask = 1 << 0;
|
||||||
|
public const int RRCrtcChangeNotifyMask = 1 << 1;
|
||||||
|
public const int RROutputChangeNotifyMask = 1 << 2;
|
||||||
|
public const int RROutputPropertyNotifyMask = 1 << 3;
|
||||||
|
|
||||||
|
// Connection status
|
||||||
|
public const int RR_Connected = 0;
|
||||||
|
public const int RR_Disconnected = 1;
|
||||||
|
public const int RR_UnknownConnection = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// XRRScreenResources structure layout.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct XRRScreenResources
|
||||||
|
{
|
||||||
|
public ulong Timestamp;
|
||||||
|
public ulong ConfigTimestamp;
|
||||||
|
public int NCrtc;
|
||||||
|
public IntPtr Crtcs; // RRCrtc* (array of ulongs)
|
||||||
|
public int NOutput;
|
||||||
|
public IntPtr Outputs; // RROutput* (array of ulongs)
|
||||||
|
public int NMode;
|
||||||
|
public IntPtr Modes; // XRRModeInfo*
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// XRROutputInfo structure layout.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct XRROutputInfo
|
||||||
|
{
|
||||||
|
public ulong Timestamp;
|
||||||
|
public ulong Crtc; // RRCrtc - current CRTC (0 if not connected)
|
||||||
|
public IntPtr Name; // char*
|
||||||
|
public int NameLen;
|
||||||
|
public ulong MmWidth; // Physical width in mm
|
||||||
|
public ulong MmHeight; // Physical height in mm
|
||||||
|
public ushort Connection; // RRConnection status
|
||||||
|
public ushort SubpixelOrder;
|
||||||
|
public int NCrtc;
|
||||||
|
public IntPtr Crtcs; // RRCrtc* - possible CRTCs
|
||||||
|
public int NClone;
|
||||||
|
public IntPtr Clones; // RROutput*
|
||||||
|
public int NMode;
|
||||||
|
public int NPreferred;
|
||||||
|
public IntPtr Modes; // RRMode*
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// XRRCrtcInfo structure layout.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct XRRCrtcInfo
|
||||||
|
{
|
||||||
|
public ulong Timestamp;
|
||||||
|
public int X;
|
||||||
|
public int Y;
|
||||||
|
public uint Width;
|
||||||
|
public uint Height;
|
||||||
|
public ulong Mode; // RRMode - current mode
|
||||||
|
public ushort Rotation;
|
||||||
|
public int NOutput;
|
||||||
|
public IntPtr Outputs; // RROutput*
|
||||||
|
public ushort Rotations; // Possible rotations
|
||||||
|
public int NPossible;
|
||||||
|
public IntPtr Possible; // RROutput*
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// XRRModeInfo structure layout.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct XRRModeInfo
|
||||||
|
{
|
||||||
|
public ulong Id; // RRMode
|
||||||
|
public uint Width;
|
||||||
|
public uint Height;
|
||||||
|
public ulong DotClock;
|
||||||
|
public uint HSyncStart;
|
||||||
|
public uint HSyncEnd;
|
||||||
|
public uint HTotal;
|
||||||
|
public uint HSkew;
|
||||||
|
public uint VSyncStart;
|
||||||
|
public uint VSyncEnd;
|
||||||
|
public uint VTotal;
|
||||||
|
public IntPtr Name; // char*
|
||||||
|
public uint NameLength;
|
||||||
|
public ulong ModeFlags;
|
||||||
|
}
|
||||||
@@ -51,6 +51,32 @@ public class DeviceDisplayService : IDeviceDisplay
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Try to use MonitorService for accurate XRandR-based info
|
||||||
|
var primaryMonitor = MonitorService.Instance.PrimaryMonitor;
|
||||||
|
if (primaryMonitor != null)
|
||||||
|
{
|
||||||
|
double scaleFactor = GetScaleFactor();
|
||||||
|
// If scale factor not set via env, use monitor's DPI-based scale
|
||||||
|
if (scaleFactor == 1.0 && primaryMonitor.ScaleFactor > 1.0)
|
||||||
|
{
|
||||||
|
scaleFactor = Math.Round(primaryMonitor.ScaleFactor * 4) / 4; // Round to nearest 0.25
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayOrientation orientation = (primaryMonitor.Width <= primaryMonitor.Height)
|
||||||
|
? DisplayOrientation.Portrait
|
||||||
|
: DisplayOrientation.Landscape;
|
||||||
|
|
||||||
|
_mainDisplayInfo = new DisplayInfo(
|
||||||
|
primaryMonitor.Width,
|
||||||
|
primaryMonitor.Height,
|
||||||
|
scaleFactor,
|
||||||
|
orientation,
|
||||||
|
DisplayRotation.Rotation0,
|
||||||
|
(float)primaryMonitor.RefreshRate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to GDK
|
||||||
IntPtr screen = GdkNative.gdk_screen_get_default();
|
IntPtr screen = GdkNative.gdk_screen_get_default();
|
||||||
if (screen != IntPtr.Zero)
|
if (screen != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
|
|||||||
104
Services/MonitorInfo.cs
Normal file
104
Services/MonitorInfo.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information about a display monitor.
|
||||||
|
/// </summary>
|
||||||
|
public record MonitorInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the unique identifier for this monitor (XRandR output ID).
|
||||||
|
/// </summary>
|
||||||
|
public ulong Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the monitor name (e.g., "HDMI-1", "DP-2", "eDP-1").
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this is the primary monitor.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPrimary { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the X position of the monitor in the virtual desktop.
|
||||||
|
/// </summary>
|
||||||
|
public int X { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Y position of the monitor in the virtual desktop.
|
||||||
|
/// </summary>
|
||||||
|
public int Y { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the width in pixels.
|
||||||
|
/// </summary>
|
||||||
|
public int Width { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the height in pixels.
|
||||||
|
/// </summary>
|
||||||
|
public int Height { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the physical width in millimeters.
|
||||||
|
/// </summary>
|
||||||
|
public int PhysicalWidthMm { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the physical height in millimeters.
|
||||||
|
/// </summary>
|
||||||
|
public int PhysicalHeightMm { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the refresh rate in Hz.
|
||||||
|
/// </summary>
|
||||||
|
public double RefreshRate { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the horizontal DPI.
|
||||||
|
/// </summary>
|
||||||
|
public double DpiX
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (PhysicalWidthMm <= 0) return 96.0;
|
||||||
|
return Width / (PhysicalWidthMm / 25.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the vertical DPI.
|
||||||
|
/// </summary>
|
||||||
|
public double DpiY
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (PhysicalHeightMm <= 0) return 96.0;
|
||||||
|
return Height / (PhysicalHeightMm / 25.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the average DPI.
|
||||||
|
/// </summary>
|
||||||
|
public double Dpi => (DpiX + DpiY) / 2.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the scale factor based on DPI (1.0 = 96 DPI).
|
||||||
|
/// </summary>
|
||||||
|
public double ScaleFactor => Dpi / 96.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the bounds rectangle.
|
||||||
|
/// </summary>
|
||||||
|
public (int X, int Y, int Width, int Height) Bounds => (X, Y, Width, Height);
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Name}: {Width}x{Height}+{X}+{Y} @ {RefreshRate:F1}Hz ({Dpi:F0} DPI){(IsPrimary ? " [Primary]" : "")}";
|
||||||
|
}
|
||||||
|
}
|
||||||
376
Services/MonitorService.cs
Normal file
376
Services/MonitorService.cs
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Interop;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for querying and monitoring display configuration using XRandR.
|
||||||
|
/// </summary>
|
||||||
|
public class MonitorService : IDisposable
|
||||||
|
{
|
||||||
|
private static MonitorService? _instance;
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
|
||||||
|
private IntPtr _display;
|
||||||
|
private IntPtr _rootWindow;
|
||||||
|
private List<MonitorInfo> _monitors = new();
|
||||||
|
private bool _initialized;
|
||||||
|
private bool _disposed;
|
||||||
|
private int _eventBase;
|
||||||
|
private int _errorBase;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the singleton instance of the monitor service.
|
||||||
|
/// </summary>
|
||||||
|
public static MonitorService Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_instance ??= new MonitorService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of connected monitors.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<MonitorInfo> Monitors
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
return _monitors.AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the primary monitor.
|
||||||
|
/// </summary>
|
||||||
|
public MonitorInfo? PrimaryMonitor
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
return _monitors.FirstOrDefault(m => m.IsPrimary) ?? _monitors.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total virtual desktop bounds (union of all monitors).
|
||||||
|
/// </summary>
|
||||||
|
public (int X, int Y, int Width, int Height) VirtualDesktopBounds
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
if (_monitors.Count == 0)
|
||||||
|
return (0, 0, 1920, 1080); // Default fallback
|
||||||
|
|
||||||
|
int minX = _monitors.Min(m => m.X);
|
||||||
|
int minY = _monitors.Min(m => m.Y);
|
||||||
|
int maxX = _monitors.Max(m => m.X + m.Width);
|
||||||
|
int maxY = _monitors.Max(m => m.Y + m.Height);
|
||||||
|
|
||||||
|
return (minX, minY, maxX - minX, maxY - minY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised when monitor configuration changes.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<MonitorConfigurationChangedEventArgs>? ConfigurationChanged;
|
||||||
|
|
||||||
|
private MonitorService()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureInitialized()
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_display = X11.XOpenDisplay(IntPtr.Zero);
|
||||||
|
if (_display == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[MonitorService] Failed to open X11 display");
|
||||||
|
_initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int screen = X11.XDefaultScreen(_display);
|
||||||
|
_rootWindow = X11.XRootWindow(_display, screen);
|
||||||
|
|
||||||
|
// Check if XRandR is available
|
||||||
|
if (XRandR.XRRQueryExtension(_display, out _eventBase, out _errorBase) == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[MonitorService] XRandR extension not available");
|
||||||
|
_initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (XRandR.XRRQueryVersion(_display, out int major, out int minor) == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[MonitorService] Failed to query XRandR version");
|
||||||
|
_initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[MonitorService] XRandR {major}.{minor} available");
|
||||||
|
|
||||||
|
RefreshMonitors();
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[MonitorService] Initialization failed: {ex.Message}");
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the monitor list from the system.
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshMonitors()
|
||||||
|
{
|
||||||
|
if (_display == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
var newMonitors = new List<MonitorInfo>();
|
||||||
|
|
||||||
|
IntPtr resources = IntPtr.Zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
resources = XRandR.XRRGetScreenResourcesCurrent(_display, _rootWindow);
|
||||||
|
if (resources == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[MonitorService] Failed to get screen resources");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = Marshal.PtrToStructure<XRRScreenResources>(resources);
|
||||||
|
|
||||||
|
// Get array of output IDs
|
||||||
|
var outputIds = new ulong[res.NOutput];
|
||||||
|
for (int i = 0; i < res.NOutput; i++)
|
||||||
|
{
|
||||||
|
outputIds[i] = (ulong)Marshal.ReadInt64(res.Outputs, i * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track which monitor is at 0,0 (likely primary)
|
||||||
|
MonitorInfo? primaryCandidate = null;
|
||||||
|
|
||||||
|
foreach (var outputId in outputIds)
|
||||||
|
{
|
||||||
|
IntPtr outputInfo = IntPtr.Zero;
|
||||||
|
IntPtr crtcInfo = IntPtr.Zero;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
outputInfo = XRandR.XRRGetOutputInfo(_display, resources, outputId);
|
||||||
|
if (outputInfo == IntPtr.Zero) continue;
|
||||||
|
|
||||||
|
var output = Marshal.PtrToStructure<XRROutputInfo>(outputInfo);
|
||||||
|
|
||||||
|
// Skip disconnected outputs
|
||||||
|
if (output.Connection != XRandR.RR_Connected)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip outputs without a CRTC (not currently displaying)
|
||||||
|
if (output.Crtc == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
crtcInfo = XRandR.XRRGetCrtcInfo(_display, resources, output.Crtc);
|
||||||
|
if (crtcInfo == IntPtr.Zero) continue;
|
||||||
|
|
||||||
|
var crtc = Marshal.PtrToStructure<XRRCrtcInfo>(crtcInfo);
|
||||||
|
|
||||||
|
// Get output name
|
||||||
|
string name = output.Name != IntPtr.Zero && output.NameLen > 0
|
||||||
|
? Marshal.PtrToStringAnsi(output.Name, output.NameLen) ?? $"Output-{outputId}"
|
||||||
|
: $"Output-{outputId}";
|
||||||
|
|
||||||
|
// Calculate refresh rate from mode info
|
||||||
|
double refreshRate = GetRefreshRate(resources, crtc.Mode);
|
||||||
|
|
||||||
|
var monitor = new MonitorInfo
|
||||||
|
{
|
||||||
|
Id = outputId,
|
||||||
|
Name = name,
|
||||||
|
X = crtc.X,
|
||||||
|
Y = crtc.Y,
|
||||||
|
Width = (int)crtc.Width,
|
||||||
|
Height = (int)crtc.Height,
|
||||||
|
PhysicalWidthMm = (int)output.MmWidth,
|
||||||
|
PhysicalHeightMm = (int)output.MmHeight,
|
||||||
|
RefreshRate = refreshRate,
|
||||||
|
IsPrimary = false // Will be set below
|
||||||
|
};
|
||||||
|
|
||||||
|
newMonitors.Add(monitor);
|
||||||
|
|
||||||
|
// Track the monitor at 0,0 as primary candidate
|
||||||
|
if (crtc.X == 0 && crtc.Y == 0)
|
||||||
|
{
|
||||||
|
primaryCandidate = monitor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (crtcInfo != IntPtr.Zero)
|
||||||
|
XRandR.XRRFreeCrtcInfo(crtcInfo);
|
||||||
|
if (outputInfo != IntPtr.Zero)
|
||||||
|
XRandR.XRRFreeOutputInfo(outputInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set primary monitor (the one at 0,0 or the first one)
|
||||||
|
if (newMonitors.Count > 0)
|
||||||
|
{
|
||||||
|
var primary = primaryCandidate ?? newMonitors[0];
|
||||||
|
var index = newMonitors.IndexOf(primary);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
newMonitors[index] = primary with { IsPrimary = true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldMonitors = _monitors;
|
||||||
|
_monitors = newMonitors;
|
||||||
|
|
||||||
|
// Log detected monitors
|
||||||
|
Console.WriteLine($"[MonitorService] Detected {_monitors.Count} monitor(s):");
|
||||||
|
foreach (var monitor in _monitors)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {monitor}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify if configuration changed
|
||||||
|
if (!MonitorListsEqual(oldMonitors, newMonitors))
|
||||||
|
{
|
||||||
|
ConfigurationChanged?.Invoke(this, new MonitorConfigurationChangedEventArgs(_monitors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (resources != IntPtr.Zero)
|
||||||
|
XRandR.XRRFreeScreenResources(resources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetRefreshRate(IntPtr resources, ulong modeId)
|
||||||
|
{
|
||||||
|
if (modeId == 0) return 60.0; // Default
|
||||||
|
|
||||||
|
var res = Marshal.PtrToStructure<XRRScreenResources>(resources);
|
||||||
|
|
||||||
|
for (int i = 0; i < res.NMode; i++)
|
||||||
|
{
|
||||||
|
var modePtr = res.Modes + i * Marshal.SizeOf<XRRModeInfo>();
|
||||||
|
var mode = Marshal.PtrToStructure<XRRModeInfo>(modePtr);
|
||||||
|
|
||||||
|
if (mode.Id == modeId)
|
||||||
|
{
|
||||||
|
if (mode.HTotal > 0 && mode.VTotal > 0 && mode.DotClock > 0)
|
||||||
|
{
|
||||||
|
return (double)mode.DotClock / (mode.HTotal * mode.VTotal);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 60.0; // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MonitorListsEqual(List<MonitorInfo> a, List<MonitorInfo> b)
|
||||||
|
{
|
||||||
|
if (a.Count != b.Count) return false;
|
||||||
|
|
||||||
|
for (int i = 0; i < a.Count; i++)
|
||||||
|
{
|
||||||
|
if (a[i].Id != b[i].Id ||
|
||||||
|
a[i].X != b[i].X ||
|
||||||
|
a[i].Y != b[i].Y ||
|
||||||
|
a[i].Width != b[i].Width ||
|
||||||
|
a[i].Height != b[i].Height)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the monitor containing the specified point.
|
||||||
|
/// </summary>
|
||||||
|
public MonitorInfo? GetMonitorAt(int x, int y)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
return _monitors.FirstOrDefault(m =>
|
||||||
|
x >= m.X && x < m.X + m.Width &&
|
||||||
|
y >= m.Y && y < m.Y + m.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the monitor containing the center of the specified rectangle.
|
||||||
|
/// </summary>
|
||||||
|
public MonitorInfo? GetMonitorFromRect(int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
int centerX = x + width / 2;
|
||||||
|
int centerY = y + height / 2;
|
||||||
|
return GetMonitorAt(centerX, centerY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the monitor by name (e.g., "HDMI-1", "DP-2").
|
||||||
|
/// </summary>
|
||||||
|
public MonitorInfo? GetMonitorByName(string name)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
return _monitors.FirstOrDefault(m =>
|
||||||
|
m.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
if (_display != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
X11.XCloseDisplay(_display);
|
||||||
|
_display = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event args for monitor configuration changes.
|
||||||
|
/// </summary>
|
||||||
|
public class MonitorConfigurationChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the updated list of monitors.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<MonitorInfo> Monitors { get; }
|
||||||
|
|
||||||
|
public MonitorConfigurationChangedEventArgs(IReadOnlyList<MonitorInfo> monitors)
|
||||||
|
{
|
||||||
|
Monitors = monitors;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -321,6 +321,84 @@ public class X11Window : IDisposable
|
|||||||
X11.XFlush(_display);
|
X11.XFlush(_display);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves the window to the specified position.
|
||||||
|
/// </summary>
|
||||||
|
public void SetPosition(int x, int y)
|
||||||
|
{
|
||||||
|
X11.XMoveWindow(_display, _window, x, y);
|
||||||
|
X11.XFlush(_display);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximizes the window.
|
||||||
|
/// </summary>
|
||||||
|
public void Maximize()
|
||||||
|
{
|
||||||
|
SendWindowStateEvent(true, "_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimizes (iconifies) the window.
|
||||||
|
/// </summary>
|
||||||
|
public void Minimize()
|
||||||
|
{
|
||||||
|
X11.XIconifyWindow(_display, _window, _screen);
|
||||||
|
X11.XFlush(_display);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the window from maximized or minimized state.
|
||||||
|
/// </summary>
|
||||||
|
public void Restore()
|
||||||
|
{
|
||||||
|
// Remove maximized state
|
||||||
|
SendWindowStateEvent(false, "_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ");
|
||||||
|
// Map window if it was minimized
|
||||||
|
X11.XMapWindow(_display, _window);
|
||||||
|
X11.XFlush(_display);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets fullscreen mode.
|
||||||
|
/// </summary>
|
||||||
|
public void SetFullscreen(bool fullscreen)
|
||||||
|
{
|
||||||
|
SendWindowStateEvent(fullscreen, "_NET_WM_STATE_FULLSCREEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendWindowStateEvent(bool add, params string[] stateNames)
|
||||||
|
{
|
||||||
|
var wmState = X11.XInternAtom(_display, "_NET_WM_STATE", false);
|
||||||
|
var rootWindow = X11.XRootWindow(_display, _screen);
|
||||||
|
|
||||||
|
foreach (var stateName in stateNames)
|
||||||
|
{
|
||||||
|
var stateAtom = X11.XInternAtom(_display, stateName, false);
|
||||||
|
|
||||||
|
var xev = new XEvent();
|
||||||
|
xev.ClientMessageEvent.Type = X11.ClientMessage;
|
||||||
|
xev.ClientMessageEvent.Window = _window;
|
||||||
|
xev.ClientMessageEvent.MessageType = wmState;
|
||||||
|
xev.ClientMessageEvent.Format = 32;
|
||||||
|
|
||||||
|
// data.l[0] = action (0=remove, 1=add, 2=toggle)
|
||||||
|
// data.l[1] = first property
|
||||||
|
// data.l[2] = second property (optional)
|
||||||
|
// data.l[3] = source indication (1 = normal application)
|
||||||
|
xev.ClientMessageEvent.Data.L0 = add ? 1 : 0;
|
||||||
|
xev.ClientMessageEvent.Data.L1 = (long)stateAtom;
|
||||||
|
xev.ClientMessageEvent.Data.L2 = 0;
|
||||||
|
xev.ClientMessageEvent.Data.L3 = 1;
|
||||||
|
|
||||||
|
X11.XSendEvent(_display, rootWindow, false,
|
||||||
|
X11.SubstructureRedirectMask | X11.SubstructureNotifyMask,
|
||||||
|
ref xev);
|
||||||
|
}
|
||||||
|
|
||||||
|
X11.XFlush(_display);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes pending X11 events.
|
/// Processes pending X11 events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user