mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 08:56:15 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
930222f364 | ||
|
|
9b3b7aa4b6 | ||
|
|
7604acf115 | ||
|
|
1cafc01cd7 | ||
|
|
68ca15a81f | ||
|
|
fcacf36187 | ||
|
|
7755ece3cd | ||
|
|
13ed2f6a22 | ||
|
|
302ff3aa05 |
@@ -1,128 +0,0 @@
|
||||
using DesktopMagic.Api;
|
||||
using DesktopMagic.Api.Settings;
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
namespace DesktopMagic.BuiltInPlugins;
|
||||
|
||||
internal class BenchmarkPlugin : Plugin
|
||||
{
|
||||
private const int BitmapWidth = 400;
|
||||
private const int BitmapHeight = 300;
|
||||
|
||||
private readonly Random rng = new(42);
|
||||
private readonly object rngLock = new();
|
||||
|
||||
private readonly System.Diagnostics.Stopwatch frameTimer = System.Diagnostics.Stopwatch.StartNew();
|
||||
private double fps;
|
||||
private long lastFrameMs;
|
||||
|
||||
[Setting("shape-count", "Shapes")]
|
||||
private readonly IntegerUpDown shapeCount = new(10, 10000, 1000);
|
||||
|
||||
[Setting("shape-type", "Shape Type")]
|
||||
private readonly ComboBox shapeType = new("Rectangles", "Ellipses", "Lines", "Text", "Mixed");
|
||||
|
||||
[Setting("fps-counter", "Fps")]
|
||||
private readonly Label fpsCounter = new("");
|
||||
|
||||
public override int UpdateInterval => 16;
|
||||
|
||||
public override Bitmap? Main()
|
||||
{
|
||||
long now = frameTimer.ElapsedMilliseconds;
|
||||
if (lastFrameMs > 0)
|
||||
{
|
||||
double elapsed = now - lastFrameMs;
|
||||
fps = (fps * 0.9) + (1000.0 / Math.Max(elapsed, 1) * 0.1);
|
||||
}
|
||||
lastFrameMs = now;
|
||||
|
||||
int w = BitmapWidth;
|
||||
int h = BitmapHeight;
|
||||
|
||||
Bitmap bmp = new Bitmap(w, h);
|
||||
using Graphics g = Graphics.FromImage(bmp);
|
||||
g.Clear(Color.Transparent);
|
||||
g.SmoothingMode = SmoothingMode.HighSpeed;
|
||||
g.CompositingQuality = CompositingQuality.HighSpeed;
|
||||
g.InterpolationMode = InterpolationMode.Low;
|
||||
|
||||
Color color = Application.Theme.PrimaryColor;
|
||||
int count = shapeCount.Value;
|
||||
string type = shapeType.Value;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int x, y, rw, rh;
|
||||
lock (rngLock)
|
||||
{
|
||||
x = rng.Next(0, w);
|
||||
y = rng.Next(0, h);
|
||||
rw = rng.Next(5, 60);
|
||||
rh = rng.Next(5, 60);
|
||||
}
|
||||
|
||||
using Brush brush = new SolidBrush(Color.FromArgb(rng.Next(100, 255), color));
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "Rectangles":
|
||||
g.FillRectangle(brush, x, y, rw, rh);
|
||||
break;
|
||||
case "Ellipses":
|
||||
g.FillEllipse(brush, x, y, rw, rh);
|
||||
break;
|
||||
case "Lines":
|
||||
{
|
||||
int thickness;
|
||||
lock (rngLock) { thickness = rng.Next(1, 5); }
|
||||
g.DrawLine(new Pen(brush, thickness), x, y, x + rw, y + rh);
|
||||
}
|
||||
break;
|
||||
case "Text":
|
||||
{
|
||||
int fontSize;
|
||||
lock (rngLock) { fontSize = rng.Next(8, 24); }
|
||||
using Font font = new("Arial", fontSize);
|
||||
g.DrawString("Mg", font, brush, x, y);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
int shape, thickness, fontSize;
|
||||
lock (rngLock)
|
||||
{
|
||||
shape = rng.Next(0, 4);
|
||||
thickness = rng.Next(1, 5);
|
||||
fontSize = rng.Next(8, 24);
|
||||
}
|
||||
if (shape == 0)
|
||||
{
|
||||
g.FillRectangle(brush, x, y, rw, rh);
|
||||
}
|
||||
else if (shape == 1)
|
||||
{
|
||||
g.FillEllipse(brush, x, y, rw, rh);
|
||||
}
|
||||
else if (shape == 2)
|
||||
{
|
||||
g.DrawLine(new Pen(brush, thickness), x, y, x + rw, y + rh);
|
||||
}
|
||||
else
|
||||
{
|
||||
using Font font = new("Arial", fontSize);
|
||||
g.DrawString("Mg", font, brush, x, y);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fpsCounter.Value = fps.ToString();
|
||||
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
using DesktopMagic.Api;
|
||||
using DesktopMagic.Api.Settings;
|
||||
|
||||
using SkiaSharp;
|
||||
|
||||
using System;
|
||||
|
||||
namespace DesktopMagic.BuiltInPlugins;
|
||||
|
||||
internal class SkiaBenchmarkPlugin : SkiaPlugin
|
||||
{
|
||||
private readonly Random rng = new(42);
|
||||
private readonly object rngLock = new();
|
||||
|
||||
private readonly System.Diagnostics.Stopwatch frameTimer = System.Diagnostics.Stopwatch.StartNew();
|
||||
private double fps;
|
||||
private long lastFrameMs;
|
||||
|
||||
[Setting("skia-shape-count", "Shapes")]
|
||||
private readonly IntegerUpDown shapeCount = new(10, 10000, 1000);
|
||||
|
||||
[Setting("skia-shape-type", "Shape Type")]
|
||||
private readonly ComboBox shapeType = new("Rectangles", "Ellipses", "Lines", "Text", "Mixed");
|
||||
|
||||
public override int UpdateInterval => 16;
|
||||
|
||||
public override void Main(SKCanvas canvas)
|
||||
{
|
||||
long now = frameTimer.ElapsedMilliseconds;
|
||||
if (lastFrameMs > 0)
|
||||
{
|
||||
double elapsed = now - lastFrameMs;
|
||||
fps = fps * 0.9 + (1000.0 / Math.Max(elapsed, 1)) * 0.1;
|
||||
}
|
||||
lastFrameMs = now;
|
||||
|
||||
int w = (int)canvas.DeviceClipBounds.Width;
|
||||
int h = (int)canvas.DeviceClipBounds.Height;
|
||||
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
SKColor color = new(
|
||||
Application.Theme.PrimaryColor.R,
|
||||
Application.Theme.PrimaryColor.G,
|
||||
Application.Theme.PrimaryColor.B,
|
||||
Application.Theme.PrimaryColor.A);
|
||||
|
||||
int count = shapeCount.Value;
|
||||
string type = shapeType.Value;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int x, y, rw, rh;
|
||||
lock (rngLock)
|
||||
{
|
||||
x = rng.Next(0, w);
|
||||
y = rng.Next(0, h);
|
||||
rw = rng.Next(5, 60);
|
||||
rh = rng.Next(5, 60);
|
||||
}
|
||||
|
||||
byte alpha = (byte)rng.Next(100, 255);
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
Color = color.WithAlpha(alpha),
|
||||
IsAntialias = false,
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "Rectangles":
|
||||
canvas.DrawRect(x, y, rw, rh, paint);
|
||||
break;
|
||||
case "Ellipses":
|
||||
canvas.DrawOval(x + rw / 2f, y + rh / 2f, rw / 2f, rh / 2f, paint);
|
||||
break;
|
||||
case "Lines":
|
||||
paint.Style = SKPaintStyle.Stroke;
|
||||
paint.StrokeWidth = 2;
|
||||
canvas.DrawLine(x, y, x + rw, y + rh, paint);
|
||||
break;
|
||||
case "Text":
|
||||
using (var textFont = new SKFont(SKTypeface.Default, 16))
|
||||
{
|
||||
canvas.DrawText("Mg", x, y + 16, SKTextAlign.Left, textFont, paint);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
int shape;
|
||||
lock (rngLock) { shape = rng.Next(0, 4); }
|
||||
if (shape == 0)
|
||||
{
|
||||
canvas.DrawRect(x, y, rw, rh, paint);
|
||||
}
|
||||
else if (shape == 1)
|
||||
{
|
||||
canvas.DrawOval(x + rw / 2f, y + rh / 2f, rw / 2f, rh / 2f, paint);
|
||||
}
|
||||
else if (shape == 2)
|
||||
{
|
||||
paint.Style = SKPaintStyle.Stroke;
|
||||
paint.StrokeWidth = 2;
|
||||
canvas.DrawLine(x, y, x + rw, y + rh, paint);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var textFont2 = new SKFont(SKTypeface.Default, 16);
|
||||
canvas.DrawText("Mg", x, y + 16, SKTextAlign.Left, textFont2, paint);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
using var fpsFont = new SKFont(SKTypeface.Default, 14);
|
||||
using var fpsPaint = new SKPaint
|
||||
{
|
||||
Color = color,
|
||||
IsAntialias = false
|
||||
};
|
||||
canvas.DrawText($"FPS: {fps:F1} | Shapes: {count} | Type: {type}", 8, 18, SKTextAlign.Left, fpsFont, fpsPaint);
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,9 @@ public class WeatherPlugin : AsyncPlugin
|
||||
showTime.OnValueChanged += Application.UpdateWindow;
|
||||
fontSizeSlider.OnValueChanged += Application.UpdateWindow;
|
||||
|
||||
await UpdateLocationAndWeather();
|
||||
Application.UpdateWindow();
|
||||
|
||||
searchButton.OnClick += () =>
|
||||
{
|
||||
isLoading = true;
|
||||
@@ -70,9 +73,6 @@ public class WeatherPlugin : AsyncPlugin
|
||||
Application.UpdateWindow();
|
||||
});
|
||||
};
|
||||
|
||||
await UpdateLocationAndWeather();
|
||||
Application.UpdateWindow();
|
||||
}
|
||||
|
||||
public override async Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<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>
|
||||
@@ -1,182 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
using DesktopMagic.Helpers;
|
||||
using DesktopMagic.Settings;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace DesktopMagic.DataContexts;
|
||||
@@ -14,8 +11,6 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private static DesktopMagicSettings settings = new();
|
||||
private static string? selectedScreenDeviceName;
|
||||
private string? selectedLayoutName;
|
||||
|
||||
private bool isLoading = true;
|
||||
private string? pluginsSearchText;
|
||||
@@ -39,43 +34,6 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ScreenDisplay> Screens { get; } = [];
|
||||
|
||||
public string? SelectedScreenId
|
||||
{
|
||||
get => selectedScreenDeviceName;
|
||||
set
|
||||
{
|
||||
selectedScreenDeviceName = value;
|
||||
UpdateSelection();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
get => selectedLayoutName;
|
||||
set
|
||||
{
|
||||
selectedLayoutName = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => isLoading;
|
||||
@@ -118,163 +76,8 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the list of detected screens and ensures a screen is selected.
|
||||
/// </summary>
|
||||
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++)
|
||||
{
|
||||
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))
|
||||
{
|
||||
ScreenDisplay? byHardwareId = previousSelected is null
|
||||
? null
|
||||
: Screens.FirstOrDefault(screen => screen.HardwareId == previousSelected.HardwareId);
|
||||
SelectedScreenId = byHardwareId?.DeviceName ?? Screens.FirstOrDefault()?.DeviceName;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises change notifications for the currently selected screen's layout.
|
||||
/// </summary>
|
||||
public void RefreshSelection()
|
||||
{
|
||||
UpdateSelection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the manager's screen selection in sync and raises notifications for the selected layout.
|
||||
/// </summary>
|
||||
private void UpdateSelection()
|
||||
{
|
||||
Manager.Instance.SelectedScreenDeviceName = selectedScreenDeviceName;
|
||||
selectedLayoutName = Manager.Instance.SelectedLayout.Name;
|
||||
OnPropertyChanged(nameof(SelectedScreenId));
|
||||
OnPropertyChanged(nameof(SelectedScreen));
|
||||
OnPropertyChanged(nameof(SelectedLayout));
|
||||
OnPropertyChanged(nameof(SelectedLayoutName));
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A detected screen displayed in the UI.
|
||||
/// </summary>
|
||||
public class ScreenDisplay : INotifyPropertyChanged
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,6 @@
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
|
||||
<PackageReference Include="Modio" Version="1.0.0" />
|
||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
||||
<PackageReference Include="SkiaSharp" Version="4.148.0" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
<PackageReference Include="WPF-UI" Version="4.2.0" />
|
||||
<PackageReference Include="WPF-UI.Markdown" Version="4.0.2" />
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<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>
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Helpers for enumerating screens and converting between percentage based
|
||||
/// positions/sizes (relative to a screen's bounds) and absolute positions.
|
||||
/// All conversions work in WPF DIP space because WPF window <see cref="Window.Left"/>,
|
||||
/// <see cref="Window.Top"/>, <see cref="Window.Width"/> and <see cref="Window.Height"/>
|
||||
/// are expressed in device independent pixels, while <see cref="System.Windows.Forms.Screen.Bounds"/>
|
||||
/// is expressed in physical pixels. Each screen is therefore converted to DIP space
|
||||
/// using its own DPI scaling factor.
|
||||
/// </summary>
|
||||
public static class ScreenUtilities
|
||||
{
|
||||
private const uint MonitorDefaultToNearest = 0x00000002;
|
||||
|
||||
private const int MDT_EFFECTIVE_DPI = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of all screens currently connected, in the order reported by Windows.
|
||||
/// </summary>
|
||||
public static List<System.Windows.Forms.Screen> GetAllScreens()
|
||||
{
|
||||
return System.Windows.Forms.Screen.AllScreens.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the screen matching the given device name (e.g. "\\.\DISPLAY1"), or null.
|
||||
/// </summary>
|
||||
public static System.Windows.Forms.Screen? GetScreenByDeviceName(string? deviceName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return System.Windows.Forms.Screen.AllScreens.FirstOrDefault(screen => string.Equals(screen.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary screen, or the first available screen as a fallback.
|
||||
/// </summary>
|
||||
public static System.Windows.Forms.Screen GetPrimaryScreen()
|
||||
{
|
||||
return System.Windows.Forms.Screen.PrimaryScreen ?? System.Windows.Forms.Screen.AllScreens.FirstOrDefault()
|
||||
?? throw new InvalidOperationException("No screens detected.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aspect ratio (Width / Height) of the screen's bounds.
|
||||
/// </summary>
|
||||
public static double GetAspectRatio(System.Windows.Forms.Screen screen)
|
||||
{
|
||||
if (screen.Bounds.Height == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return screen.Bounds.Width / (double)screen.Bounds.Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a percentage based position (0..1 relative to the screen bounds) to an absolute
|
||||
/// WPF position (DIPs) on that screen.
|
||||
/// </summary>
|
||||
public static Point PercentToPosition(Point percent, System.Drawing.Rectangle bounds)
|
||||
{
|
||||
Rect dips = GetScreenDips(bounds);
|
||||
return new Point(
|
||||
dips.Left + (percent.X * dips.Width),
|
||||
dips.Top + (percent.Y * dips.Height));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an absolute WPF position (DIPs) to a percentage (0..1) of the screen bounds.
|
||||
/// </summary>
|
||||
public static Point PositionToPercent(Point position, System.Drawing.Rectangle bounds)
|
||||
{
|
||||
Rect dips = GetScreenDips(bounds);
|
||||
if (dips.Width == 0 || dips.Height == 0)
|
||||
{
|
||||
return new Point(0.05, 0.05);
|
||||
}
|
||||
|
||||
return new Point(
|
||||
(position.X - dips.Left) / dips.Width,
|
||||
(position.Y - dips.Top) / dips.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a percentage based size (0..1 of the screen bounds) to an absolute WPF size (DIPs).
|
||||
/// </summary>
|
||||
public static Point PercentSizeToSize(Point percent, System.Drawing.Rectangle bounds)
|
||||
{
|
||||
Rect dips = GetScreenDips(bounds);
|
||||
return new Point(
|
||||
percent.X * dips.Width,
|
||||
percent.Y * dips.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an absolute WPF size (DIPs) to a percentage (0..1) of the screen bounds.
|
||||
/// </summary>
|
||||
public static Point SizeToPercent(Point size, System.Drawing.Rectangle bounds)
|
||||
{
|
||||
Rect dips = GetScreenDips(bounds);
|
||||
if (dips.Width == 0 || dips.Height == 0)
|
||||
{
|
||||
return new Point(0.3, 0.3);
|
||||
}
|
||||
|
||||
return new Point(
|
||||
size.X / dips.Width,
|
||||
size.Y / dips.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps an absolute WPF position (DIPs) so that the window (of the given size)
|
||||
/// stays within the screen bounds. This prevents widgets from being moved to another screen.
|
||||
/// </summary>
|
||||
public static Point ClampToScreenBounds(Point topLeft, Size size, System.Drawing.Rectangle bounds)
|
||||
{
|
||||
Rect dips = GetScreenDips(bounds);
|
||||
double x = topLeft.X;
|
||||
double y = topLeft.Y;
|
||||
|
||||
if (size.Width <= dips.Width)
|
||||
{
|
||||
x = Math.Clamp(x, dips.Left, dips.Right - size.Width);
|
||||
}
|
||||
else
|
||||
{
|
||||
x = dips.Left;
|
||||
}
|
||||
|
||||
if (size.Height <= dips.Height)
|
||||
{
|
||||
y = Math.Clamp(y, dips.Top, dips.Bottom - size.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
y = dips.Top;
|
||||
}
|
||||
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a human readable label for a screen, e.g. "Display 1 · DELL U2715H · 3840x2160".
|
||||
/// </summary>
|
||||
public static string GetScreenLabel(System.Windows.Forms.Screen screen, int index)
|
||||
{
|
||||
string name = GetFriendlyName(screen);
|
||||
string resolution = $"{screen.Bounds.Width}x{screen.Bounds.Height}";
|
||||
|
||||
return string.IsNullOrWhiteSpace(name) || string.Equals(name, "Generic PnP Monitor", StringComparison.OrdinalIgnoreCase)
|
||||
? $"Display {index + 1} · {resolution}"
|
||||
: $"Display {index + 1} · {name} · {resolution}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the friendly monitor model name (e.g. "DELL U2715H") for the given screen,
|
||||
/// 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
|
||||
{
|
||||
DISPLAY_DEVICE device = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||
if (EnumDisplayDevices(screen.DeviceName, 0, ref device, 0))
|
||||
{
|
||||
DISPLAY_DEVICE monitor = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||
if (EnumDisplayDevices(device.DeviceName, 0, ref monitor, 0))
|
||||
{
|
||||
return monitor;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fall through to null.
|
||||
}
|
||||
|
||||
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>
|
||||
/// Gets the screen bounds converted to WPF DIP space using the screen's own DPI scaling factor.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DPI scaling factor (relative to 96 DPI) of the screen containing the given bounds.
|
||||
/// </summary>
|
||||
private static double GetDpiScale(System.Drawing.Rectangle bounds)
|
||||
{
|
||||
try
|
||||
{
|
||||
POINT center = new POINT
|
||||
{
|
||||
X = bounds.Left + (bounds.Width / 2),
|
||||
Y = bounds.Top + (bounds.Height / 2)
|
||||
};
|
||||
IntPtr monitor = MonitorFromPoint(center, MonitorDefaultToNearest);
|
||||
if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out uint dpiX, out _) == 0)
|
||||
{
|
||||
return dpiX / 96.0;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fall through to no scaling if DPI APIs are unavailable.
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromPoint(POINT point, uint dwFlags);
|
||||
|
||||
[DllImport("shcore.dll")]
|
||||
private static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern bool EnumDisplayDevices(string? lpDevice, uint iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct DISPLAY_DEVICE
|
||||
{
|
||||
public uint cb;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string DeviceName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceString;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceID;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceKey;
|
||||
|
||||
public uint StateFlags;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct POINT
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
MinHeight="600"
|
||||
MinWidth="940"
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowState="Normal"
|
||||
WindowState="Minimized"
|
||||
Loaded="Window_Loaded">
|
||||
|
||||
<Grid>
|
||||
|
||||
@@ -39,8 +39,6 @@ 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
|
||||
@@ -49,18 +47,6 @@ public partial class MainWindow : FluentWindow
|
||||
_manager.LoadPlugins();
|
||||
_manager.LoadLayout();
|
||||
|
||||
if (_manager.Settings.IsFirstRun)
|
||||
{
|
||||
_manager.Settings.IsFirstRun = false;
|
||||
_ = Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowState = WindowState.Minimized;
|
||||
Visibility = Visibility.Collapsed;
|
||||
ShowInTaskbar = false;
|
||||
}
|
||||
|
||||
_manager.IsLoaded = true;
|
||||
_mainWindowDataContext.IsLoading = false;
|
||||
await ShowLatestReleaseInfoAfterUpdateAsync();
|
||||
@@ -101,8 +87,6 @@ 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();
|
||||
@@ -117,16 +101,6 @@ 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++)
|
||||
|
||||
+33
-242
@@ -5,7 +5,6 @@ using DesktopMagic.Settings;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
@@ -36,9 +35,6 @@ public sealed class Manager
|
||||
}
|
||||
}
|
||||
|
||||
// Name of the built-in layout that shows no widgets on a screen.
|
||||
public const string EmptyLayoutName = "Empty";
|
||||
|
||||
// Plugin management
|
||||
private readonly Dictionary<uint, InternalPluginData> _plugins = [];
|
||||
private readonly Dictionary<PluginMetadata, Type> _builtInPlugins = new()
|
||||
@@ -50,8 +46,6 @@ public sealed class Manager
|
||||
{new((string)App.LanguageDictionary["weather"], 5) { Author = "Stone_Red" }, typeof(WeatherPlugin)},
|
||||
{new((string)App.LanguageDictionary["nextMeetingCountdown"], 6) { Author = "Stone_Red" }, typeof(NextMeetingCountdownPlugin)},
|
||||
{new((string)App.LanguageDictionary["agenda"], 7) { Author = "Stone_Red" }, typeof(AgendaPlugin)},
|
||||
{new((string)App.LanguageDictionary["benchmark"], 8) { Author = "Stone_Red" }, typeof(BenchmarkPlugin)},
|
||||
{new((string)App.LanguageDictionary["skiaBenchmark"], 9) { Author = "Stone_Red" }, typeof(SkiaBenchmarkPlugin)},
|
||||
};
|
||||
|
||||
// Window management
|
||||
@@ -69,13 +63,6 @@ public sealed class Manager
|
||||
public DesktopMagicSettings Settings { get; set; } = new();
|
||||
public bool IsLoaded { get; set; } = false;
|
||||
|
||||
// Screen selection (which screen is currently edited in the UI)
|
||||
public string? SelectedScreenDeviceName { get; set; }
|
||||
|
||||
public System.Windows.Forms.Screen SelectedScreen => ScreenUtilities.GetScreenByDeviceName(SelectedScreenDeviceName) ?? ScreenUtilities.GetPrimaryScreen();
|
||||
|
||||
public Layout SelectedLayout => GetLayoutForScreen(SelectedScreen);
|
||||
|
||||
private readonly JsonSerializerOptions _jsonSettingsOptions = new()
|
||||
{
|
||||
Converters = { new ColorJsonConverter() }
|
||||
@@ -148,46 +135,20 @@ public sealed class Manager
|
||||
App.Logger.LogInfo($"Loaded {_plugins.Count} plugins", source: "Manager");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads (or unloads) the given plugin on every screen that currently uses the given layout.
|
||||
/// This keeps screens sharing a layout in sync when plugins are enabled or disabled.
|
||||
/// </summary>
|
||||
public void LoadPlugin(uint pluginId, Layout layout, Action<InternalPluginData>? onPluginLoaded = null)
|
||||
public void LoadPlugin(uint pluginId, Action<InternalPluginData>? onPluginLoaded = null)
|
||||
{
|
||||
if (!_plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||
{
|
||||
pluginSettings = new PluginSettings();
|
||||
layout.Plugins.Add(pluginId, pluginSettings);
|
||||
Settings.CurrentLayout.Plugins.Add(pluginId, pluginSettings);
|
||||
}
|
||||
|
||||
pluginSettings.Metadata = internalPluginData.Metadata;
|
||||
pluginSettings.Owner = layout;
|
||||
|
||||
foreach (System.Windows.Forms.Screen screen in ScreenUtilities.GetAllScreens())
|
||||
{
|
||||
if (GetLayoutForScreen(screen) == layout)
|
||||
{
|
||||
EnsurePluginWindow(screen, layout, internalPluginData, pluginSettings, onPluginLoaded);
|
||||
}
|
||||
}
|
||||
|
||||
layout.UpdatePlugins();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or closes the plugin window for a single screen, based on the plugin settings.
|
||||
/// </summary>
|
||||
private void EnsurePluginWindow(System.Windows.Forms.Screen screen, Layout layout, InternalPluginData internalPluginData, PluginSettings pluginSettings, Action<InternalPluginData>? onPluginLoaded)
|
||||
{
|
||||
string screenDeviceName = screen.DeviceName;
|
||||
Rectangle screenBounds = screen.Bounds;
|
||||
|
||||
IPluginWindow? existingWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id && w.ScreenDeviceName == screenDeviceName);
|
||||
IPluginWindow? existingWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id);
|
||||
|
||||
if (existingWindow is not null || !pluginSettings.Enabled)
|
||||
{
|
||||
@@ -213,21 +174,21 @@ public sealed class Manager
|
||||
|
||||
if (_builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
|
||||
{
|
||||
window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings, screenBounds, screenDeviceName)
|
||||
window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings)
|
||||
{
|
||||
Title = internalPluginData.Metadata.Id.ToString()
|
||||
};
|
||||
}
|
||||
else if (internalPluginData.Type == PluginType.Web)
|
||||
{
|
||||
window = new WebPluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath, screenBounds, screenDeviceName)
|
||||
window = new WebPluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
|
||||
{
|
||||
Title = internalPluginData.Metadata.Id.ToString()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath, screenBounds, screenDeviceName)
|
||||
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
|
||||
{
|
||||
Title = internalPluginData.Metadata.Id.ToString()
|
||||
};
|
||||
@@ -242,21 +203,10 @@ public sealed class Manager
|
||||
|
||||
Action exitHandler = () =>
|
||||
{
|
||||
// Close the widget on every screen using this layout
|
||||
foreach (System.Windows.Forms.Screen sharedScreen in ScreenUtilities.GetAllScreens())
|
||||
{
|
||||
if (GetLayoutForScreen(sharedScreen) == layout)
|
||||
{
|
||||
IPluginWindow? sharedWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id && w.ScreenDeviceName == sharedScreen.DeviceName);
|
||||
if (sharedWindow is not null)
|
||||
{
|
||||
PluginWindows.Remove(sharedWindow);
|
||||
PluginWindows.Remove(window);
|
||||
BlockWindowsClosing = false;
|
||||
sharedWindow.Close();
|
||||
window.Close();
|
||||
BlockWindowsClosing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
pluginSettings.Enabled = false;
|
||||
};
|
||||
|
||||
@@ -274,7 +224,7 @@ public sealed class Manager
|
||||
App.Logger.LogInfo("Reloading plugins", source: "PluginManager");
|
||||
|
||||
LoadPlugins();
|
||||
LoadLayout();
|
||||
LoadLayout(false);
|
||||
}
|
||||
|
||||
public void SetEditMode(bool editMode)
|
||||
@@ -291,32 +241,6 @@ public sealed class Manager
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
private readonly Dictionary<(Layout, uint), SettingSynchronizer> settingSynchronizers = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets (or creates) the synchronizer that keeps the settings of all windows showing the
|
||||
/// given plugin in the given layout in sync.
|
||||
/// </summary>
|
||||
internal SettingSynchronizer GetSettingSynchronizer(Layout layout, uint pluginId)
|
||||
{
|
||||
(Layout, uint) key = (layout, pluginId);
|
||||
if (!settingSynchronizers.TryGetValue(key, out SettingSynchronizer? synchronizer))
|
||||
{
|
||||
synchronizer = new SettingSynchronizer();
|
||||
settingSynchronizers.Add(key, synchronizer);
|
||||
}
|
||||
|
||||
return synchronizer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the synchronizer for the given plugin in the given layout once no windows use it anymore.
|
||||
/// </summary>
|
||||
internal void ReleaseSettingSynchronizer(Layout layout, uint pluginId)
|
||||
{
|
||||
_ = settingSynchronizers.Remove((layout, pluginId));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Settings Management
|
||||
@@ -329,9 +253,7 @@ public sealed class Manager
|
||||
{
|
||||
Settings = new DesktopMagicSettings();
|
||||
Settings.Layouts.Add(new Layout("Default"));
|
||||
Settings.Layouts.Add(new Layout(EmptyLayoutName));
|
||||
Settings.Themes.Add(new Theme("Default"));
|
||||
Settings.SchemaVersion = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -348,62 +270,9 @@ public sealed class Manager
|
||||
Settings.Themes.Add(new Theme("Default"));
|
||||
}
|
||||
|
||||
if (Settings.SchemaVersion < 1)
|
||||
{
|
||||
MigrateToScreenAwareSettings();
|
||||
Settings.SchemaVersion = 1;
|
||||
}
|
||||
|
||||
if (!Settings.Layouts.Any(layout => layout.Name == EmptyLayoutName))
|
||||
{
|
||||
Settings.Layouts.Add(new Layout(EmptyLayoutName));
|
||||
}
|
||||
|
||||
SettingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migrates legacy settings to screen-aware layouts:
|
||||
/// records the primary screen aspect ratio on each layout and converts
|
||||
/// absolute pixel positions/sizes to percentages of the primary screen bounds.
|
||||
/// </summary>
|
||||
private void MigrateToScreenAwareSettings()
|
||||
{
|
||||
App.Logger.LogInfo("Migrating settings to screen-aware layouts", source: "Manager");
|
||||
|
||||
System.Windows.Forms.Screen primaryScreen = ScreenUtilities.GetPrimaryScreen();
|
||||
double aspectRatio = ScreenUtilities.GetAspectRatio(primaryScreen);
|
||||
Rectangle bounds = primaryScreen.Bounds;
|
||||
|
||||
foreach (Layout layout in Settings.Layouts)
|
||||
{
|
||||
if (layout.ScreenAspectRatio <= 0)
|
||||
{
|
||||
layout.ScreenAspectRatio = aspectRatio;
|
||||
}
|
||||
|
||||
foreach (PluginSettings plugin in layout.Plugins.Values)
|
||||
{
|
||||
if (plugin.Position.X > 1 || plugin.Position.Y > 1 || plugin.Position.X < 0 || plugin.Position.Y < 0)
|
||||
{
|
||||
plugin.Position = ScreenUtilities.PositionToPercent(new System.Windows.Point(plugin.Position.X, plugin.Position.Y), bounds);
|
||||
}
|
||||
|
||||
if (plugin.Size.X > 1 || plugin.Size.Y > 1)
|
||||
{
|
||||
plugin.Size = ScreenUtilities.SizeToPercent(new System.Windows.Point(plugin.Size.X, plugin.Size.Y), bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Settings.ScreenLayouts.Count == 0)
|
||||
{
|
||||
Settings.ScreenLayouts[ScreenUtilities.GetMonitorHardwareId(primaryScreen)] = Settings.CurrentLayoutName ?? "Default";
|
||||
}
|
||||
|
||||
App.Logger.LogInfo("Settings migrated to screen-aware layouts", source: "Manager");
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
if (!IsLoaded)
|
||||
@@ -421,60 +290,9 @@ public sealed class Manager
|
||||
|
||||
#region Layout Management
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the layout that should be applied to the given screen:
|
||||
/// 1. the layout explicitly bound to this screen's device name,
|
||||
/// 2. the layout with the closest matching aspect ratio,
|
||||
/// 3. the first layout.
|
||||
/// </summary>
|
||||
public Layout GetLayoutForScreen(System.Windows.Forms.Screen screen)
|
||||
public void LoadLayout(bool minimize = true, Action? onComplete = null)
|
||||
{
|
||||
if (Settings.ScreenLayouts.TryGetValue(ScreenUtilities.GetMonitorHardwareId(screen), out string? layoutName))
|
||||
{
|
||||
Layout? bound = Settings.Layouts.FirstOrDefault(layout => layout.Name == layoutName);
|
||||
if (bound is not null)
|
||||
{
|
||||
return bound;
|
||||
}
|
||||
}
|
||||
|
||||
double targetRatio = ScreenUtilities.GetAspectRatio(screen);
|
||||
Layout? byAspectRatio = Settings.Layouts
|
||||
.Where(layout => layout.ScreenAspectRatio > 0)
|
||||
.OrderBy(layout => Math.Abs(layout.ScreenAspectRatio - targetRatio))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (byAspectRatio is not null)
|
||||
{
|
||||
return byAspectRatio;
|
||||
}
|
||||
|
||||
return Settings.Layouts.FirstOrDefault() ?? new Layout("ERROR");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the given layout to the given screen on this machine.
|
||||
/// </summary>
|
||||
public void BindLayoutToScreen(System.Windows.Forms.Screen screen, Layout layout)
|
||||
{
|
||||
Settings.ScreenLayouts[ScreenUtilities.GetMonitorHardwareId(screen)] = layout.Name;
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all plugin windows currently shown on the given screen.
|
||||
/// </summary>
|
||||
public IEnumerable<IPluginWindow> GetWindowsForScreen(string screenDeviceName)
|
||||
{
|
||||
return PluginWindows.Where(window => window.ScreenDeviceName == screenDeviceName).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all screens' layouts at once, opening the enabled widgets of every screen.
|
||||
/// </summary>
|
||||
public void LoadLayout(Action? onComplete = null)
|
||||
{
|
||||
App.Logger.LogInfo("Loading layouts", source: "Manager");
|
||||
App.Logger.LogInfo("Loading layout", source: "Manager");
|
||||
BlockWindowsClosing = false;
|
||||
|
||||
foreach (IPluginWindow window in PluginWindows)
|
||||
@@ -485,47 +303,7 @@ public sealed class Manager
|
||||
BlockWindowsClosing = true;
|
||||
PluginWindows.Clear();
|
||||
|
||||
foreach (System.Windows.Forms.Screen screen in ScreenUtilities.GetAllScreens())
|
||||
{
|
||||
Layout layout = GetLayoutForScreen(screen);
|
||||
LoadScreen(screen, layout);
|
||||
}
|
||||
|
||||
onComplete?.Invoke();
|
||||
App.Logger.LogInfo("Layouts loaded", source: "Manager");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the widgets of a single screen using the layout currently bound to it.
|
||||
/// </summary>
|
||||
public void ReloadScreen(System.Windows.Forms.Screen screen)
|
||||
{
|
||||
App.Logger.LogInfo($"Reloading screen {screen.DeviceName}", source: "Manager");
|
||||
|
||||
List<IPluginWindow> windows = PluginWindows.Where(window => window.ScreenDeviceName == screen.DeviceName).ToList();
|
||||
|
||||
BlockWindowsClosing = false;
|
||||
foreach (IPluginWindow window in windows)
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
|
||||
BlockWindowsClosing = true;
|
||||
PluginWindows.RemoveAll(window => windows.Contains(window));
|
||||
|
||||
Layout layout = GetLayoutForScreen(screen);
|
||||
LoadScreen(screen, layout);
|
||||
}
|
||||
|
||||
private void LoadScreen(System.Windows.Forms.Screen screen, Layout layout)
|
||||
{
|
||||
App.Logger.LogInfo($"Loading layout \"{layout.Name}\" for screen {screen.DeviceName}", source: "Manager");
|
||||
|
||||
// The empty layout intentionally shows no widgets and is never populated.
|
||||
if (layout.Name == EmptyLayoutName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
bool showWindow = true;
|
||||
|
||||
// Load plugins
|
||||
foreach (uint pluginId in _plugins.Keys)
|
||||
@@ -533,29 +311,42 @@ public sealed class Manager
|
||||
InternalPluginData internalPluginData = _plugins[pluginId];
|
||||
|
||||
// Add plugin to layout if it doesn't exist
|
||||
if (!layout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||
{
|
||||
layout.Plugins.Add(pluginId, new PluginSettings() { Metadata = internalPluginData.Metadata, Owner = layout });
|
||||
Settings.CurrentLayout.Plugins.Add(pluginId, new PluginSettings() { Metadata = internalPluginData.Metadata });
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginSettings.Metadata = internalPluginData.Metadata;
|
||||
pluginSettings.Owner = layout;
|
||||
|
||||
if (pluginSettings.Enabled)
|
||||
{
|
||||
EnsurePluginWindow(screen, layout, internalPluginData, pluginSettings, null);
|
||||
LoadPlugin(pluginId);
|
||||
}
|
||||
|
||||
if (showWindow && pluginSettings.Enabled)
|
||||
{
|
||||
showWindow = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove plugins that are not loaded anymore
|
||||
List<uint> pluginIdsToRemove = layout.Plugins.Keys.Where(id => !_plugins.ContainsKey(id)).ToList();
|
||||
List<uint> pluginIdsToRemove = Settings.CurrentLayout.Plugins.Keys.Where(id => !_plugins.ContainsKey(id)).ToList();
|
||||
foreach (uint pluginId in pluginIdsToRemove)
|
||||
{
|
||||
layout.Plugins.Remove(pluginId);
|
||||
Settings.CurrentLayout.Plugins.Remove(pluginId);
|
||||
}
|
||||
|
||||
layout.UpdatePlugins();
|
||||
Settings.CurrentLayout.UpdatePlugins();
|
||||
|
||||
if (minimize && !showWindow)
|
||||
{
|
||||
Application.Current.MainWindow.WindowState = WindowState.Minimized;
|
||||
Application.Current.MainWindow.ShowInTaskbar = false;
|
||||
}
|
||||
|
||||
onComplete?.Invoke();
|
||||
App.Logger.LogInfo("Layout loaded", source: "Manager");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</ui:Card>
|
||||
|
||||
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel" Grid.Row="1" VerticalAlignment="Stretch" VerticalContentAlignment="Top">
|
||||
<ItemsControl x:Name="pluginsItemsControl" ItemsSource="{Binding SelectedLayout.Plugins}">
|
||||
<ItemsControl x:Name="pluginsItemsControl" ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
|
||||
@@ -69,28 +69,7 @@
|
||||
</ScrollViewer>
|
||||
|
||||
<ui:Card Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="5" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<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" />
|
||||
|
||||
<Grid Grid.Row="2" Grid.ColumnSpan="3">
|
||||
<Grid >
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
@@ -99,9 +78,9 @@
|
||||
<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>
|
||||
<ComboBox x:Name="layoutsComboBox" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding Settings.CurrentLayoutName}" SelectedValuePath="Name" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="4" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
||||
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Grid.Column="2" Icon="{ui:SymbolIcon Add24}" 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>
|
||||
</ui:Card>
|
||||
</Grid>
|
||||
|
||||
@@ -20,6 +20,7 @@ public partial class MainPage : Page
|
||||
{
|
||||
private readonly Manager _manager = Manager.Instance;
|
||||
private readonly MainWindowDataContext _dataContext;
|
||||
private bool _isLoadingLayout = false;
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
@@ -44,8 +45,6 @@ public partial class MainPage : Page
|
||||
{
|
||||
// Initialize edit checkbox state
|
||||
editCheckBox.IsChecked = _manager.IsEditMode;
|
||||
|
||||
_dataContext.RefreshScreens();
|
||||
}
|
||||
|
||||
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
|
||||
@@ -77,16 +76,6 @@ 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)
|
||||
@@ -96,7 +85,7 @@ public partial class MainPage : Page
|
||||
|
||||
uint pluginId = uint.Parse(checkBox.Tag.ToString()!);
|
||||
|
||||
_manager.LoadPlugin(pluginId, _manager.SelectedLayout, (internalPluginData) =>
|
||||
_manager.LoadPlugin(pluginId, (internalPluginData) =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
@@ -156,46 +145,32 @@ public partial class MainPage : Page
|
||||
|
||||
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
ApplySelectedLayout();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the currently selected layout to the currently selected screen and reloads it.
|
||||
/// No-op when the layout is already the one bound to the screen (e.g. programmatic resets).
|
||||
/// </summary>
|
||||
private void ApplySelectedLayout()
|
||||
{
|
||||
if (_dataContext.SelectedScreenId is null)
|
||||
// Prevent recursive calls and only process if fully loaded
|
||||
if (_isLoadingLayout || !_manager.IsLoaded || !IsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Windows.Forms.Screen? screen = ScreenUtilities.GetScreenByDeviceName(_dataContext.SelectedScreenId);
|
||||
if (screen is null)
|
||||
// Check if this is actually a user-initiated change
|
||||
// by verifying that the removed and added items are different
|
||||
if (e.RemovedItems.Count > 0 && e.AddedItems.Count > 0)
|
||||
{
|
||||
if (e.RemovedItems[0] == e.AddedItems[0])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? layoutName = _dataContext.SelectedLayoutName;
|
||||
if (layoutName is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Layout? layout = _manager.Settings.Layouts.FirstOrDefault(l => l.Name == layoutName);
|
||||
if (layout is null)
|
||||
try
|
||||
{
|
||||
return;
|
||||
_isLoadingLayout = true;
|
||||
_manager.SaveSettings();
|
||||
_manager.LoadLayout(false);
|
||||
}
|
||||
|
||||
if (_manager.GetLayoutForScreen(screen) == layout)
|
||||
finally
|
||||
{
|
||||
return;
|
||||
_isLoadingLayout = false;
|
||||
}
|
||||
|
||||
_manager.BindLayoutToScreen(screen, layout);
|
||||
_manager.ReloadScreen(screen);
|
||||
_dataContext.RefreshSelection();
|
||||
}
|
||||
|
||||
private async void NewLayoutButton_Click(object sender, RoutedEventArgs e)
|
||||
@@ -219,11 +194,18 @@ public partial class MainPage : Page
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isLoadingLayout = true;
|
||||
_manager.Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
|
||||
_manager.Settings.CurrentLayoutName = inputDialog.ResponseText.Trim();
|
||||
_manager.SaveSettings();
|
||||
|
||||
// Select the new layout so the user can apply it to the current screen
|
||||
_dataContext.SelectedLayoutName = inputDialog.ResponseText.Trim();
|
||||
_manager.LoadLayout(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingLayout = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,40 +237,17 @@ public partial class MainPage : Page
|
||||
return;
|
||||
}
|
||||
|
||||
Layout? layout = _manager.Settings.Layouts.FirstOrDefault(l => l.Name == _dataContext.SelectedLayoutName);
|
||||
if (layout is null)
|
||||
try
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (layout.Name == Manager.EmptyLayoutName)
|
||||
{
|
||||
Wpf.Ui.Controls.MessageBox cannotDeleteMessageBox = new Wpf.Ui.Controls.MessageBox
|
||||
{
|
||||
Title = App.AppName,
|
||||
Content = (string)FindResource("cannotDeleteEmptyLayout"),
|
||||
CloseButtonText = "Ok"
|
||||
};
|
||||
_ = await cannotDeleteMessageBox.ShowDialogAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_ = _manager.Settings.Layouts.Remove(layout);
|
||||
|
||||
// Remove any screen bindings pointing to the deleted layout
|
||||
List<string> boundScreens = _manager.Settings.ScreenLayouts
|
||||
.Where(kvp => kvp.Value == layout.Name)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (string screenId in boundScreens)
|
||||
{
|
||||
_ = _manager.Settings.ScreenLayouts.Remove(screenId);
|
||||
}
|
||||
|
||||
_isLoadingLayout = true;
|
||||
_ = _manager.Settings.Layouts.Remove(_manager.Settings.CurrentLayout);
|
||||
_manager.SaveSettings();
|
||||
_manager.LoadLayout();
|
||||
_dataContext.RefreshSelection();
|
||||
_manager.LoadLayout(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingLayout = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:ListView x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" ItemsSource="{Binding Settings.Themes}" SelectedValue="{Binding SelectedLayout.CurrentThemeName}" SelectedValuePath="Name" SelectedIndex="2" SelectionMode="Single">
|
||||
<ui:ListView x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" ItemsSource="{Binding Settings.Themes}" SelectedValue="{Binding Settings.CurrentLayout.CurrentThemeName}" SelectedValuePath="Name" SelectedIndex="2" SelectionMode="Single">
|
||||
<ui:ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
|
||||
@@ -69,7 +69,7 @@ public partial class ThemePage : Page
|
||||
}
|
||||
|
||||
_manager.Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
|
||||
_manager.SelectedLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
|
||||
_manager.Settings.CurrentLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
|
||||
_manager.SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ public interface IPluginWindow
|
||||
PluginMetadata PluginMetadata { get; }
|
||||
string PluginFolderPath { get; }
|
||||
string Title { get; set; }
|
||||
string ScreenDeviceName { get; }
|
||||
|
||||
void Exit();
|
||||
void SetEditMode(bool enabled);
|
||||
@@ -21,8 +20,5 @@ public interface IPluginWindow
|
||||
void Hide();
|
||||
void Close();
|
||||
|
||||
void ApplySettingValue(string id, string value);
|
||||
void ApplyButtonClick(string id);
|
||||
|
||||
event System.ComponentModel.CancelEventHandler Closing;
|
||||
}
|
||||
|
||||
@@ -40,12 +40,13 @@ public partial class PluginManager : Page
|
||||
private readonly string pluginDevelopmentPath = Path.Combine(App.ApplicationDataPath, "PluginDevelopment");
|
||||
private readonly Manager _manager = Manager.Instance;
|
||||
|
||||
private bool changed = false;
|
||||
|
||||
private readonly DispatcherTimer searchTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(300),
|
||||
};
|
||||
|
||||
private bool changed = false;
|
||||
private Client modIoClient;
|
||||
|
||||
public PluginManager()
|
||||
@@ -79,70 +80,6 @@ public partial class PluginManager : Page
|
||||
Unloaded += PluginManager_Unloaded;
|
||||
}
|
||||
|
||||
public async Task Remove(string pluginPath, uint id)
|
||||
{
|
||||
App.Logger.LogInfo($"Removing plugin with ID {id} from path: {pluginPath}", source: "PluginManager");
|
||||
pluginManagerDataContext.IsLoading = true;
|
||||
changed = true;
|
||||
|
||||
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
|
||||
|
||||
if (Directory.Exists(pluginPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(pluginPath, true);
|
||||
App.Logger.LogInfo($"Successfully deleted plugin directory: {pluginPath}", source: "PluginManager");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
|
||||
{
|
||||
Title = "Plugin Manager",
|
||||
Content = ex.Message,
|
||||
CloseButtonText = "Ok"
|
||||
};
|
||||
_ = await messageBox.ShowDialogAsync();
|
||||
App.Logger.LogError(ex.Message, source: "PluginManager");
|
||||
}
|
||||
}
|
||||
|
||||
if (pluginEntryDataContext is not null)
|
||||
{
|
||||
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
|
||||
App.Logger.LogInfo($"Removed plugin {id} from installed plugins list", source: "PluginManager");
|
||||
}
|
||||
|
||||
if (pluginManagerDataContext.IsAuthenticated)
|
||||
{
|
||||
try
|
||||
{
|
||||
await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
|
||||
App.Logger.LogInfo($"Unsubscribed from plugin {id} on mod.io", source: "PluginManager");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
App.Logger.LogError($"Failed to unsubscribe from plugin {id}: {ex.Message}", source: "PluginManager");
|
||||
}
|
||||
}
|
||||
|
||||
pluginManagerDataContext.IsLoading = false;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"[^a-zA-Z0-9]")]
|
||||
private static partial Regex IdentifierNameRegex();
|
||||
|
||||
private static string GetPluginSafeName(string pluginName)
|
||||
{
|
||||
string pluginSafeName = pluginName.ToLower().Replace("_", " ");
|
||||
|
||||
TextInfo info = CultureInfo.CurrentCulture.TextInfo;
|
||||
pluginSafeName = info.ToTitleCase(pluginSafeName);
|
||||
pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
|
||||
|
||||
return pluginSafeName;
|
||||
}
|
||||
|
||||
private async void PluginManager_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await InitializePluginManager();
|
||||
@@ -242,6 +179,59 @@ public partial class PluginManager : Page
|
||||
App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
|
||||
}
|
||||
|
||||
public async Task Remove(string pluginPath, uint id)
|
||||
{
|
||||
App.Logger.LogInfo($"Removing plugin with ID {id} from path: {pluginPath}", source: "PluginManager");
|
||||
pluginManagerDataContext.IsLoading = true;
|
||||
changed = true;
|
||||
|
||||
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
|
||||
|
||||
if (Directory.Exists(pluginPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(pluginPath, true);
|
||||
App.Logger.LogInfo($"Successfully deleted plugin directory: {pluginPath}", source: "PluginManager");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
|
||||
{
|
||||
Title = "Plugin Manager",
|
||||
Content = ex.Message,
|
||||
CloseButtonText = "Ok"
|
||||
};
|
||||
_ = await messageBox.ShowDialogAsync();
|
||||
App.Logger.LogError(ex.Message, source: "PluginManager");
|
||||
}
|
||||
}
|
||||
|
||||
if (pluginEntryDataContext is not null)
|
||||
{
|
||||
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
|
||||
App.Logger.LogInfo($"Removed plugin {id} from installed plugins list", source: "PluginManager");
|
||||
}
|
||||
|
||||
if (pluginManagerDataContext.IsAuthenticated)
|
||||
{
|
||||
try
|
||||
{
|
||||
await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
|
||||
App.Logger.LogInfo($"Unsubscribed from plugin {id} on mod.io", source: "PluginManager");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
App.Logger.LogError($"Failed to unsubscribe from plugin {id}: {ex.Message}", source: "PluginManager");
|
||||
}
|
||||
}
|
||||
|
||||
pluginManagerDataContext.IsLoading = false;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"[^a-zA-Z0-9]")]
|
||||
private static partial Regex IdentifierNameRegex();
|
||||
|
||||
private async Task Install(Mod mod)
|
||||
{
|
||||
App.Logger.LogInfo($"Installing plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
|
||||
@@ -807,6 +797,17 @@ public class {pluginSafeName}Plugin : Plugin
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetPluginSafeName(string pluginName)
|
||||
{
|
||||
string pluginSafeName = pluginName.ToLower().Replace("_", " ");
|
||||
|
||||
TextInfo info = CultureInfo.CurrentCulture.TextInfo;
|
||||
pluginSafeName = info.ToTitleCase(pluginSafeName);
|
||||
pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
|
||||
|
||||
return pluginSafeName;
|
||||
}
|
||||
|
||||
private string GetCsprojPath(string pluginName)
|
||||
{
|
||||
string pluginSafeName = GetPluginSafeName(pluginName);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<RectangleGeometry x:Name="rectangleGeometry" RadiusX="{Binding ElementName=border, Path=CornerRadius.TopLeft}" RadiusY="{Binding ElementName=border, Path=CornerRadius.TopLeft}" />
|
||||
</Viewbox.Clip>
|
||||
<Border x:Name="imageBorder" BorderBrush="#BAFFEF">
|
||||
<Image x:Name="image" RenderOptions.EdgeMode="Aliased" Margin="0,0,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5" Stretch="Uniform" Focusable="True" MouseDown="Image_MouseDown" MouseMove="Image_MouseMove" MouseWheel="Image_MouseWheel" KeyDown="Image_KeyDown" KeyUp="Image_KeyUp" />
|
||||
<Image x:Name="image" RenderOptions.EdgeMode="Aliased" Margin="0,0,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5" Stretch="Uniform" MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" MouseWheel="Window_MouseWheel" />
|
||||
</Border>
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
|
||||
@@ -6,13 +6,8 @@ using DesktopMagic.Helpers;
|
||||
using DesktopMagic.Plugins;
|
||||
using DesktopMagic.Settings;
|
||||
|
||||
using SkiaSharp;
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
@@ -42,11 +37,6 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
private Plugin? pluginClassInstance;
|
||||
private AssemblyLoadContext assemblyLoadContext;
|
||||
|
||||
private readonly Rectangle screenBounds;
|
||||
private readonly string screenDeviceName;
|
||||
private bool isUpdatingPosition = false;
|
||||
private bool _editMode = false;
|
||||
|
||||
private CancellationTokenSource? pluginCancellationTokenSource;
|
||||
private FileSystemWatcher? pluginFileWatcher;
|
||||
private System.Timers.Timer? reloadDebounceTimer;
|
||||
@@ -55,25 +45,11 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
private WriteableBitmap? writeableBitmap;
|
||||
private BitmapScalingMode lastBitmapScalingMode = BitmapScalingMode.Unspecified;
|
||||
|
||||
// Event handlers on long-lived settings objects, tracked so they can be unsubscribed on close.
|
||||
private readonly PropertyChangedEventHandler settingsPropertyChangedHandler;
|
||||
private readonly PropertyChangedEventHandler themePropertyChangedHandler;
|
||||
private Theme? subscribedTheme;
|
||||
private NotifyCollectionChangedEventHandler? themesCollectionChangedHandler;
|
||||
private readonly List<(Setting Setting, Action Handler)> subscribedSettings = [];
|
||||
private readonly List<(Setting Setting, Action Handler)> defaultSettingsSubscriptions = [];
|
||||
private readonly List<(Button Button, Action Handler)> subscribedButtonClicks = [];
|
||||
|
||||
private readonly ConcurrentDictionary<string, Setting> localSettings = [];
|
||||
private SettingSynchronizer? synchronizer;
|
||||
private bool suppressButtonSync = false;
|
||||
|
||||
public bool IsRunning { get; private set; } = true;
|
||||
public PluginMetadata PluginMetadata { get; private set; }
|
||||
public string PluginFolderPath { get; private set; }
|
||||
public string ScreenDeviceName => screenDeviceName;
|
||||
|
||||
public PluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath, Rectangle screenBounds, string screenDeviceName)
|
||||
public PluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -93,47 +69,33 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
|
||||
Owner = w;
|
||||
|
||||
settingsPropertyChangedHandler = (_, s) =>
|
||||
settings.PropertyChanged += (e, s) =>
|
||||
{
|
||||
if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
|
||||
{
|
||||
SubscribeToTheme(settings.Theme);
|
||||
settings.Theme.PropertyChanged += (se, ev) =>
|
||||
{
|
||||
ThemeChanged();
|
||||
};
|
||||
ThemeChanged();
|
||||
}
|
||||
else if (s.PropertyName == nameof(PluginSettings.Position))
|
||||
{
|
||||
UpdatePosition();
|
||||
}
|
||||
else if (s.PropertyName == nameof(PluginSettings.Size))
|
||||
{
|
||||
UpdateSize();
|
||||
}
|
||||
};
|
||||
settings.PropertyChanged += settingsPropertyChangedHandler;
|
||||
|
||||
themePropertyChangedHandler = (_, _) => ThemeChanged();
|
||||
SubscribeToTheme(settings.Theme);
|
||||
settings.Theme.PropertyChanged += (se, ev) =>
|
||||
{
|
||||
ThemeChanged();
|
||||
};
|
||||
|
||||
PluginMetadata = pluginMetadata;
|
||||
this.settings = settings;
|
||||
this.screenBounds = screenBounds;
|
||||
this.screenDeviceName = screenDeviceName;
|
||||
|
||||
System.Windows.Point position = ScreenUtilities.PercentToPosition(settings.Position, screenBounds);
|
||||
System.Windows.Point size = ScreenUtilities.PercentSizeToSize(settings.Size, screenBounds);
|
||||
Left = position.X;
|
||||
Top = position.Y;
|
||||
Width = size.X;
|
||||
Height = size.Y;
|
||||
Left = settings.Position.X;
|
||||
Top = settings.Position.Y;
|
||||
Width = settings.Size.X;
|
||||
Height = settings.Size.Y;
|
||||
|
||||
PluginFolderPath = pluginFolderPath;
|
||||
|
||||
if (settings.Owner is not null)
|
||||
{
|
||||
synchronizer = Manager.Instance.GetSettingSynchronizer(settings.Owner, PluginMetadata.Id);
|
||||
synchronizer.Register(this);
|
||||
}
|
||||
|
||||
assemblyLoadContext = CreateAssemblyLoadContext();
|
||||
|
||||
// Initialize hot reload watcher if plugin supports unloading and is external
|
||||
@@ -143,27 +105,11 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
}
|
||||
}
|
||||
|
||||
public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings, Rectangle screenBounds, string screenDeviceName) : this(pluginMetadata, settings, string.Empty, screenBounds, screenDeviceName)
|
||||
public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings) : this(pluginMetadata, settings, string.Empty)
|
||||
{
|
||||
this.pluginClassInstance = pluginClassInstance;
|
||||
}
|
||||
|
||||
private void SubscribeToTheme(Theme theme)
|
||||
{
|
||||
if (ReferenceEquals(subscribedTheme, theme))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscribedTheme is not null)
|
||||
{
|
||||
subscribedTheme.PropertyChanged -= themePropertyChangedHandler;
|
||||
}
|
||||
|
||||
subscribedTheme = theme;
|
||||
subscribedTheme.PropertyChanged += themePropertyChangedHandler;
|
||||
}
|
||||
|
||||
private AssemblyLoadContext CreateAssemblyLoadContext()
|
||||
{
|
||||
AssemblyLoadContext context = new(PluginMetadata.Name, isCollectible: true);
|
||||
@@ -172,10 +118,11 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
string assemblyPath = Path.Combine(PluginFolderPath, assemblyName.Name + ".dll");
|
||||
if (File.Exists(assemblyPath))
|
||||
{
|
||||
byte[] assemblyData = File.ReadAllBytes(assemblyPath);
|
||||
using MemoryStream assemblyStream = new(assemblyData);
|
||||
|
||||
return ctx.LoadFromStream(assemblyStream);
|
||||
return ctx.LoadFromAssemblyPath(assemblyPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = ctx.LoadFromAssemblyName(assemblyName);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -359,8 +306,6 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
|
||||
public void SetEditMode(bool enabled)
|
||||
{
|
||||
_editMode = enabled;
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
Topmost = true;
|
||||
@@ -536,7 +481,7 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll");
|
||||
}
|
||||
|
||||
Type? instanceType = Array.Find(dll.GetTypes(), type => type.IsAssignableTo(typeof(Plugin)));
|
||||
Type? instanceType = Array.Find(dll.GetTypes(), type => type.GetTypeInfo().BaseType == typeof(Plugin));
|
||||
|
||||
if (instanceType is null)
|
||||
{
|
||||
@@ -617,18 +562,14 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
SetThemeOverride();
|
||||
SetThemeOverrideItems();
|
||||
|
||||
SubscribeToDefaultSetting(pluginClassInstance.horizontalAlignment, SetHorizontalAlignment);
|
||||
SubscribeToDefaultSetting(pluginClassInstance.verticalAlignment, SetVerticalAlignment);
|
||||
SubscribeToDefaultSetting(pluginClassInstance.windowLayer, SetWindowLayer);
|
||||
SubscribeToDefaultSetting(pluginClassInstance.rotation, SetRotation);
|
||||
SubscribeToDefaultSetting(pluginClassInstance.themeOverride, SetThemeOverride);
|
||||
pluginClassInstance.horizontalAlignment.OnValueChanged += SetHorizontalAlignment;
|
||||
pluginClassInstance.verticalAlignment.OnValueChanged += SetVerticalAlignment;
|
||||
pluginClassInstance.windowLayer.OnValueChanged += SetWindowLayer;
|
||||
pluginClassInstance.rotation.OnValueChanged += SetRotation;
|
||||
pluginClassInstance.themeOverride.OnValueChanged += SetThemeOverride;
|
||||
|
||||
if (themesCollectionChangedHandler is null)
|
||||
{
|
||||
themesCollectionChangedHandler = (_, _) => SetThemeOverrideItems();
|
||||
DesktopMagicSettings desktopMagicSettings = MainWindowDataContext.GetSettings();
|
||||
desktopMagicSettings.Themes.CollectionChanged += themesCollectionChangedHandler;
|
||||
}
|
||||
desktopMagicSettings.Themes.CollectionChanged += (s, e) => SetThemeOverrideItems();
|
||||
});
|
||||
|
||||
void SetVerticalAlignment()
|
||||
@@ -660,16 +601,9 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
}
|
||||
|
||||
void SetWindowLayer()
|
||||
{
|
||||
if (_editMode)
|
||||
{
|
||||
Topmost = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowPos.SetWindowLayer(this, pluginClassInstance.windowLayer.Value);
|
||||
}
|
||||
}
|
||||
|
||||
void SetRotation()
|
||||
{
|
||||
@@ -702,50 +636,6 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeToDefaultSetting(Setting setting, Action handler)
|
||||
{
|
||||
setting.OnValueChanged += handler;
|
||||
defaultSettingsSubscriptions.Add((setting, handler));
|
||||
}
|
||||
|
||||
private void OnPluginSettingValueChanged(Setting setting, string id)
|
||||
{
|
||||
pluginClassInstance?.OnSettingsChanged();
|
||||
|
||||
if (pluginClassInstance?.UpdateInterval is 0 or > 500)
|
||||
{
|
||||
pluginClassInstance.Application.UpdateWindow();
|
||||
}
|
||||
|
||||
synchronizer?.SettingChanged(this, id, setting.GetJsonValue());
|
||||
}
|
||||
|
||||
public void ApplySettingValue(string id, string value)
|
||||
{
|
||||
if (localSettings.TryGetValue(id, out Setting? setting) && setting.GetJsonValue() != value)
|
||||
{
|
||||
setting.SetJsonValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyButtonClick(string id)
|
||||
{
|
||||
if (!localSettings.TryGetValue(id, out Setting? setting) || setting is not Button button)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
suppressButtonSync = true;
|
||||
try
|
||||
{
|
||||
button.Click();
|
||||
}
|
||||
finally
|
||||
{
|
||||
suppressButtonSync = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadOptions(object instance)
|
||||
{
|
||||
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin options", source: "Plugin");
|
||||
@@ -763,36 +653,23 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
{
|
||||
if (attribute is SettingAttribute elementAttribute)
|
||||
{
|
||||
localSettings[elementAttribute.Id] = element;
|
||||
|
||||
SettingElement settingElement = new SettingElement(element, elementAttribute.Id, elementAttribute.Name, elementAttribute.OrderIndex);
|
||||
|
||||
if (settings.Settings.Exists(e => e.Id == elementAttribute.Id))
|
||||
{
|
||||
SettingElement settingsSettingElement = settings.Settings.First(e => e.Id == elementAttribute.Id);
|
||||
string savedValue = settingsSettingElement.JsonValue;
|
||||
if (!string.IsNullOrEmpty(savedValue) || element is not Label and not Button)
|
||||
{
|
||||
settingElement.JsonValue = savedValue;
|
||||
}
|
||||
settingElement.JsonValue = settingsSettingElement.JsonValue;
|
||||
}
|
||||
|
||||
Action valueChangedHandler = () => OnPluginSettingValueChanged(element, elementAttribute.Id);
|
||||
element.OnValueChanged += valueChangedHandler;
|
||||
subscribedSettings.Add((element, valueChangedHandler));
|
||||
element.OnValueChanged += () =>
|
||||
{
|
||||
pluginClassInstance?.OnSettingsChanged();
|
||||
|
||||
if (element is Button button)
|
||||
if (pluginClassInstance?.UpdateInterval is 0 or > 500)
|
||||
{
|
||||
Action clickHandler = () =>
|
||||
{
|
||||
if (!suppressButtonSync)
|
||||
{
|
||||
synchronizer?.ButtonClicked(this, elementAttribute.Id);
|
||||
pluginClassInstance.Application.UpdateWindow();
|
||||
}
|
||||
};
|
||||
button.OnClick += clickHandler;
|
||||
subscribedButtonClicks.Add((button, clickHandler));
|
||||
}
|
||||
|
||||
settingElements.Add(settingElement);
|
||||
break;
|
||||
@@ -953,39 +830,34 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
|
||||
private async void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs? e)
|
||||
{
|
||||
// Capture the fields into locals so an in-flight tick keeps the plugin assembly
|
||||
// alive and stays safe when the window is being closed or the plugin reloaded.
|
||||
Plugin? plugin = pluginClassInstance;
|
||||
System.Timers.Timer? timer = updateTimer;
|
||||
|
||||
if (!IsRunning || plugin is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (plugin is SkiaPlugin or SkiaAsyncPlugin)
|
||||
{
|
||||
await RenderSkiaFrame();
|
||||
}
|
||||
else
|
||||
if (IsRunning && pluginClassInstance is not null)
|
||||
{
|
||||
Bitmap? result;
|
||||
|
||||
if (plugin is AsyncPlugin asyncPlugin)
|
||||
if (pluginClassInstance is AsyncPlugin asyncPlugin)
|
||||
{
|
||||
CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
result = await asyncPlugin.MainAsync(token);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = plugin.Main();
|
||||
result = pluginClassInstance.Main();
|
||||
}
|
||||
|
||||
if (pluginClassInstance.UpdateInterval > 0)
|
||||
{
|
||||
updateTimer!.Interval = pluginClassInstance.UpdateInterval;
|
||||
}
|
||||
else
|
||||
{
|
||||
updateTimer!.Stop();
|
||||
}
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
BitmapScalingMode renderOptions = plugin.RenderQuality switch
|
||||
BitmapScalingMode renderOptions = pluginClassInstance.RenderQuality switch
|
||||
{
|
||||
RenderQuality.High => BitmapScalingMode.HighQuality,
|
||||
RenderQuality.Low => BitmapScalingMode.LowQuality,
|
||||
@@ -996,30 +868,9 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
UpdateImageFromBitmap(result, renderOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (timer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (plugin.UpdateInterval > 0)
|
||||
{
|
||||
timer.Interval = plugin.UpdateInterval;
|
||||
}
|
||||
else
|
||||
{
|
||||
timer.Stop();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If the window is closing, the plugin state is being torn down and the
|
||||
// exception is a teardown race - ignore it instead of showing an error.
|
||||
if (!IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsRunning = false;
|
||||
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
|
||||
_ = await Dispatcher.InvokeAsync(async () =>
|
||||
@@ -1033,74 +884,13 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
_ = await messageBox.ShowDialogAsync();
|
||||
});
|
||||
Exit();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RenderSkiaFrame()
|
||||
{
|
||||
Plugin? plugin = pluginClassInstance;
|
||||
|
||||
if (plugin is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
if (!IsRunning)
|
||||
{
|
||||
width = Math.Max(1, (int)ActualWidth);
|
||||
height = Math.Max(1, (int)ActualHeight);
|
||||
});
|
||||
|
||||
using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul));
|
||||
surface.Canvas.Clear(SKColors.Transparent);
|
||||
|
||||
if (plugin is SkiaAsyncPlugin skiaAsyncPlugin)
|
||||
{
|
||||
CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
await skiaAsyncPlugin.MainAsync(surface.Canvas, token);
|
||||
updateTimer!.Stop();
|
||||
}
|
||||
else if (plugin is SkiaPlugin skiaPlugin)
|
||||
{
|
||||
skiaPlugin.Main(surface.Canvas);
|
||||
}
|
||||
|
||||
SKPixmap? pixmap = surface.PeekPixels();
|
||||
|
||||
if (pixmap is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (writeableBitmap is null || writeableBitmap.PixelWidth != width || writeableBitmap.PixelHeight != height)
|
||||
{
|
||||
writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
|
||||
image.Source = writeableBitmap;
|
||||
}
|
||||
|
||||
writeableBitmap.Lock();
|
||||
try
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
pixmap.GetPixels().ToPointer(),
|
||||
writeableBitmap.BackBuffer.ToPointer(),
|
||||
writeableBitmap.BackBufferStride * height,
|
||||
pixmap.RowBytes * pixmap.Height);
|
||||
}
|
||||
|
||||
writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
|
||||
}
|
||||
finally
|
||||
{
|
||||
writeableBitmap.Unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void Border_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
@@ -1131,148 +921,26 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
reloadDebounceTimer?.Dispose();
|
||||
reloadDebounceTimer = null;
|
||||
|
||||
updateTimer?.Stop();
|
||||
updateTimer?.Dispose();
|
||||
updateTimer = null;
|
||||
|
||||
// Stop plugin using the shared method (no need to await in synchronous event handler)
|
||||
_ = StopPlugin(unloadAssembly: true);
|
||||
|
||||
UnsubscribeEvents();
|
||||
DetachSettings();
|
||||
|
||||
if (synchronizer is not null && settings.Owner is not null)
|
||||
{
|
||||
if (synchronizer.Unregister(this))
|
||||
{
|
||||
Manager.Instance.ReleaseSettingSynchronizer(settings.Owner, PluginMetadata.Id);
|
||||
}
|
||||
synchronizer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void DetachSettings()
|
||||
{
|
||||
foreach (SettingElement settingElement in settings.Settings)
|
||||
{
|
||||
string value = settingElement.JsonValue;
|
||||
settingElement.Input = null;
|
||||
settingElement.JsonValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeEvents()
|
||||
{
|
||||
settings.PropertyChanged -= settingsPropertyChangedHandler;
|
||||
|
||||
if (subscribedTheme is not null)
|
||||
{
|
||||
subscribedTheme.PropertyChanged -= themePropertyChangedHandler;
|
||||
subscribedTheme = null;
|
||||
}
|
||||
|
||||
if (themesCollectionChangedHandler is not null)
|
||||
{
|
||||
MainWindowDataContext.GetSettings().Themes.CollectionChanged -= themesCollectionChangedHandler;
|
||||
themesCollectionChangedHandler = null;
|
||||
}
|
||||
|
||||
foreach ((Setting Setting, Action Handler) subscription in subscribedSettings)
|
||||
{
|
||||
subscription.Setting.OnValueChanged -= subscription.Handler;
|
||||
}
|
||||
subscribedSettings.Clear();
|
||||
|
||||
foreach ((Setting Setting, Action Handler) subscription in defaultSettingsSubscriptions)
|
||||
{
|
||||
subscription.Setting.OnValueChanged -= subscription.Handler;
|
||||
}
|
||||
defaultSettingsSubscriptions.Clear();
|
||||
|
||||
foreach ((Button Button, Action Handler) subscription in subscribedButtonClicks)
|
||||
{
|
||||
subscription.Button.OnClick -= subscription.Handler;
|
||||
}
|
||||
subscribedButtonClicks.Clear();
|
||||
|
||||
localSettings.Clear();
|
||||
}
|
||||
|
||||
private void UpdatePosition()
|
||||
{
|
||||
if (isUpdatingPosition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Windows.Point position = ScreenUtilities.PercentToPosition(settings.Position, screenBounds);
|
||||
if (position == new System.Windows.Point(Left, Top))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdatingPosition = true;
|
||||
Left = position.X;
|
||||
Top = position.Y;
|
||||
isUpdatingPosition = false;
|
||||
}
|
||||
|
||||
private void UpdateSize()
|
||||
{
|
||||
if (isUpdatingPosition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Windows.Point size = ScreenUtilities.PercentSizeToSize(settings.Size, screenBounds);
|
||||
if (size == new System.Windows.Point(Width, Height))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdatingPosition = true;
|
||||
Width = size.X;
|
||||
Height = size.Y;
|
||||
isUpdatingPosition = false;
|
||||
}
|
||||
|
||||
#region Window Events
|
||||
|
||||
private void Window_LocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (isUpdatingPosition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Windows.Point topLeft = new(Left, Top);
|
||||
System.Windows.Point clamped = ScreenUtilities.ClampToScreenBounds(topLeft, new System.Windows.Size(ActualWidth, ActualHeight), screenBounds);
|
||||
|
||||
if (clamped != topLeft)
|
||||
{
|
||||
isUpdatingPosition = true;
|
||||
Left = clamped.X;
|
||||
Top = clamped.Y;
|
||||
isUpdatingPosition = false;
|
||||
}
|
||||
|
||||
settings.Position = ScreenUtilities.PositionToPercent(new System.Windows.Point(Left, Top), screenBounds);
|
||||
settings.Position = new System.Windows.Point(Left, Top);
|
||||
}
|
||||
|
||||
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (!isUpdatingPosition)
|
||||
{
|
||||
settings.Size = ScreenUtilities.SizeToPercent(new System.Windows.Point(Width, Height), screenBounds);
|
||||
}
|
||||
settings.Size = new System.Windows.Point(Width, Height);
|
||||
|
||||
tileBar.CaptionHeight = ActualHeight - 10;
|
||||
}
|
||||
|
||||
private void Image_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
private void Window_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
_ = image.Focus();
|
||||
|
||||
ImageSource imageSource = image.Source;
|
||||
BitmapSource bitmapImage = (BitmapSource)imageSource;
|
||||
double pixelMousePositionX = e.GetPosition(image).X * bitmapImage.PixelWidth / image.ActualHeight;
|
||||
@@ -1302,7 +970,7 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
pluginClassInstance?.OnMouseClick(point, mouseButton);
|
||||
}
|
||||
|
||||
private void Image_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
private void Window_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
ImageSource imageSource = image.Source;
|
||||
BitmapSource bitmapImage = (BitmapSource)imageSource;
|
||||
@@ -1313,7 +981,7 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
pluginClassInstance?.OnMouseMove(point);
|
||||
}
|
||||
|
||||
private void Image_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
|
||||
private void Window_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
|
||||
{
|
||||
ImageSource imageSource = image.Source;
|
||||
BitmapSource bitmapImage = (BitmapSource)imageSource;
|
||||
@@ -1324,39 +992,5 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
pluginClassInstance?.OnMouseWheel(point, e.Delta);
|
||||
}
|
||||
|
||||
private void Image_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
if (pluginClassInstance is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Keys key = (Keys)e.Key;
|
||||
System.Windows.Input.ModifierKeys modifiers = e.KeyboardDevice.Modifiers;
|
||||
bool alt = modifiers.HasFlag(System.Windows.Input.ModifierKeys.Alt);
|
||||
bool control = modifiers.HasFlag(System.Windows.Input.ModifierKeys.Control);
|
||||
bool shift = modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift);
|
||||
bool windows = modifiers.HasFlag(System.Windows.Input.ModifierKeys.Windows);
|
||||
|
||||
pluginClassInstance.OnKeyDown(new KeyEventArgs(key, alt, control, shift, windows));
|
||||
}
|
||||
|
||||
private void Image_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
if (pluginClassInstance is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Keys key = (Keys)e.Key;
|
||||
System.Windows.Input.ModifierKeys modifiers = e.KeyboardDevice.Modifiers;
|
||||
bool alt = modifiers.HasFlag(System.Windows.Input.ModifierKeys.Alt);
|
||||
bool control = modifiers.HasFlag(System.Windows.Input.ModifierKeys.Control);
|
||||
bool shift = modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift);
|
||||
bool windows = modifiers.HasFlag(System.Windows.Input.ModifierKeys.Windows);
|
||||
|
||||
pluginClassInstance.OnKeyUp(new KeyEventArgs(key, alt, control, shift, windows));
|
||||
}
|
||||
|
||||
#endregion Window Events
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public class SettingElement
|
||||
private string jsonValue = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public Setting? Input { get; set; }
|
||||
public Setting Input { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public int OrderIndex { get; set; }
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DesktopMagic.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the per-instance plugin settings of every window sharing the same layout in sync.
|
||||
/// Each window owns its own <see cref="DesktopMagic.Api.Settings.Setting"/> objects (plugins
|
||||
/// hold them in readonly fields and subscribe to them in Start), so a value change on one
|
||||
/// window has to be mirrored onto the sibling windows.
|
||||
/// </summary>
|
||||
internal sealed class SettingSynchronizer
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly List<IPluginWindow> windows = [];
|
||||
private readonly HashSet<(IPluginWindow Window, string Id)> inFlight = [];
|
||||
|
||||
/// <summary>
|
||||
/// Registers a window so it receives mirrored settings from the other windows of this layout.
|
||||
/// </summary>
|
||||
public void Register(IPluginWindow window)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!windows.Contains(window))
|
||||
{
|
||||
windows.Add(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a window and returns whether no windows remain.
|
||||
/// </summary>
|
||||
public bool Unregister(IPluginWindow window)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
windows.Remove(window);
|
||||
return windows.Count == 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors a setting value change onto every other registered window.
|
||||
/// </summary>
|
||||
public void SettingChanged(IPluginWindow origin, string id, string value)
|
||||
{
|
||||
Mirror(origin, id, window => window.ApplySettingValue(id, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors a button click onto every other registered window.
|
||||
/// </summary>
|
||||
public void ButtonClicked(IPluginWindow origin, string id)
|
||||
{
|
||||
Mirror(origin, id, window => window.ApplyButtonClick(id));
|
||||
}
|
||||
|
||||
private void Mirror(IPluginWindow origin, string id, Action<IPluginWindow> apply)
|
||||
{
|
||||
IPluginWindow[] snapshot;
|
||||
lock (_lock)
|
||||
{
|
||||
snapshot = windows.ToArray();
|
||||
}
|
||||
|
||||
foreach (IPluginWindow window in snapshot)
|
||||
{
|
||||
if (ReferenceEquals(window, origin) || !window.IsRunning)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!inFlight.Add((window, id)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (window is not DispatcherObject dispatcherObject)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
inFlight.Remove((window, id));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
_ = dispatcherObject.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
apply(window);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
inFlight.Remove((window, id));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ using DesktopMagic.Settings;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
@@ -31,27 +29,11 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
private System.Timers.Timer? reloadDebounceTimer;
|
||||
private bool isReloading = false;
|
||||
|
||||
private readonly System.Drawing.Rectangle screenBounds;
|
||||
private readonly string screenDeviceName;
|
||||
private bool isUpdatingPosition = false;
|
||||
|
||||
// Event handlers on long-lived settings objects, tracked so they can be unsubscribed on close.
|
||||
private readonly PropertyChangedEventHandler settingsPropertyChangedHandler;
|
||||
private readonly PropertyChangedEventHandler themePropertyChangedHandler;
|
||||
private Theme? subscribedTheme;
|
||||
private readonly List<(Setting Setting, Action Handler)> subscribedSettings = [];
|
||||
private readonly List<(Button Button, Action Handler)> subscribedButtonClicks = [];
|
||||
|
||||
private readonly ConcurrentDictionary<string, Setting> localSettings = [];
|
||||
private SettingSynchronizer? synchronizer;
|
||||
private bool suppressButtonSync = false;
|
||||
|
||||
public bool IsRunning { get; private set; } = true;
|
||||
public PluginMetadata PluginMetadata { get; private set; }
|
||||
public string PluginFolderPath { get; private set; }
|
||||
public string ScreenDeviceName => screenDeviceName;
|
||||
|
||||
public WebPluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath, System.Drawing.Rectangle screenBounds, string screenDeviceName)
|
||||
public WebPluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -71,69 +53,39 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
|
||||
Owner = w;
|
||||
|
||||
settingsPropertyChangedHandler = (_, s) =>
|
||||
settings.PropertyChanged += (e, s) =>
|
||||
{
|
||||
if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
|
||||
{
|
||||
SubscribeToTheme(settings.Theme);
|
||||
settings.Theme.PropertyChanged += (se, ev) =>
|
||||
{
|
||||
ThemeChanged();
|
||||
};
|
||||
ThemeChanged();
|
||||
}
|
||||
else if (s.PropertyName == nameof(PluginSettings.Position))
|
||||
{
|
||||
UpdatePosition();
|
||||
}
|
||||
else if (s.PropertyName == nameof(PluginSettings.Size))
|
||||
{
|
||||
UpdateSize();
|
||||
}
|
||||
};
|
||||
settings.PropertyChanged += settingsPropertyChangedHandler;
|
||||
|
||||
themePropertyChangedHandler = (_, _) => ThemeChanged();
|
||||
SubscribeToTheme(settings.Theme);
|
||||
settings.Theme.PropertyChanged += (se, ev) =>
|
||||
{
|
||||
ThemeChanged();
|
||||
};
|
||||
|
||||
PluginMetadata = pluginMetadata;
|
||||
this.settings = settings;
|
||||
this.screenBounds = screenBounds;
|
||||
this.screenDeviceName = screenDeviceName;
|
||||
|
||||
Point position = ScreenUtilities.PercentToPosition(settings.Position, screenBounds);
|
||||
Point size = ScreenUtilities.PercentSizeToSize(settings.Size, screenBounds);
|
||||
Left = position.X;
|
||||
Top = position.Y;
|
||||
Width = size.X;
|
||||
Height = size.Y;
|
||||
Left = settings.Position.X;
|
||||
Top = settings.Position.Y;
|
||||
Width = settings.Size.X;
|
||||
Height = settings.Size.Y;
|
||||
|
||||
PluginFolderPath = pluginFolderPath;
|
||||
|
||||
if (settings.Owner is not null)
|
||||
{
|
||||
synchronizer = Manager.Instance.GetSettingSynchronizer(settings.Owner, PluginMetadata.Id);
|
||||
synchronizer.Register(this);
|
||||
}
|
||||
|
||||
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
|
||||
{
|
||||
InitializeHotReload();
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeToTheme(Theme theme)
|
||||
{
|
||||
if (ReferenceEquals(subscribedTheme, theme))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscribedTheme is not null)
|
||||
{
|
||||
subscribedTheme.PropertyChanged -= themePropertyChangedHandler;
|
||||
}
|
||||
|
||||
subscribedTheme = theme;
|
||||
subscribedTheme.PropertyChanged += themePropertyChangedHandler;
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
IsRunning = false;
|
||||
@@ -168,32 +120,6 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplySettingValue(string id, string value)
|
||||
{
|
||||
if (localSettings.TryGetValue(id, out Setting? setting) && setting.GetJsonValue() != value)
|
||||
{
|
||||
setting.SetJsonValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyButtonClick(string id)
|
||||
{
|
||||
if (!localSettings.TryGetValue(id, out Setting? setting) || setting is not Button button)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
suppressButtonSync = true;
|
||||
try
|
||||
{
|
||||
button.Click();
|
||||
}
|
||||
finally
|
||||
{
|
||||
suppressButtonSync = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
@@ -323,18 +249,6 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
|
||||
reloadDebounceTimer?.Stop();
|
||||
reloadDebounceTimer?.Dispose();
|
||||
reloadDebounceTimer = null;
|
||||
|
||||
UnsubscribeEvents();
|
||||
|
||||
if (synchronizer is not null && settings.Owner is not null)
|
||||
{
|
||||
if (synchronizer.Unregister(this))
|
||||
{
|
||||
Manager.Instance.ReleaseSettingSynchronizer(settings.Owner, PluginMetadata.Id);
|
||||
}
|
||||
synchronizer = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -349,96 +263,14 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeEvents()
|
||||
{
|
||||
settings.PropertyChanged -= settingsPropertyChangedHandler;
|
||||
|
||||
if (subscribedTheme is not null)
|
||||
{
|
||||
subscribedTheme.PropertyChanged -= themePropertyChangedHandler;
|
||||
subscribedTheme = null;
|
||||
}
|
||||
|
||||
foreach ((Setting setting, Action handler) in subscribedSettings)
|
||||
{
|
||||
setting.OnValueChanged -= handler;
|
||||
}
|
||||
subscribedSettings.Clear();
|
||||
|
||||
foreach ((Button button, Action handler) in subscribedButtonClicks)
|
||||
{
|
||||
button.OnClick -= handler;
|
||||
}
|
||||
subscribedButtonClicks.Clear();
|
||||
|
||||
localSettings.Clear();
|
||||
}
|
||||
|
||||
private void UpdatePosition()
|
||||
{
|
||||
if (isUpdatingPosition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Point position = ScreenUtilities.PercentToPosition(settings.Position, screenBounds);
|
||||
if (position == new Point(Left, Top))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdatingPosition = true;
|
||||
Left = position.X;
|
||||
Top = position.Y;
|
||||
isUpdatingPosition = false;
|
||||
}
|
||||
|
||||
private void UpdateSize()
|
||||
{
|
||||
if (isUpdatingPosition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Point size = ScreenUtilities.PercentSizeToSize(settings.Size, screenBounds);
|
||||
if (size == new Point(Width, Height))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdatingPosition = true;
|
||||
Width = size.X;
|
||||
Height = size.Y;
|
||||
isUpdatingPosition = false;
|
||||
}
|
||||
|
||||
private void Window_LocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (isUpdatingPosition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Point topLeft = new(Left, Top);
|
||||
Point clamped = ScreenUtilities.ClampToScreenBounds(topLeft, new Size(ActualWidth, ActualHeight), screenBounds);
|
||||
|
||||
if (clamped != topLeft)
|
||||
{
|
||||
isUpdatingPosition = true;
|
||||
Left = clamped.X;
|
||||
Top = clamped.Y;
|
||||
isUpdatingPosition = false;
|
||||
}
|
||||
|
||||
settings.Position = ScreenUtilities.PositionToPercent(new Point(Left, Top), screenBounds);
|
||||
settings.Position = new System.Windows.Point(Left, Top);
|
||||
}
|
||||
|
||||
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (!isUpdatingPosition)
|
||||
{
|
||||
settings.Size = ScreenUtilities.SizeToPercent(new Point(Width, Height), screenBounds);
|
||||
}
|
||||
settings.Size = new System.Windows.Point(Width, Height);
|
||||
|
||||
tileBar.CaptionHeight = ActualHeight - 10;
|
||||
}
|
||||
@@ -498,30 +330,19 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
continue;
|
||||
}
|
||||
|
||||
localSettings[id] = setting;
|
||||
|
||||
SettingElement settingElement = new(setting, id, name, orderIndex);
|
||||
|
||||
if (settings.Settings.Exists(e => e.Id == id))
|
||||
{
|
||||
SettingElement saved = settings.Settings.First(e => e.Id == id);
|
||||
string savedValue = saved.JsonValue;
|
||||
if (!string.IsNullOrEmpty(savedValue) || setting is not Label and not Button)
|
||||
{
|
||||
settingElement.JsonValue = savedValue;
|
||||
}
|
||||
settingElement.JsonValue = saved.JsonValue;
|
||||
}
|
||||
|
||||
string capturedId = id;
|
||||
if (setting is Button button)
|
||||
{
|
||||
Action clickHandler = () =>
|
||||
button.OnClick += () =>
|
||||
{
|
||||
if (!suppressButtonSync)
|
||||
{
|
||||
synchronizer?.ButtonClicked(this, capturedId);
|
||||
}
|
||||
|
||||
_ = webView.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
@@ -534,14 +355,10 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
}
|
||||
});
|
||||
};
|
||||
button.OnClick += clickHandler;
|
||||
subscribedButtonClicks.Add((button, clickHandler));
|
||||
}
|
||||
|
||||
Action valueChangedHandler = () =>
|
||||
setting.OnValueChanged += () =>
|
||||
{
|
||||
synchronizer?.SettingChanged(this, capturedId, setting.GetJsonValue());
|
||||
|
||||
_ = webView.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
@@ -554,8 +371,6 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
}
|
||||
});
|
||||
};
|
||||
setting.OnValueChanged += valueChangedHandler;
|
||||
subscribedSettings.Add((setting, valueChangedHandler));
|
||||
|
||||
settingElements.Add(settingElement);
|
||||
orderIndex++;
|
||||
@@ -725,7 +540,7 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
Dictionary<string, object?> dict = [];
|
||||
foreach (SettingElement element in settings.Settings)
|
||||
{
|
||||
dict[element.Id] = GetSettingValue(element.Input!);
|
||||
dict[element.Id] = GetSettingValue(element.Input);
|
||||
}
|
||||
return JsonSerializer.Serialize(dict);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<system:String x:Key="weather">Wetter</system:String>
|
||||
<system:String x:Key="nextMeetingCountdown">Nächstes Meeting Countdown</system:String>
|
||||
<system:String x:Key="agenda">Agenda</system:String>
|
||||
<system:String x:Key="benchmark">Performance Benchmark</system:String>
|
||||
<system:String x:Key="skiaBenchmark">SkiaSharp Benchmark</system:String>
|
||||
<system:String x:Key="amplifier">Signalverstärkung:</system:String>
|
||||
<system:String x:Key="newLayout">Neues Layout</system:String>
|
||||
<system:String x:Key="deleteLayout">Layout Löschen</system:String>
|
||||
@@ -55,7 +53,6 @@
|
||||
<system:String x:Key="confirmDeleteTheme">Möchten Sie dieses Theme wirklich löschen?</system:String>
|
||||
<system:String x:Key="enterLayoutName">Layoutnamen eingeben:</system:String>
|
||||
<system:String x:Key="layoutAlreadyExists">Layout existiert bereits!</system:String>
|
||||
<system:String x:Key="cannotDeleteEmptyLayout">Das leere Layout kann nicht gelöscht werden!</system:String>
|
||||
<system:String x:Key="enterThemeName">Themenamen eingeben:</system:String>
|
||||
<system:String x:Key="themeAlreadyExists">Theme existiert bereits!</system:String>
|
||||
<system:String x:Key="install">Installieren</system:String>
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<system:String x:Key="weather">Weather</system:String>
|
||||
<system:String x:Key="nextMeetingCountdown">Next Meeting Countdown</system:String>
|
||||
<system:String x:Key="agenda">Agenda</system:String>
|
||||
<system:String x:Key="benchmark">Performance Benchmark</system:String>
|
||||
<system:String x:Key="skiaBenchmark">SkiaSharp Benchmark</system:String>
|
||||
<system:String x:Key="amplifier">Signal Amplification:</system:String>
|
||||
<system:String x:Key="newLayout">New Layout</system:String>
|
||||
<system:String x:Key="deleteLayout">Delete Layout</system:String>
|
||||
@@ -56,7 +54,6 @@
|
||||
<system:String x:Key="enterLayoutName">Enter layout name</system:String>
|
||||
<system:String x:Key="layoutAlreadyExists">Layout already exists!</system:String>
|
||||
<system:String x:Key="cannotDeleteLastLayout">Can't delete last layout!</system:String>
|
||||
<system:String x:Key="cannotDeleteEmptyLayout">Can't delete the empty layout!</system:String>
|
||||
<system:String x:Key="enterThemeName">Enter theme name</system:String>
|
||||
<system:String x:Key="themeAlreadyExists">Theme already exists!</system:String>
|
||||
<system:String x:Key="cannotDeleteLastTheme">Can't delete last theme!</system:String>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using DesktopMagic.Plugins;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
@@ -23,13 +22,7 @@ public class DesktopMagicSettings : INotifyPropertyChanged
|
||||
init
|
||||
{
|
||||
themes = value;
|
||||
themes.CollectionChanged += (s, e) =>
|
||||
{
|
||||
foreach (Layout layout in layouts)
|
||||
{
|
||||
layout.UpdateTheme();
|
||||
}
|
||||
};
|
||||
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
@@ -69,33 +62,13 @@ public class DesktopMagicSettings : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps screen device names (e.g. "\\.\DISPLAY1") to the layout applied on that screen.
|
||||
/// The layouts themselves are portable and store their target aspect ratio.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ScreenLayouts { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Version of the settings schema, used to migrate older settings files.
|
||||
/// 0 = legacy (layouts not screen aware, pixel based positions).
|
||||
/// </summary>
|
||||
public int SchemaVersion { get; set; }
|
||||
|
||||
public string? ModIoAccessToken { get; set; }
|
||||
|
||||
public string? ReleaseInfoLastAppVersion { get; set; }
|
||||
|
||||
public bool IsFirstRun { get; set; } = true;
|
||||
|
||||
public DesktopMagicSettings()
|
||||
{
|
||||
themes.CollectionChanged += (s, e) =>
|
||||
{
|
||||
foreach (Layout layout in layouts)
|
||||
{
|
||||
layout.UpdateTheme();
|
||||
}
|
||||
};
|
||||
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
|
||||
|
||||
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayout));
|
||||
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayoutName));
|
||||
|
||||
@@ -16,7 +16,6 @@ public class Layout(string name) : INotifyPropertyChanged
|
||||
private string name = name;
|
||||
private string? currentThemeName = null;
|
||||
private Dictionary<uint, PluginSettings> plugins = [];
|
||||
private double screenAspectRatio = 0;
|
||||
|
||||
[JsonIgnore]
|
||||
public Theme Theme
|
||||
@@ -61,23 +60,6 @@ public class Layout(string name) : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aspect ratio (Width / Height) of the screen this layout was designed for.
|
||||
/// Used to match layouts to screens and to make layouts portable across screens.
|
||||
/// </summary>
|
||||
public double ScreenAspectRatio
|
||||
{
|
||||
get => screenAspectRatio;
|
||||
set
|
||||
{
|
||||
if (screenAspectRatio != value)
|
||||
{
|
||||
screenAspectRatio = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePlugins()
|
||||
{
|
||||
plugins = plugins.ToDictionary();
|
||||
|
||||
@@ -19,24 +19,20 @@ public class PluginSettings : INotifyPropertyChanged
|
||||
private List<SettingElement> settings = [];
|
||||
private Dictionary<string, JsonElement> state = [];
|
||||
private bool enabled = false;
|
||||
private Point position = new Point(0.05, 0.05);
|
||||
private Point size = new Point(0.3, 0.3);
|
||||
private Point position = new Point(100, 100);
|
||||
private Point size = new Point(300, 300);
|
||||
|
||||
// Only for internal use to show the name of the plugin in the main window
|
||||
[JsonIgnore]
|
||||
public PluginMetadata Metadata { get; set; } = new();
|
||||
|
||||
// The layout this plugin belongs to, used to resolve the fallback theme
|
||||
[JsonIgnore]
|
||||
public Layout? Owner { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Theme Theme
|
||||
{
|
||||
get
|
||||
{
|
||||
DesktopMagicSettings settings = MainWindowDataContext.GetSettings();
|
||||
return settings.Themes.FirstOrDefault(t => t.Name == currentThemeName) ?? Owner?.Theme ?? settings.CurrentLayout.Theme;
|
||||
return settings.Themes.FirstOrDefault(t => t.Name == currentThemeName) ?? settings.CurrentLayout.Theme;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +93,6 @@ public class PluginSettings : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position of the plugin window as percentages (0..1) of the owning screen's bounds.
|
||||
/// </summary>
|
||||
public Point Position
|
||||
{
|
||||
get => position;
|
||||
@@ -113,9 +106,6 @@ public class PluginSettings : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Size of the plugin window as percentages (0..1) of the owning screen's bounds.
|
||||
/// </summary>
|
||||
public Point Size
|
||||
{
|
||||
get => size;
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" Version="4.148.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for keyboard events.
|
||||
/// </summary>
|
||||
public class KeyEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The key that was pressed or released.
|
||||
/// </summary>
|
||||
public Keys Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Alt modifier was pressed.
|
||||
/// </summary>
|
||||
public bool Alt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Control modifier was pressed.
|
||||
/// </summary>
|
||||
public bool Control { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Shift modifier was pressed.
|
||||
/// </summary>
|
||||
public bool Shift { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Windows logo key was pressed.
|
||||
/// </summary>
|
||||
public bool Windows { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyEventArgs"/> class.
|
||||
/// </summary>
|
||||
public KeyEventArgs(Keys key, bool alt, bool control, bool shift, bool windows)
|
||||
{
|
||||
Key = key;
|
||||
Alt = alt;
|
||||
Control = control;
|
||||
Shift = shift;
|
||||
Windows = windows;
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Defines keyboard keys available for plugin input handling.
|
||||
/// Values match <c>System.Windows.Input.Key</c> for direct casting.
|
||||
/// </summary>
|
||||
public enum Keys
|
||||
{
|
||||
None = 0,
|
||||
Cancel = 1,
|
||||
Back = 2,
|
||||
Tab = 3,
|
||||
LineFeed = 4,
|
||||
Clear = 5,
|
||||
Return = 6,
|
||||
Enter = 6,
|
||||
ShiftKey = 16,
|
||||
ControlKey = 17,
|
||||
AltKey = 18,
|
||||
Pause = 19,
|
||||
CapsLock = 20,
|
||||
Escape = 27,
|
||||
Space = 32,
|
||||
PageUp = 33,
|
||||
PageDown = 34,
|
||||
End = 35,
|
||||
Home = 36,
|
||||
Left = 37,
|
||||
Up = 38,
|
||||
Right = 39,
|
||||
Down = 40,
|
||||
Select = 41,
|
||||
Print = 42,
|
||||
Execute = 43,
|
||||
PrintScreen = 44,
|
||||
Insert = 45,
|
||||
Delete = 46,
|
||||
Help = 47,
|
||||
D0 = 48,
|
||||
D1 = 49,
|
||||
D2 = 50,
|
||||
D3 = 51,
|
||||
D4 = 52,
|
||||
D5 = 53,
|
||||
D6 = 54,
|
||||
D7 = 55,
|
||||
D8 = 56,
|
||||
D9 = 57,
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
LWin = 91,
|
||||
RWin = 92,
|
||||
Apps = 93,
|
||||
Sleep = 95,
|
||||
NumPad0 = 96,
|
||||
NumPad1 = 97,
|
||||
NumPad2 = 98,
|
||||
NumPad3 = 99,
|
||||
NumPad4 = 100,
|
||||
NumPad5 = 101,
|
||||
NumPad6 = 102,
|
||||
NumPad7 = 103,
|
||||
NumPad8 = 104,
|
||||
NumPad9 = 105,
|
||||
Multiply = 106,
|
||||
Add = 107,
|
||||
Separator = 108,
|
||||
Subtract = 109,
|
||||
Decimal = 110,
|
||||
Divide = 111,
|
||||
F1 = 112,
|
||||
F2 = 113,
|
||||
F3 = 114,
|
||||
F4 = 115,
|
||||
F5 = 116,
|
||||
F6 = 117,
|
||||
F7 = 118,
|
||||
F8 = 119,
|
||||
F9 = 120,
|
||||
F10 = 121,
|
||||
F11 = 122,
|
||||
F12 = 123,
|
||||
F13 = 124,
|
||||
F14 = 125,
|
||||
F15 = 126,
|
||||
F16 = 127,
|
||||
F17 = 128,
|
||||
F18 = 129,
|
||||
F19 = 130,
|
||||
F20 = 131,
|
||||
F21 = 132,
|
||||
F22 = 133,
|
||||
F23 = 134,
|
||||
F24 = 135,
|
||||
NumLock = 144,
|
||||
Scroll = 145,
|
||||
LeftShift = 160,
|
||||
RightShift = 161,
|
||||
LeftCtrl = 162,
|
||||
RightCtrl = 163,
|
||||
LeftAlt = 164,
|
||||
RightAlt = 165,
|
||||
BrowserBack = 166,
|
||||
BrowserForward = 167,
|
||||
BrowserRefresh = 168,
|
||||
BrowserStop = 169,
|
||||
BrowserSearch = 170,
|
||||
BrowserFavorites = 171,
|
||||
BrowserHome = 172,
|
||||
VolumeMute = 173,
|
||||
VolumeDown = 174,
|
||||
VolumeUp = 175,
|
||||
MediaNextTrack = 176,
|
||||
MediaPreviousTrack = 177,
|
||||
MediaStop = 178,
|
||||
MediaPlayPause = 179,
|
||||
LaunchMail = 180,
|
||||
SelectMedia = 181,
|
||||
LaunchApplication1 = 182,
|
||||
LaunchApplication2 = 183,
|
||||
OemSemicolon = 186,
|
||||
OemPlus = 187,
|
||||
OemComma = 188,
|
||||
OemMinus = 189,
|
||||
OemPeriod = 190,
|
||||
OemQuestion = 191,
|
||||
OemTilde = 192,
|
||||
OemOpenBrackets = 219,
|
||||
OemPipe = 220,
|
||||
OemCloseBrackets = 221,
|
||||
OemQuotes = 222,
|
||||
Oem8 = 223,
|
||||
OemBackslash = 226,
|
||||
}
|
||||
@@ -99,22 +99,6 @@ public abstract class Plugin
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a key is pressed while the plugin window has keyboard focus.
|
||||
/// </summary>
|
||||
/// <param name="e">The key event data.</param>
|
||||
public virtual void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a key is released while the plugin window has keyboard focus.
|
||||
/// </summary>
|
||||
/// <param name="e">The key event data.</param>
|
||||
public virtual void OnKeyUp(KeyEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the application's theme has changed.
|
||||
/// </summary>
|
||||
|
||||
@@ -46,14 +46,4 @@ public class Button : Setting
|
||||
{
|
||||
OnClick?.Invoke();
|
||||
}
|
||||
|
||||
internal override string GetJsonValue()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
internal override void SetJsonValue(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
@@ -38,14 +38,4 @@ public class Label : Setting
|
||||
Value = value;
|
||||
Bold = bold;
|
||||
}
|
||||
|
||||
internal override string GetJsonValue()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
internal override void SetJsonValue(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using SkiaSharp;
|
||||
|
||||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for creating asynchronous plugins using SkiaSharp rendering.
|
||||
/// Plugins override <see cref="MainAsync(SKCanvas, CancellationToken)"/> instead of <see cref="Plugin.Main()"/>.
|
||||
/// All existing <see cref="Plugin"/>, <see cref="AsyncPlugin"/>, and <see cref="SkiaPlugin"/> code is unaffected.
|
||||
/// </summary>
|
||||
public abstract class SkiaAsyncPlugin : AsyncPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// This method is sealed and returns null for async Skia plugins.
|
||||
/// Override <see cref="MainAsync(SKCanvas, CancellationToken)"/> instead.
|
||||
/// </summary>
|
||||
public sealed override Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<Bitmap?>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called every <see cref="Plugin.UpdateInterval"/> milliseconds to render the plugin asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The SkiaSharp canvas to draw on.</param>
|
||||
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
|
||||
public abstract Task MainAsync(SKCanvas canvas, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using SkiaSharp;
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for creating plugins using SkiaSharp rendering.
|
||||
/// Plugins override <see cref="Main(SKCanvas)"/> instead of <see cref="Plugin.Main()"/>.
|
||||
/// All existing <see cref="Plugin"/> and <see cref="AsyncPlugin"/> code is unaffected.
|
||||
/// </summary>
|
||||
public abstract class SkiaPlugin : Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// This method is sealed and returns null for Skia plugins.
|
||||
/// Override <see cref="Main(SKCanvas)"/> instead.
|
||||
/// </summary>
|
||||
/// <returns>Always null.</returns>
|
||||
public sealed override Bitmap? Main()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called every <see cref="Plugin.UpdateInterval"/> milliseconds to render the plugin.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The SkiaSharp canvas to draw on.</param>
|
||||
public abstract void Main(SKCanvas canvas);
|
||||
}
|
||||
Reference in New Issue
Block a user