mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 00:46:12 +02:00
Add per-screen layouts with DPI-aware widget positioning and an empty layout option
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
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;
|
||||
@@ -11,6 +14,8 @@ 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;
|
||||
@@ -34,6 +39,30 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ScreenDisplay> Screens { get; } = [];
|
||||
|
||||
public string? SelectedScreenId
|
||||
{
|
||||
get => selectedScreenDeviceName;
|
||||
set
|
||||
{
|
||||
selectedScreenDeviceName = value;
|
||||
UpdateSelection();
|
||||
}
|
||||
}
|
||||
|
||||
public Layout SelectedLayout => Manager.Instance.SelectedLayout;
|
||||
|
||||
public string? SelectedLayoutName
|
||||
{
|
||||
get => selectedLayoutName;
|
||||
set
|
||||
{
|
||||
selectedLayoutName = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => isLoading;
|
||||
@@ -76,8 +105,67 @@ 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();
|
||||
|
||||
Screens.Clear();
|
||||
for (int i = 0; i < allScreens.Count; i++)
|
||||
{
|
||||
Screens.Add(new ScreenDisplay(allScreens[i], i));
|
||||
}
|
||||
|
||||
if (selectedScreenDeviceName is null || !Screens.Any(screen => screen.DeviceName == selectedScreenDeviceName))
|
||||
{
|
||||
SelectedScreenId = 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(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
|
||||
{
|
||||
public ScreenDisplay(System.Windows.Forms.Screen screen, int index)
|
||||
{
|
||||
DeviceName = screen.DeviceName;
|
||||
DisplayName = ScreenUtilities.GetScreenLabel(screen, index);
|
||||
}
|
||||
|
||||
public string DeviceName { get; }
|
||||
|
||||
public string DisplayName { get; }
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
var device = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||
if (EnumDisplayDevices(screen.DeviceName, 0, ref device, 0))
|
||||
{
|
||||
var monitor = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||
if (EnumDisplayDevices(device.DeviceName, 0, ref monitor, 0))
|
||||
{
|
||||
return monitor.DeviceString;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fall through to an empty name.
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
var 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;
|
||||
}
|
||||
}
|
||||
+213
-19
@@ -5,6 +5,7 @@ using DesktopMagic.Settings;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
@@ -35,6 +36,9 @@ 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()
|
||||
@@ -65,6 +69,13 @@ 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() }
|
||||
@@ -137,20 +148,46 @@ public sealed class Manager
|
||||
App.Logger.LogInfo($"Loaded {_plugins.Count} plugins", source: "Manager");
|
||||
}
|
||||
|
||||
public void LoadPlugin(uint pluginId, Action<InternalPluginData>? onPluginLoaded = null)
|
||||
/// <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)
|
||||
{
|
||||
if (!_plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||
if (!layout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||
{
|
||||
pluginSettings = new PluginSettings();
|
||||
Settings.CurrentLayout.Plugins.Add(pluginId, pluginSettings);
|
||||
layout.Plugins.Add(pluginId, pluginSettings);
|
||||
}
|
||||
|
||||
IPluginWindow? existingWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id);
|
||||
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);
|
||||
|
||||
if (existingWindow is not null || !pluginSettings.Enabled)
|
||||
{
|
||||
@@ -176,21 +213,21 @@ public sealed class Manager
|
||||
|
||||
if (_builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
|
||||
{
|
||||
window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings)
|
||||
window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings, screenBounds, screenDeviceName)
|
||||
{
|
||||
Title = internalPluginData.Metadata.Id.ToString()
|
||||
};
|
||||
}
|
||||
else if (internalPluginData.Type == PluginType.Web)
|
||||
{
|
||||
window = new WebPluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
|
||||
window = new WebPluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath, screenBounds, screenDeviceName)
|
||||
{
|
||||
Title = internalPluginData.Metadata.Id.ToString()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
|
||||
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath, screenBounds, screenDeviceName)
|
||||
{
|
||||
Title = internalPluginData.Metadata.Id.ToString()
|
||||
};
|
||||
@@ -205,10 +242,21 @@ public sealed class Manager
|
||||
|
||||
Action exitHandler = () =>
|
||||
{
|
||||
PluginWindows.Remove(window);
|
||||
// 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);
|
||||
BlockWindowsClosing = false;
|
||||
window.Close();
|
||||
sharedWindow.Close();
|
||||
BlockWindowsClosing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
pluginSettings.Enabled = false;
|
||||
};
|
||||
|
||||
@@ -255,7 +303,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -272,9 +322,62 @@ 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[primaryScreen.DeviceName] = Settings.CurrentLayoutName ?? "Default";
|
||||
}
|
||||
|
||||
App.Logger.LogInfo("Settings migrated to screen-aware layouts", source: "Manager");
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
if (!IsLoaded)
|
||||
@@ -292,9 +395,60 @@ 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)
|
||||
{
|
||||
if (Settings.ScreenLayouts.TryGetValue(screen.DeviceName, 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[screen.DeviceName] = 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 layout", source: "Manager");
|
||||
App.Logger.LogInfo("Loading layouts", source: "Manager");
|
||||
BlockWindowsClosing = false;
|
||||
|
||||
foreach (IPluginWindow window in PluginWindows)
|
||||
@@ -305,37 +459,77 @@ 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;
|
||||
}
|
||||
|
||||
// Load plugins
|
||||
foreach (uint pluginId in _plugins.Keys)
|
||||
{
|
||||
InternalPluginData internalPluginData = _plugins[pluginId];
|
||||
|
||||
// Add plugin to layout if it doesn't exist
|
||||
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||
if (!layout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||
{
|
||||
Settings.CurrentLayout.Plugins.Add(pluginId, new PluginSettings() { Metadata = internalPluginData.Metadata });
|
||||
layout.Plugins.Add(pluginId, new PluginSettings() { Metadata = internalPluginData.Metadata, Owner = layout });
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginSettings.Metadata = internalPluginData.Metadata;
|
||||
pluginSettings.Owner = layout;
|
||||
|
||||
if (pluginSettings.Enabled)
|
||||
{
|
||||
LoadPlugin(pluginId);
|
||||
EnsurePluginWindow(screen, layout, internalPluginData, pluginSettings, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove plugins that are not loaded anymore
|
||||
List<uint> pluginIdsToRemove = Settings.CurrentLayout.Plugins.Keys.Where(id => !_plugins.ContainsKey(id)).ToList();
|
||||
List<uint> pluginIdsToRemove = layout.Plugins.Keys.Where(id => !_plugins.ContainsKey(id)).ToList();
|
||||
foreach (uint pluginId in pluginIdsToRemove)
|
||||
{
|
||||
Settings.CurrentLayout.Plugins.Remove(pluginId);
|
||||
layout.Plugins.Remove(pluginId);
|
||||
}
|
||||
|
||||
Settings.CurrentLayout.UpdatePlugins();
|
||||
|
||||
onComplete?.Invoke();
|
||||
App.Logger.LogInfo("Layout loaded", source: "Manager");
|
||||
layout.UpdatePlugins();
|
||||
}
|
||||
|
||||
#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 Settings.CurrentLayout.Plugins}">
|
||||
<ItemsControl x:Name="pluginsItemsControl" ItemsSource="{Binding SelectedLayout.Plugins}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
|
||||
@@ -69,18 +69,25 @@
|
||||
</ScrollViewer>
|
||||
|
||||
<ui:Card Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
||||
<Grid >
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="5" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="0.5*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="0.5*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<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"/>
|
||||
<ComboBox x:Name="screensComboBox" ItemsSource="{Binding Screens}" DisplayMemberPath="DisplayName" SelectedValue="{Binding SelectedScreenId}" SelectedValuePath="DeviceName" VerticalAlignment="Center" />
|
||||
<ComboBox x:Name="layoutsComboBox" Grid.Column="2" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding SelectedLayoutName}" SelectedValuePath="Name" VerticalAlignment="Center" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
||||
|
||||
<StackPanel Grid.Row="2" Grid.ColumnSpan="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Icon="{ui:SymbolIcon Add24}" Margin="0,0,5,0" Click="NewLayoutButton_Click" FontWeight="Regular" />
|
||||
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Margin="0,0,5,0" Click="RemoveLayoutButton_Click" FontWeight="Regular" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ui:Card>
|
||||
</Grid>
|
||||
|
||||
@@ -20,7 +20,6 @@ public partial class MainPage : Page
|
||||
{
|
||||
private readonly Manager _manager = Manager.Instance;
|
||||
private readonly MainWindowDataContext _dataContext;
|
||||
private bool _isLoadingLayout = false;
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
@@ -45,6 +44,8 @@ public partial class MainPage : Page
|
||||
{
|
||||
// Initialize edit checkbox state
|
||||
editCheckBox.IsChecked = _manager.IsEditMode;
|
||||
|
||||
_dataContext.RefreshScreens();
|
||||
}
|
||||
|
||||
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
|
||||
@@ -85,7 +86,7 @@ public partial class MainPage : Page
|
||||
|
||||
uint pluginId = uint.Parse(checkBox.Tag.ToString()!);
|
||||
|
||||
_manager.LoadPlugin(pluginId, (internalPluginData) =>
|
||||
_manager.LoadPlugin(pluginId, _manager.SelectedLayout, (internalPluginData) =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
@@ -145,32 +146,46 @@ public partial class MainPage : Page
|
||||
|
||||
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
// Prevent recursive calls and only process if fully loaded
|
||||
if (_isLoadingLayout || !_manager.IsLoaded || !IsLoaded)
|
||||
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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 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])
|
||||
System.Windows.Forms.Screen? screen = ScreenUtilities.GetScreenByDeviceName(_dataContext.SelectedScreenId);
|
||||
if (screen is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? layoutName = _dataContext.SelectedLayoutName;
|
||||
if (layoutName is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
Layout? layout = _manager.Settings.Layouts.FirstOrDefault(l => l.Name == layoutName);
|
||||
if (layout is null)
|
||||
{
|
||||
_isLoadingLayout = true;
|
||||
_manager.SaveSettings();
|
||||
_manager.LoadLayout();
|
||||
return;
|
||||
}
|
||||
finally
|
||||
|
||||
if (_manager.GetLayoutForScreen(screen) == layout)
|
||||
{
|
||||
_isLoadingLayout = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_manager.BindLayoutToScreen(screen, layout);
|
||||
_manager.ReloadScreen(screen);
|
||||
_dataContext.RefreshSelection();
|
||||
}
|
||||
|
||||
private async void NewLayoutButton_Click(object sender, RoutedEventArgs e)
|
||||
@@ -194,18 +209,11 @@ 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();
|
||||
_manager.LoadLayout();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingLayout = false;
|
||||
}
|
||||
|
||||
// Select the new layout so the user can apply it to the current screen
|
||||
_dataContext.SelectedLayoutName = inputDialog.ResponseText.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,17 +245,40 @@ public partial class MainPage : Page
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
Layout? layout = _manager.Settings.Layouts.FirstOrDefault(l => l.Name == _dataContext.SelectedLayoutName);
|
||||
if (layout is null)
|
||||
{
|
||||
_isLoadingLayout = true;
|
||||
_ = _manager.Settings.Layouts.Remove(_manager.Settings.CurrentLayout);
|
||||
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);
|
||||
}
|
||||
|
||||
_manager.SaveSettings();
|
||||
_manager.LoadLayout();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingLayout = false;
|
||||
}
|
||||
_dataContext.RefreshSelection();
|
||||
}
|
||||
|
||||
#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 Settings.CurrentLayout.CurrentThemeName}" SelectedValuePath="Name" SelectedIndex="2" SelectionMode="Single">
|
||||
<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.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
|
||||
@@ -69,7 +69,7 @@ public partial class ThemePage : Page
|
||||
}
|
||||
|
||||
_manager.Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
|
||||
_manager.Settings.CurrentLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
|
||||
_manager.SelectedLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
|
||||
_manager.SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public interface IPluginWindow
|
||||
PluginMetadata PluginMetadata { get; }
|
||||
string PluginFolderPath { get; }
|
||||
string Title { get; set; }
|
||||
string ScreenDeviceName { get; }
|
||||
|
||||
void Exit();
|
||||
void SetEditMode(bool enabled);
|
||||
|
||||
@@ -39,6 +39,10 @@ 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 CancellationTokenSource? pluginCancellationTokenSource;
|
||||
private FileSystemWatcher? pluginFileWatcher;
|
||||
private System.Timers.Timer? reloadDebounceTimer;
|
||||
@@ -50,8 +54,9 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
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)
|
||||
public PluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath, Rectangle screenBounds, string screenDeviceName)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -81,6 +86,14 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
};
|
||||
ThemeChanged();
|
||||
}
|
||||
else if (s.PropertyName == nameof(PluginSettings.Position))
|
||||
{
|
||||
UpdatePosition();
|
||||
}
|
||||
else if (s.PropertyName == nameof(PluginSettings.Size))
|
||||
{
|
||||
UpdateSize();
|
||||
}
|
||||
};
|
||||
|
||||
settings.Theme.PropertyChanged += (se, ev) =>
|
||||
@@ -90,11 +103,15 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
|
||||
PluginMetadata = pluginMetadata;
|
||||
this.settings = settings;
|
||||
this.screenBounds = screenBounds;
|
||||
this.screenDeviceName = screenDeviceName;
|
||||
|
||||
Left = settings.Position.X;
|
||||
Top = settings.Position.Y;
|
||||
Width = settings.Size.X;
|
||||
Height = settings.Size.Y;
|
||||
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;
|
||||
|
||||
PluginFolderPath = pluginFolderPath;
|
||||
|
||||
@@ -107,7 +124,7 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
}
|
||||
}
|
||||
|
||||
public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings) : this(pluginMetadata, settings, string.Empty)
|
||||
public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings, Rectangle screenBounds, string screenDeviceName) : this(pluginMetadata, settings, string.Empty, screenBounds, screenDeviceName)
|
||||
{
|
||||
this.pluginClassInstance = pluginClassInstance;
|
||||
}
|
||||
@@ -993,16 +1010,73 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
_ = StopPlugin(unloadAssembly: true);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
settings.Position = new System.Windows.Point(Left, Top);
|
||||
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);
|
||||
}
|
||||
|
||||
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
settings.Size = new System.Windows.Point(Width, Height);
|
||||
if (!isUpdatingPosition)
|
||||
{
|
||||
settings.Size = ScreenUtilities.SizeToPercent(new System.Windows.Point(Width, Height), screenBounds);
|
||||
}
|
||||
|
||||
tileBar.CaptionHeight = ActualHeight - 10;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,16 @@ 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;
|
||||
|
||||
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)
|
||||
public WebPluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath, System.Drawing.Rectangle screenBounds, string screenDeviceName)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -63,6 +68,14 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
};
|
||||
ThemeChanged();
|
||||
}
|
||||
else if (s.PropertyName == nameof(PluginSettings.Position))
|
||||
{
|
||||
UpdatePosition();
|
||||
}
|
||||
else if (s.PropertyName == nameof(PluginSettings.Size))
|
||||
{
|
||||
UpdateSize();
|
||||
}
|
||||
};
|
||||
|
||||
settings.Theme.PropertyChanged += (se, ev) =>
|
||||
@@ -72,11 +85,15 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
|
||||
PluginMetadata = pluginMetadata;
|
||||
this.settings = settings;
|
||||
this.screenBounds = screenBounds;
|
||||
this.screenDeviceName = screenDeviceName;
|
||||
|
||||
Left = settings.Position.X;
|
||||
Top = settings.Position.Y;
|
||||
Width = settings.Size.X;
|
||||
Height = settings.Size.Y;
|
||||
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;
|
||||
|
||||
PluginFolderPath = pluginFolderPath;
|
||||
|
||||
@@ -263,14 +280,71 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
settings.Position = new System.Windows.Point(Left, Top);
|
||||
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);
|
||||
}
|
||||
|
||||
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
settings.Size = new System.Windows.Point(Width, Height);
|
||||
if (!isUpdatingPosition)
|
||||
{
|
||||
settings.Size = ScreenUtilities.SizeToPercent(new Point(Width, Height), screenBounds);
|
||||
}
|
||||
|
||||
tileBar.CaptionHeight = ActualHeight - 10;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
<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>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<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,5 +1,6 @@
|
||||
using DesktopMagic.Plugins;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
@@ -22,7 +23,13 @@ public class DesktopMagicSettings : INotifyPropertyChanged
|
||||
init
|
||||
{
|
||||
themes = value;
|
||||
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
|
||||
themes.CollectionChanged += (s, e) =>
|
||||
{
|
||||
foreach (Layout layout in layouts)
|
||||
{
|
||||
layout.UpdateTheme();
|
||||
}
|
||||
};
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
@@ -62,6 +69,18 @@ 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; }
|
||||
@@ -70,7 +89,13 @@ public class DesktopMagicSettings : INotifyPropertyChanged
|
||||
|
||||
public DesktopMagicSettings()
|
||||
{
|
||||
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
|
||||
themes.CollectionChanged += (s, e) =>
|
||||
{
|
||||
foreach (Layout layout in layouts)
|
||||
{
|
||||
layout.UpdateTheme();
|
||||
}
|
||||
};
|
||||
|
||||
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayout));
|
||||
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayoutName));
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -60,6 +61,23 @@ 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,20 +19,24 @@ public class PluginSettings : INotifyPropertyChanged
|
||||
private List<SettingElement> settings = [];
|
||||
private Dictionary<string, JsonElement> state = [];
|
||||
private bool enabled = false;
|
||||
private Point position = new Point(100, 100);
|
||||
private Point size = new Point(300, 300);
|
||||
private Point position = new Point(0.05, 0.05);
|
||||
private Point size = new Point(0.3, 0.3);
|
||||
|
||||
// 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) ?? settings.CurrentLayout.Theme;
|
||||
return settings.Themes.FirstOrDefault(t => t.Name == currentThemeName) ?? Owner?.Theme ?? settings.CurrentLayout.Theme;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +97,9 @@ 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;
|
||||
@@ -106,6 +113,9 @@ 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;
|
||||
|
||||
Reference in New Issue
Block a user