Improve screen selector and make screen IDs stable

This commit is contained in:
Stone_Red
2026-08-15 16:02:26 +02:00
parent 507a0e9458
commit 4ba39c67ce
10 changed files with 608 additions and 24 deletions
@@ -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 Layout SelectedLayout => Manager.Instance.SelectedLayout;
public string? SelectedLayoutName public string? SelectedLayoutName
@@ -111,16 +124,26 @@ internal class MainWindowDataContext : INotifyPropertyChanged
public void RefreshScreens() public void RefreshScreens()
{ {
List<System.Windows.Forms.Screen> allScreens = ScreenUtilities.GetAllScreens(); 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(); Screens.Clear();
for (int i = 0; i < allScreens.Count; i++) 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)) 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 else
{ {
@@ -144,6 +167,7 @@ internal class MainWindowDataContext : INotifyPropertyChanged
Manager.Instance.SelectedScreenDeviceName = selectedScreenDeviceName; Manager.Instance.SelectedScreenDeviceName = selectedScreenDeviceName;
selectedLayoutName = Manager.Instance.SelectedLayout.Name; selectedLayoutName = Manager.Instance.SelectedLayout.Name;
OnPropertyChanged(nameof(SelectedScreenId)); OnPropertyChanged(nameof(SelectedScreenId));
OnPropertyChanged(nameof(SelectedScreen));
OnPropertyChanged(nameof(SelectedLayout)); OnPropertyChanged(nameof(SelectedLayout));
OnPropertyChanged(nameof(SelectedLayoutName)); OnPropertyChanged(nameof(SelectedLayoutName));
} }
@@ -157,15 +181,100 @@ internal class MainWindowDataContext : INotifyPropertyChanged
/// <summary> /// <summary>
/// A detected screen displayed in the UI. /// A detected screen displayed in the UI.
/// </summary> /// </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; DeviceName = screen.DeviceName;
DisplayName = ScreenUtilities.GetScreenLabel(screen, index); 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; } 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 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;
}
}
+127 -11
View File
@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Windows; using System.Windows;
namespace DesktopMagic.Helpers; namespace DesktopMagic.Helpers;
@@ -168,25 +170,106 @@ public static class ScreenUtilities
/// or an empty string when it cannot be determined. /// or an empty string when it cannot be determined.
/// </summary> /// </summary>
public static string GetFriendlyName(System.Windows.Forms.Screen screen) 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 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)) 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)) if (EnumDisplayDevices(device.DeviceName, 0, ref monitor, 0))
{ {
return monitor.DeviceString; return monitor;
} }
} }
} }
catch (Exception) 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> /// <summary>
@@ -195,11 +278,7 @@ public static class ScreenUtilities
private static Rect GetScreenDips(System.Drawing.Rectangle bounds) private static Rect GetScreenDips(System.Drawing.Rectangle bounds)
{ {
double scale = GetDpiScale(bounds); double scale = GetDpiScale(bounds);
return new Rect( return new Rect(bounds.Left / scale, bounds.Top / scale, bounds.Width / scale, bounds.Height / scale);
bounds.Left / scale,
bounds.Top / scale,
bounds.Width / scale,
bounds.Height / scale);
} }
/// <summary> /// <summary>
@@ -209,7 +288,7 @@ public static class ScreenUtilities
{ {
try try
{ {
var center = new POINT POINT center = new POINT
{ {
X = bounds.Left + (bounds.Width / 2), X = bounds.Left + (bounds.Width / 2),
Y = bounds.Top + (bounds.Height / 2) Y = bounds.Top + (bounds.Height / 2)
@@ -263,4 +342,41 @@ public static class ScreenUtilities
public int X; public int X;
public int Y; 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;
}
} }
+14
View File
@@ -39,6 +39,8 @@ public partial class MainWindow : FluentWindow
{ {
App.Logger.LogInfo("Loading application", source: "MainWindow"); App.Logger.LogInfo("Loading application", source: "MainWindow");
Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged;
_mainWindowDataContext.IsLoading = true; _mainWindowDataContext.IsLoading = true;
// Load plugins and settings through manager // Load plugins and settings through manager
@@ -99,6 +101,8 @@ public partial class MainWindow : FluentWindow
private void Window_Closed(object sender, EventArgs e) private void Window_Closed(object sender, EventArgs e)
{ {
Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged;
Visibility = Visibility.Collapsed; Visibility = Visibility.Collapsed;
UpdateLayout(); UpdateLayout();
_manager.CloseAllPluginWindows(); _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() internal void RestoreWindow()
{ {
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
+3 -3
View File
@@ -398,7 +398,7 @@ public sealed class Manager
if (Settings.ScreenLayouts.Count == 0) 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"); App.Logger.LogInfo("Settings migrated to screen-aware layouts", source: "Manager");
@@ -429,7 +429,7 @@ public sealed class Manager
/// </summary> /// </summary>
public Layout GetLayoutForScreen(System.Windows.Forms.Screen screen) 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); Layout? bound = Settings.Layouts.FirstOrDefault(layout => layout.Name == layoutName);
if (bound is not null) if (bound is not null)
@@ -457,7 +457,7 @@ public sealed class Manager
/// </summary> /// </summary>
public void BindLayoutToScreen(System.Windows.Forms.Screen screen, Layout layout) public void BindLayoutToScreen(System.Windows.Forms.Screen screen, Layout layout)
{ {
Settings.ScreenLayouts[screen.DeviceName] = layout.Name; Settings.ScreenLayouts[ScreenUtilities.GetMonitorHardwareId(screen)] = layout.Name;
SaveSettings(); SaveSettings();
} }
+20 -6
View File
@@ -76,18 +76,32 @@
<RowDefinition Height="auto" /> <RowDefinition Height="auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" /> <ColumnDefinition Width="auto" />
<ColumnDefinition Width="5" /> <ColumnDefinition Width="5" />
<ColumnDefinition Width="1*" /> <ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions> </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" /> <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"> <Grid Grid.Row="2" Grid.ColumnSpan="3">
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Icon="{ui:SymbolIcon Add24}" Margin="0,0,5,0" Click="NewLayoutButton_Click" FontWeight="Regular" /> <Grid.ColumnDefinitions>
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Margin="0,0,5,0" Click="RemoveLayoutButton_Click" FontWeight="Regular" /> <ColumnDefinition Width="1*" />
</StackPanel> <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> </Grid>
</ui:Card> </ui:Card>
</Grid> </Grid>
+10
View File
@@ -77,6 +77,16 @@ public partial class MainPage : Page
_manager.SetEditMode(editCheckBox.IsChecked == true); _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) private void PluginCheckBox_Click(object sender, RoutedEventArgs e)
{ {
if (sender is not Control checkBox) if (sender is not Control checkBox)