mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 00:46:12 +02:00
Improve screen selector and make screen IDs stable
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
<UserControl x:Class="DesktopMagic.Controls.ScreenSelector"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="60"
|
||||
d:DesignWidth="600"
|
||||
Loaded="UserControl_Loaded"
|
||||
SizeChanged="UserControl_SizeChanged"
|
||||
Unloaded="UserControl_Unloaded">
|
||||
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Background="Transparent">
|
||||
<ItemsControl ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=UserControl}}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border x:Name="ScreenBorder"
|
||||
Width="{Binding Width}"
|
||||
Height="{Binding Height}"
|
||||
MouseLeftButtonDown="Screen_MouseLeftButtonDown"
|
||||
Cursor="Hand"
|
||||
ToolTipService.ToolTip="{Binding ToolTipText}">
|
||||
<Border.RenderTransform>
|
||||
<TranslateTransform X="{Binding X}" Y="{Binding Y}" />
|
||||
</Border.RenderTransform>
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentFillColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
|
||||
<Grid>
|
||||
<TextBlock
|
||||
Margin="4,1,0,0"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding Index}" />
|
||||
|
||||
<Ellipse
|
||||
Width="7"
|
||||
Height="7"
|
||||
Margin="0,0,3,3"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Fill="{DynamicResource AccentFillColorDefaultBrush}"
|
||||
Visibility="{Binding IsPrimary, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
ToolTipService.ToolTip="Primary screen" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,182 @@
|
||||
using DesktopMagic.DataContexts;
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace DesktopMagic.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Visual screen selector showing the connected monitors as a mini-map
|
||||
/// scaled to the control's size while preserving their relative positions.
|
||||
/// </summary>
|
||||
public partial class ScreenSelector : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty ItemsSourceProperty =
|
||||
DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(ScreenSelector),
|
||||
new PropertyMetadata(null, OnItemsSourceChanged));
|
||||
|
||||
public IEnumerable? ItemsSource
|
||||
{
|
||||
get => (IEnumerable?)GetValue(ItemsSourceProperty);
|
||||
set => SetValue(ItemsSourceProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SelectedItemProperty =
|
||||
DependencyProperty.Register(nameof(SelectedItem), typeof(ScreenDisplay), typeof(ScreenSelector),
|
||||
new PropertyMetadata(null, OnSelectedItemChanged));
|
||||
|
||||
public ScreenDisplay? SelectedItem
|
||||
{
|
||||
get => (ScreenDisplay?)GetValue(SelectedItemProperty);
|
||||
set => SetValue(SelectedItemProperty, value);
|
||||
}
|
||||
|
||||
private INotifyCollectionChanged? notifySource;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user picks a screen by clicking it.
|
||||
/// </summary>
|
||||
public event Action? SelectionChanged;
|
||||
|
||||
public ScreenSelector()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
ScreenSelector control = (ScreenSelector)d;
|
||||
control.AttachSource();
|
||||
control.UpdateCanvas();
|
||||
control.UpdateSelection();
|
||||
}
|
||||
|
||||
private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((ScreenSelector)d).UpdateSelection();
|
||||
}
|
||||
|
||||
private void AttachSource()
|
||||
{
|
||||
if (notifySource is INotifyCollectionChanged oldSource)
|
||||
{
|
||||
oldSource.CollectionChanged -= Source_CollectionChanged;
|
||||
}
|
||||
|
||||
notifySource = ItemsSource as INotifyCollectionChanged;
|
||||
if (notifySource is not null)
|
||||
{
|
||||
notifySource.CollectionChanged += Source_CollectionChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void Source_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateCanvas();
|
||||
UpdateSelection();
|
||||
}
|
||||
|
||||
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Re-attach in case the control was unloaded and the source instance changed.
|
||||
AttachSource();
|
||||
UpdateCanvas();
|
||||
}
|
||||
|
||||
private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateCanvas();
|
||||
}
|
||||
|
||||
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (notifySource is INotifyCollectionChanged source)
|
||||
{
|
||||
source.CollectionChanged -= Source_CollectionChanged;
|
||||
}
|
||||
|
||||
notifySource = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the real screen bounds so they fit the control while keeping their
|
||||
/// relative positions (including negative coordinates), then centers them.
|
||||
/// </summary>
|
||||
private void UpdateCanvas()
|
||||
{
|
||||
if (ItemsSource is null || ActualWidth <= 0 || ActualHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<ScreenDisplay> screens = ItemsSource.Cast<ScreenDisplay>().ToList();
|
||||
if (screens.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Union of all monitor bounds (virtual screen space, negative coords kept).
|
||||
Rectangle totalBounds = new Rectangle();
|
||||
foreach (var item in screens)
|
||||
{
|
||||
totalBounds = Rectangle.Union(totalBounds, item.Bounds);
|
||||
}
|
||||
|
||||
// Uniform scale factor + a little margin so the mini-map never touches the edge.
|
||||
double factor = Math.Max(totalBounds.Height / ActualHeight, totalBounds.Width / ActualWidth) + 2;
|
||||
|
||||
foreach (var item in screens)
|
||||
{
|
||||
item.X = item.Bounds.Left / factor;
|
||||
item.Y = item.Bounds.Top / factor;
|
||||
item.Width = item.Bounds.Width / factor;
|
||||
item.Height = item.Bounds.Height / factor;
|
||||
}
|
||||
|
||||
// Center the whole arrangement in the control.
|
||||
double minLeft = screens.Min(item => item.X);
|
||||
double maxRight = screens.Max(item => item.X + item.Width);
|
||||
double minTop = screens.Min(item => item.Y);
|
||||
double maxBottom = screens.Max(item => item.Y + item.Height);
|
||||
|
||||
double horizontalOffset = ((maxRight + minLeft) / 2) - (ActualWidth / 2);
|
||||
double verticalOffset = ((maxBottom + minTop) / 2) - (ActualHeight / 2);
|
||||
|
||||
foreach (var item in screens)
|
||||
{
|
||||
item.X -= horizontalOffset;
|
||||
item.Y -= verticalOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelection()
|
||||
{
|
||||
if (ItemsSource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ScreenDisplay item in ItemsSource)
|
||||
{
|
||||
item.IsSelected = item == SelectedItem;
|
||||
}
|
||||
}
|
||||
|
||||
private void Screen_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element && element.DataContext is ScreenDisplay screen)
|
||||
{
|
||||
SelectedItem = screen;
|
||||
SelectionChanged?.Invoke();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,19 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public ScreenDisplay? SelectedScreen
|
||||
{
|
||||
get => Screens.FirstOrDefault(screen => screen.DeviceName == SelectedScreenId);
|
||||
set
|
||||
{
|
||||
if (value is not null)
|
||||
{
|
||||
SelectedScreenId = value.DeviceName;
|
||||
}
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Layout SelectedLayout => Manager.Instance.SelectedLayout;
|
||||
|
||||
public string? SelectedLayoutName
|
||||
@@ -111,16 +124,26 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
public void RefreshScreens()
|
||||
{
|
||||
List<System.Windows.Forms.Screen> allScreens = ScreenUtilities.GetAllScreens();
|
||||
Dictionary<string, System.Drawing.Rectangle> physicalBounds = ScreenUtilities.GetDisplayPhysicalBounds();
|
||||
|
||||
// Capture the hardware id of the currently selected screen so the selection can be
|
||||
// preserved across display changes (e.g. unplug/replug renumbers device names).
|
||||
ScreenDisplay? previousSelected = SelectedScreen;
|
||||
|
||||
Screens.Clear();
|
||||
for (int i = 0; i < allScreens.Count; i++)
|
||||
{
|
||||
Screens.Add(new ScreenDisplay(allScreens[i], i));
|
||||
System.Windows.Forms.Screen screen = allScreens[i];
|
||||
physicalBounds.TryGetValue(screen.DeviceName, out System.Drawing.Rectangle physicalBoundsRect);
|
||||
Screens.Add(new ScreenDisplay(screen, i, physicalBoundsRect, ScreenUtilities.GetMonitorHardwareId(screen)));
|
||||
}
|
||||
|
||||
if (selectedScreenDeviceName is null || !Screens.Any(screen => screen.DeviceName == selectedScreenDeviceName))
|
||||
{
|
||||
SelectedScreenId = Screens.FirstOrDefault()?.DeviceName;
|
||||
ScreenDisplay? byHardwareId = previousSelected is null
|
||||
? null
|
||||
: Screens.FirstOrDefault(screen => screen.HardwareId == previousSelected.HardwareId);
|
||||
SelectedScreenId = byHardwareId?.DeviceName ?? Screens.FirstOrDefault()?.DeviceName;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -144,6 +167,7 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
Manager.Instance.SelectedScreenDeviceName = selectedScreenDeviceName;
|
||||
selectedLayoutName = Manager.Instance.SelectedLayout.Name;
|
||||
OnPropertyChanged(nameof(SelectedScreenId));
|
||||
OnPropertyChanged(nameof(SelectedScreen));
|
||||
OnPropertyChanged(nameof(SelectedLayout));
|
||||
OnPropertyChanged(nameof(SelectedLayoutName));
|
||||
}
|
||||
@@ -157,15 +181,100 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
/// <summary>
|
||||
/// A detected screen displayed in the UI.
|
||||
/// </summary>
|
||||
public class ScreenDisplay
|
||||
public class ScreenDisplay : INotifyPropertyChanged
|
||||
{
|
||||
public ScreenDisplay(System.Windows.Forms.Screen screen, int index)
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private bool isSelected;
|
||||
private double x;
|
||||
private double y;
|
||||
private double width;
|
||||
private double height;
|
||||
|
||||
public ScreenDisplay(System.Windows.Forms.Screen screen, int index, System.Drawing.Rectangle physicalBounds, string hardwareId)
|
||||
{
|
||||
DeviceName = screen.DeviceName;
|
||||
DisplayName = ScreenUtilities.GetScreenLabel(screen, index);
|
||||
Bounds = physicalBounds.Width > 0 && physicalBounds.Height > 0 ? physicalBounds : screen.Bounds;
|
||||
IsPrimary = screen.Primary;
|
||||
Index = index + 1;
|
||||
HardwareId = hardwareId;
|
||||
}
|
||||
|
||||
public string DeviceName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stable hardware identifier of the monitor (see <see cref="ScreenUtilities.GetMonitorHardwareId"/>),
|
||||
/// used to persist screen bindings across display changes.
|
||||
/// </summary>
|
||||
public string HardwareId { get; }
|
||||
|
||||
public string DisplayName { get; }
|
||||
|
||||
public System.Drawing.Rectangle Bounds { get; }
|
||||
|
||||
public bool IsPrimary { get; }
|
||||
|
||||
public int Index { get; }
|
||||
|
||||
public string ToolTipText => IsPrimary ? $"{DisplayName} · Primary" : DisplayName;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this screen is currently selected in the screen selector.
|
||||
/// </summary>
|
||||
public bool IsSelected
|
||||
{
|
||||
get => isSelected;
|
||||
set
|
||||
{
|
||||
isSelected = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Normalized (control-local) position/size set by the screen selector.
|
||||
public double X
|
||||
{
|
||||
get => x;
|
||||
set
|
||||
{
|
||||
x = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public double Y
|
||||
{
|
||||
get => y;
|
||||
set
|
||||
{
|
||||
y = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public double Width
|
||||
{
|
||||
get => width;
|
||||
set
|
||||
{
|
||||
width = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public double Height
|
||||
{
|
||||
get => height;
|
||||
set
|
||||
{
|
||||
height = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.ScreenSelectorDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:controls="clr-namespace:DesktopMagic.Controls"
|
||||
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
|
||||
mc:Ignorable="d"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
|
||||
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
|
||||
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
WindowCornerPreference="Round"
|
||||
WindowBackdropType="Tabbed"
|
||||
SizeToContent="Height"
|
||||
Width="380"
|
||||
MinWidth="380"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ExtendsContentIntoTitleBar="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TitleBar x:Name="titleBar" ShowMinimize="False" ShowMaximize="False">
|
||||
<ui:TitleBar.Icon>
|
||||
<ui:ImageIcon Source="{StaticResource Icon}" />
|
||||
</ui:TitleBar.Icon>
|
||||
</ui:TitleBar>
|
||||
|
||||
<controls:ScreenSelector x:Name="screenSelector" Grid.Row="1" Margin="15" Width="340" Height="200" ItemsSource="{Binding Screens}" SelectedItem="{Binding SelectedScreen, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
</ui:FluentWindow>
|
||||
@@ -0,0 +1,26 @@
|
||||
using DesktopMagic.DataContexts;
|
||||
|
||||
using System.Windows;
|
||||
|
||||
namespace DesktopMagic.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Modal picker for selecting a screen from a visual monitor layout.
|
||||
/// Closes itself once a screen has been picked.
|
||||
/// </summary>
|
||||
public partial class ScreenSelectorDialog : Wpf.Ui.Controls.FluentWindow
|
||||
{
|
||||
internal ScreenSelectorDialog(MainWindowDataContext dataContext, string title = App.AppName)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Resources.MergedDictionaries.Add(App.LanguageDictionary);
|
||||
|
||||
DataContext = dataContext;
|
||||
titleBar.Title = title;
|
||||
Title = title;
|
||||
|
||||
// Close the dialog once a screen has been picked.
|
||||
screenSelector.SelectionChanged += () => DialogResult = true;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
namespace DesktopMagic.Helpers;
|
||||
@@ -168,25 +170,106 @@ public static class ScreenUtilities
|
||||
/// or an empty string when it cannot be determined.
|
||||
/// </summary>
|
||||
public static string GetFriendlyName(System.Windows.Forms.Screen screen)
|
||||
{
|
||||
return GetMonitorDisplayDevice(screen)?.DeviceString ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a stable hardware identifier for the screen: the PnP device instance ID of its
|
||||
/// monitor (e.g. "MONITOR\DEL41F1\{...}\{0001}"), which is derived from the monitor's
|
||||
/// EDID and survives disconnects and reconnects of the same monitor. When no hardware ID
|
||||
/// is available (e.g. virtual or remote displays), a deterministic SHA-256 hash of the
|
||||
/// bounds is used so the identifier is never empty.
|
||||
/// </summary>
|
||||
public static string GetMonitorHardwareId(System.Windows.Forms.Screen screen)
|
||||
{
|
||||
string? deviceId = GetMonitorDisplayDevice(screen)?.DeviceID;
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
System.Drawing.Rectangle bounds = screen.Bounds;
|
||||
string boundsString = $"{bounds.X}-{bounds.Y}-{bounds.Width}-{bounds.Height}";
|
||||
string hashString = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(boundsString))).ToLowerInvariant();
|
||||
return $"DISPLAY#{hashString}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the monitor device info (second-level <see cref="EnumDisplayDevices"/> entry) for
|
||||
/// the given screen, or null when it cannot be determined.
|
||||
/// </summary>
|
||||
private static DISPLAY_DEVICE? GetMonitorDisplayDevice(System.Windows.Forms.Screen screen)
|
||||
{
|
||||
try
|
||||
{
|
||||
var device = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||
DISPLAY_DEVICE device = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||
if (EnumDisplayDevices(screen.DeviceName, 0, ref device, 0))
|
||||
{
|
||||
var monitor = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||
DISPLAY_DEVICE monitor = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||
if (EnumDisplayDevices(device.DeviceName, 0, ref monitor, 0))
|
||||
{
|
||||
return monitor.DeviceString;
|
||||
return monitor;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fall through to an empty name.
|
||||
// Fall through to null.
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates monitors in a Per-Monitor V2 DPI awareness context and returns each
|
||||
/// monitor's bounds in physical (device) pixels on the virtual screen, keyed by device
|
||||
/// name (e.g. "\\.\DISPLAY1"). This uses the same coordinate space as DPI-aware apps
|
||||
/// such as Lively Wallpaper, keeping multi-screen layouts with mixed DPI scaling
|
||||
/// consistent regardless of this app's own DPI awareness mode.
|
||||
/// </summary>
|
||||
public static Dictionary<string, System.Drawing.Rectangle> GetDisplayPhysicalBounds()
|
||||
{
|
||||
Dictionary<string, System.Drawing.Rectangle> result = new Dictionary<string, System.Drawing.Rectangle>();
|
||||
|
||||
IntPtr prevContext = IntPtr.Zero;
|
||||
bool contextChanged = false;
|
||||
try
|
||||
{
|
||||
prevContext = SetThreadDpiAwarenessContext((IntPtr)DpiAwarenessContextPerMonitorV2);
|
||||
contextChanged = prevContext != IntPtr.Zero;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// SetThreadDpiAwarenessContext unavailable; fall back to default context.
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (hMonitor, hdcMonitor, lprcMonitor, dwData) =>
|
||||
{
|
||||
MONITORINFOEX info = new MONITORINFOEX { cbSize = (uint)Marshal.SizeOf<MONITORINFOEX>() };
|
||||
if (GetMonitorInfo(hMonitor, ref info))
|
||||
{
|
||||
string deviceName = info.szDevice.TrimEnd('\0');
|
||||
result[deviceName] = new System.Drawing.Rectangle(
|
||||
info.rcMonitor.Left, info.rcMonitor.Top,
|
||||
info.rcMonitor.Right - info.rcMonitor.Left,
|
||||
info.rcMonitor.Bottom - info.rcMonitor.Top);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (contextChanged)
|
||||
{
|
||||
_ = SetThreadDpiAwarenessContext(prevContext);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -195,11 +278,7 @@ public static class ScreenUtilities
|
||||
private static Rect GetScreenDips(System.Drawing.Rectangle bounds)
|
||||
{
|
||||
double scale = GetDpiScale(bounds);
|
||||
return new Rect(
|
||||
bounds.Left / scale,
|
||||
bounds.Top / scale,
|
||||
bounds.Width / scale,
|
||||
bounds.Height / scale);
|
||||
return new Rect(bounds.Left / scale, bounds.Top / scale, bounds.Width / scale, bounds.Height / scale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -209,7 +288,7 @@ public static class ScreenUtilities
|
||||
{
|
||||
try
|
||||
{
|
||||
var center = new POINT
|
||||
POINT center = new POINT
|
||||
{
|
||||
X = bounds.Left + (bounds.Width / 2),
|
||||
Y = bounds.Top + (bounds.Height / 2)
|
||||
@@ -263,4 +342,41 @@ public static class ScreenUtilities
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
private const long DpiAwarenessContextPerMonitorV2 = -4;
|
||||
|
||||
private delegate bool EnumMonitorsProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumMonitorsProc lpfnEnum, IntPtr dwData);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MONITORRECT
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct MONITORINFOEX
|
||||
{
|
||||
public uint cbSize;
|
||||
|
||||
public MONITORRECT rcMonitor;
|
||||
|
||||
public MONITORRECT rcWork;
|
||||
|
||||
public uint dwFlags;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string szDevice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ public partial class MainWindow : FluentWindow
|
||||
{
|
||||
App.Logger.LogInfo("Loading application", source: "MainWindow");
|
||||
|
||||
Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged;
|
||||
|
||||
_mainWindowDataContext.IsLoading = true;
|
||||
|
||||
// Load plugins and settings through manager
|
||||
@@ -99,6 +101,8 @@ public partial class MainWindow : FluentWindow
|
||||
|
||||
private void Window_Closed(object sender, EventArgs e)
|
||||
{
|
||||
Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged;
|
||||
|
||||
Visibility = Visibility.Collapsed;
|
||||
UpdateLayout();
|
||||
_manager.CloseAllPluginWindows();
|
||||
@@ -113,6 +117,16 @@ public partial class MainWindow : FluentWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// Re-enumerate screens and reload all widget windows when monitors are added or removed.
|
||||
_ = Application.Current.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
_mainWindowDataContext.RefreshScreens();
|
||||
_manager.LoadLayout();
|
||||
});
|
||||
}
|
||||
|
||||
internal void RestoreWindow()
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
|
||||
@@ -398,7 +398,7 @@ public sealed class Manager
|
||||
|
||||
if (Settings.ScreenLayouts.Count == 0)
|
||||
{
|
||||
Settings.ScreenLayouts[primaryScreen.DeviceName] = Settings.CurrentLayoutName ?? "Default";
|
||||
Settings.ScreenLayouts[ScreenUtilities.GetMonitorHardwareId(primaryScreen)] = Settings.CurrentLayoutName ?? "Default";
|
||||
}
|
||||
|
||||
App.Logger.LogInfo("Settings migrated to screen-aware layouts", source: "Manager");
|
||||
@@ -429,7 +429,7 @@ public sealed class Manager
|
||||
/// </summary>
|
||||
public Layout GetLayoutForScreen(System.Windows.Forms.Screen screen)
|
||||
{
|
||||
if (Settings.ScreenLayouts.TryGetValue(screen.DeviceName, out string? layoutName))
|
||||
if (Settings.ScreenLayouts.TryGetValue(ScreenUtilities.GetMonitorHardwareId(screen), out string? layoutName))
|
||||
{
|
||||
Layout? bound = Settings.Layouts.FirstOrDefault(layout => layout.Name == layoutName);
|
||||
if (bound is not null)
|
||||
@@ -457,7 +457,7 @@ public sealed class Manager
|
||||
/// </summary>
|
||||
public void BindLayoutToScreen(System.Windows.Forms.Screen screen, Layout layout)
|
||||
{
|
||||
Settings.ScreenLayouts[screen.DeviceName] = layout.Name;
|
||||
Settings.ScreenLayouts[ScreenUtilities.GetMonitorHardwareId(screen)] = layout.Name;
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
|
||||
@@ -76,18 +76,32 @@
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ComboBox x:Name="screensComboBox" ItemsSource="{Binding Screens}" DisplayMemberPath="DisplayName" SelectedValue="{Binding SelectedScreenId}" SelectedValuePath="DeviceName" VerticalAlignment="Center" />
|
||||
<ui:Button x:Name="screenSelectorButton" VerticalAlignment="Stretch" Padding="10,4" Click="ScreenSelectorButton_Click">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="screenSelectorButtonText" VerticalAlignment="Center" Text="{Binding SelectedScreen.DisplayName}" />
|
||||
<ui:SymbolIcon Symbol="ChevronDown24" Margin="8,0,0,0" FontSize="12" />
|
||||
</StackPanel>
|
||||
</ui:Button>
|
||||
|
||||
<ComboBox x:Name="layoutsComboBox" Grid.Column="2" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding SelectedLayoutName}" SelectedValuePath="Name" VerticalAlignment="Center" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
||||
|
||||
<StackPanel Grid.Row="2" Grid.ColumnSpan="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Icon="{ui:SymbolIcon Add24}" Margin="0,0,5,0" Click="NewLayoutButton_Click" FontWeight="Regular" />
|
||||
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Margin="0,0,5,0" Click="RemoveLayoutButton_Click" FontWeight="Regular" />
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="2" Grid.ColumnSpan="3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="0.5*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="0.5*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="NewLayoutButton_Click" FontWeight="Regular" />
|
||||
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="RemoveLayoutButton_Click" FontWeight="Regular" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ui:Card>
|
||||
</Grid>
|
||||
|
||||
@@ -77,6 +77,16 @@ public partial class MainPage : Page
|
||||
_manager.SetEditMode(editCheckBox.IsChecked == true);
|
||||
}
|
||||
|
||||
private void ScreenSelectorButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ScreenSelectorDialog dialog = new(_dataContext)
|
||||
{
|
||||
Owner = Window.GetWindow(this)
|
||||
};
|
||||
|
||||
_ = dialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void PluginCheckBox_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Control checkBox)
|
||||
|
||||
Reference in New Issue
Block a user