mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 08:56:15 +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.Helpers;
|
||||||
using DesktopMagic.Settings;
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace DesktopMagic.DataContexts;
|
namespace DesktopMagic.DataContexts;
|
||||||
@@ -11,6 +14,8 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
|||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
private static DesktopMagicSettings settings = new();
|
private static DesktopMagicSettings settings = new();
|
||||||
|
private static string? selectedScreenDeviceName;
|
||||||
|
private string? selectedLayoutName;
|
||||||
|
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private string? pluginsSearchText;
|
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
|
public bool IsLoading
|
||||||
{
|
{
|
||||||
get => isLoading;
|
get => isLoading;
|
||||||
@@ -76,8 +105,67 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
|||||||
return settings;
|
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)
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
{
|
{
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+215
-21
@@ -5,6 +5,7 @@ using DesktopMagic.Settings;
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
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
|
// Plugin management
|
||||||
private readonly Dictionary<uint, InternalPluginData> _plugins = [];
|
private readonly Dictionary<uint, InternalPluginData> _plugins = [];
|
||||||
private readonly Dictionary<PluginMetadata, Type> _builtInPlugins = new()
|
private readonly Dictionary<PluginMetadata, Type> _builtInPlugins = new()
|
||||||
@@ -65,6 +69,13 @@ public sealed class Manager
|
|||||||
public DesktopMagicSettings Settings { get; set; } = new();
|
public DesktopMagicSettings Settings { get; set; } = new();
|
||||||
public bool IsLoaded { get; set; } = false;
|
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()
|
private readonly JsonSerializerOptions _jsonSettingsOptions = new()
|
||||||
{
|
{
|
||||||
Converters = { new ColorJsonConverter() }
|
Converters = { new ColorJsonConverter() }
|
||||||
@@ -137,20 +148,46 @@ public sealed class Manager
|
|||||||
App.Logger.LogInfo($"Loaded {_plugins.Count} plugins", source: "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))
|
if (!_plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
if (!layout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||||
{
|
{
|
||||||
pluginSettings = new 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)
|
if (existingWindow is not null || !pluginSettings.Enabled)
|
||||||
{
|
{
|
||||||
@@ -176,21 +213,21 @@ public sealed class Manager
|
|||||||
|
|
||||||
if (_builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
|
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()
|
Title = internalPluginData.Metadata.Id.ToString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (internalPluginData.Type == PluginType.Web)
|
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()
|
Title = internalPluginData.Metadata.Id.ToString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
|
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath, screenBounds, screenDeviceName)
|
||||||
{
|
{
|
||||||
Title = internalPluginData.Metadata.Id.ToString()
|
Title = internalPluginData.Metadata.Id.ToString()
|
||||||
};
|
};
|
||||||
@@ -205,10 +242,21 @@ public sealed class Manager
|
|||||||
|
|
||||||
Action exitHandler = () =>
|
Action exitHandler = () =>
|
||||||
{
|
{
|
||||||
PluginWindows.Remove(window);
|
// Close the widget on every screen using this layout
|
||||||
BlockWindowsClosing = false;
|
foreach (System.Windows.Forms.Screen sharedScreen in ScreenUtilities.GetAllScreens())
|
||||||
window.Close();
|
{
|
||||||
BlockWindowsClosing = true;
|
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;
|
||||||
|
sharedWindow.Close();
|
||||||
|
BlockWindowsClosing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
pluginSettings.Enabled = false;
|
pluginSettings.Enabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -255,7 +303,9 @@ public sealed class Manager
|
|||||||
{
|
{
|
||||||
Settings = new DesktopMagicSettings();
|
Settings = new DesktopMagicSettings();
|
||||||
Settings.Layouts.Add(new Layout("Default"));
|
Settings.Layouts.Add(new Layout("Default"));
|
||||||
|
Settings.Layouts.Add(new Layout(EmptyLayoutName));
|
||||||
Settings.Themes.Add(new Theme("Default"));
|
Settings.Themes.Add(new Theme("Default"));
|
||||||
|
Settings.SchemaVersion = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,9 +322,62 @@ public sealed class Manager
|
|||||||
Settings.Themes.Add(new Theme("Default"));
|
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();
|
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()
|
public void SaveSettings()
|
||||||
{
|
{
|
||||||
if (!IsLoaded)
|
if (!IsLoaded)
|
||||||
@@ -292,9 +395,60 @@ public sealed class Manager
|
|||||||
|
|
||||||
#region Layout Management
|
#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)
|
public void LoadLayout(Action? onComplete = null)
|
||||||
{
|
{
|
||||||
App.Logger.LogInfo("Loading layout", source: "Manager");
|
App.Logger.LogInfo("Loading layouts", source: "Manager");
|
||||||
BlockWindowsClosing = false;
|
BlockWindowsClosing = false;
|
||||||
|
|
||||||
foreach (IPluginWindow window in PluginWindows)
|
foreach (IPluginWindow window in PluginWindows)
|
||||||
@@ -305,37 +459,77 @@ public sealed class Manager
|
|||||||
BlockWindowsClosing = true;
|
BlockWindowsClosing = true;
|
||||||
PluginWindows.Clear();
|
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
|
// Load plugins
|
||||||
foreach (uint pluginId in _plugins.Keys)
|
foreach (uint pluginId in _plugins.Keys)
|
||||||
{
|
{
|
||||||
InternalPluginData internalPluginData = _plugins[pluginId];
|
InternalPluginData internalPluginData = _plugins[pluginId];
|
||||||
|
|
||||||
// Add plugin to layout if it doesn't exist
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginSettings.Metadata = internalPluginData.Metadata;
|
pluginSettings.Metadata = internalPluginData.Metadata;
|
||||||
|
pluginSettings.Owner = layout;
|
||||||
|
|
||||||
if (pluginSettings.Enabled)
|
if (pluginSettings.Enabled)
|
||||||
{
|
{
|
||||||
LoadPlugin(pluginId);
|
EnsurePluginWindow(screen, layout, internalPluginData, pluginSettings, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove plugins that are not loaded anymore
|
// 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)
|
foreach (uint pluginId in pluginIdsToRemove)
|
||||||
{
|
{
|
||||||
Settings.CurrentLayout.Plugins.Remove(pluginId);
|
layout.Plugins.Remove(pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings.CurrentLayout.UpdatePlugins();
|
layout.UpdatePlugins();
|
||||||
|
|
||||||
onComplete?.Invoke();
|
|
||||||
App.Logger.LogInfo("Layout loaded", source: "Manager");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</ui:Card>
|
</ui:Card>
|
||||||
|
|
||||||
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel" Grid.Row="1" VerticalAlignment="Stretch" VerticalContentAlignment="Top">
|
<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>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
|
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
|
||||||
@@ -69,18 +69,25 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<ui:Card Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
<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>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="1*" />
|
<ColumnDefinition Width="1*" />
|
||||||
<ColumnDefinition Width="5" />
|
<ColumnDefinition Width="5" />
|
||||||
<ColumnDefinition Width="0.5*" />
|
<ColumnDefinition Width="1*" />
|
||||||
<ColumnDefinition Width="5" />
|
|
||||||
<ColumnDefinition Width="0.5*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
</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" />
|
<ComboBox x:Name="screensComboBox" ItemsSource="{Binding Screens}" DisplayMemberPath="DisplayName" SelectedValue="{Binding SelectedScreenId}" SelectedValuePath="DeviceName" VerticalAlignment="Center" />
|
||||||
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Grid.Column="2" Icon="{ui:SymbolIcon Add24}" HorizontalAlignment="Stretch" Click="NewLayoutButton_Click" FontWeight="Regular"/>
|
<ComboBox x:Name="layoutsComboBox" Grid.Column="2" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding SelectedLayoutName}" SelectedValuePath="Name" VerticalAlignment="Center" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
||||||
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="RemoveLayoutButton_Click" FontWeight="Regular"/>
|
|
||||||
|
<StackPanel Grid.Row="2" Grid.ColumnSpan="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Icon="{ui:SymbolIcon Add24}" Margin="0,0,5,0" Click="NewLayoutButton_Click" FontWeight="Regular" />
|
||||||
|
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Margin="0,0,5,0" Click="RemoveLayoutButton_Click" FontWeight="Regular" />
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ui:Card>
|
</ui:Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ public partial class MainPage : Page
|
|||||||
{
|
{
|
||||||
private readonly Manager _manager = Manager.Instance;
|
private readonly Manager _manager = Manager.Instance;
|
||||||
private readonly MainWindowDataContext _dataContext;
|
private readonly MainWindowDataContext _dataContext;
|
||||||
private bool _isLoadingLayout = false;
|
|
||||||
|
|
||||||
public MainPage()
|
public MainPage()
|
||||||
{
|
{
|
||||||
@@ -45,6 +44,8 @@ public partial class MainPage : Page
|
|||||||
{
|
{
|
||||||
// Initialize edit checkbox state
|
// Initialize edit checkbox state
|
||||||
editCheckBox.IsChecked = _manager.IsEditMode;
|
editCheckBox.IsChecked = _manager.IsEditMode;
|
||||||
|
|
||||||
|
_dataContext.RefreshScreens();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
|
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
|
||||||
@@ -85,7 +86,7 @@ public partial class MainPage : Page
|
|||||||
|
|
||||||
uint pluginId = uint.Parse(checkBox.Tag.ToString()!);
|
uint pluginId = uint.Parse(checkBox.Tag.ToString()!);
|
||||||
|
|
||||||
_manager.LoadPlugin(pluginId, (internalPluginData) =>
|
_manager.LoadPlugin(pluginId, _manager.SelectedLayout, (internalPluginData) =>
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
@@ -145,32 +146,46 @@ public partial class MainPage : Page
|
|||||||
|
|
||||||
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
// Prevent recursive calls and only process if fully loaded
|
ApplySelectedLayout();
|
||||||
if (_isLoadingLayout || !_manager.IsLoaded || !IsLoaded)
|
}
|
||||||
|
|
||||||
|
/// <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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is actually a user-initiated change
|
System.Windows.Forms.Screen? screen = ScreenUtilities.GetScreenByDeviceName(_dataContext.SelectedScreenId);
|
||||||
// by verifying that the removed and added items are different
|
if (screen is null)
|
||||||
if (e.RemovedItems.Count > 0 && e.AddedItems.Count > 0)
|
|
||||||
{
|
{
|
||||||
if (e.RemovedItems[0] == e.AddedItems[0])
|
return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
string? layoutName = _dataContext.SelectedLayoutName;
|
||||||
|
if (layoutName is null)
|
||||||
{
|
{
|
||||||
_isLoadingLayout = true;
|
return;
|
||||||
_manager.SaveSettings();
|
|
||||||
_manager.LoadLayout();
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
Layout? layout = _manager.Settings.Layouts.FirstOrDefault(l => l.Name == layoutName);
|
||||||
|
if (layout is null)
|
||||||
{
|
{
|
||||||
_isLoadingLayout = false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_manager.GetLayoutForScreen(screen) == layout)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_manager.BindLayoutToScreen(screen, layout);
|
||||||
|
_manager.ReloadScreen(screen);
|
||||||
|
_dataContext.RefreshSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void NewLayoutButton_Click(object sender, RoutedEventArgs e)
|
private async void NewLayoutButton_Click(object sender, RoutedEventArgs e)
|
||||||
@@ -194,18 +209,11 @@ public partial class MainPage : Page
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
_manager.Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
|
||||||
{
|
_manager.SaveSettings();
|
||||||
_isLoadingLayout = true;
|
|
||||||
_manager.Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
|
// Select the new layout so the user can apply it to the current screen
|
||||||
_manager.Settings.CurrentLayoutName = inputDialog.ResponseText.Trim();
|
_dataContext.SelectedLayoutName = inputDialog.ResponseText.Trim();
|
||||||
_manager.SaveSettings();
|
|
||||||
_manager.LoadLayout();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isLoadingLayout = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,17 +245,40 @@ public partial class MainPage : Page
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
Layout? layout = _manager.Settings.Layouts.FirstOrDefault(l => l.Name == _dataContext.SelectedLayoutName);
|
||||||
|
if (layout is null)
|
||||||
{
|
{
|
||||||
_isLoadingLayout = true;
|
return;
|
||||||
_ = _manager.Settings.Layouts.Remove(_manager.Settings.CurrentLayout);
|
|
||||||
_manager.SaveSettings();
|
|
||||||
_manager.LoadLayout();
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
if (layout.Name == Manager.EmptyLayoutName)
|
||||||
{
|
{
|
||||||
_isLoadingLayout = false;
|
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();
|
||||||
|
_dataContext.RefreshSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<RowDefinition Height="auto" />
|
<RowDefinition Height="auto" />
|
||||||
</Grid.RowDefinitions>
|
</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>
|
<ui:ListView.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public partial class ThemePage : Page
|
|||||||
}
|
}
|
||||||
|
|
||||||
_manager.Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
|
_manager.Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
|
||||||
_manager.Settings.CurrentLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
|
_manager.SelectedLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
|
||||||
_manager.SaveSettings();
|
_manager.SaveSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public interface IPluginWindow
|
|||||||
PluginMetadata PluginMetadata { get; }
|
PluginMetadata PluginMetadata { get; }
|
||||||
string PluginFolderPath { get; }
|
string PluginFolderPath { get; }
|
||||||
string Title { get; set; }
|
string Title { get; set; }
|
||||||
|
string ScreenDeviceName { get; }
|
||||||
|
|
||||||
void Exit();
|
void Exit();
|
||||||
void SetEditMode(bool enabled);
|
void SetEditMode(bool enabled);
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ public partial class PluginWindow : Window, IPluginWindow
|
|||||||
private Plugin? pluginClassInstance;
|
private Plugin? pluginClassInstance;
|
||||||
private AssemblyLoadContext assemblyLoadContext;
|
private AssemblyLoadContext assemblyLoadContext;
|
||||||
|
|
||||||
|
private readonly Rectangle screenBounds;
|
||||||
|
private readonly string screenDeviceName;
|
||||||
|
private bool isUpdatingPosition = false;
|
||||||
|
|
||||||
private CancellationTokenSource? pluginCancellationTokenSource;
|
private CancellationTokenSource? pluginCancellationTokenSource;
|
||||||
private FileSystemWatcher? pluginFileWatcher;
|
private FileSystemWatcher? pluginFileWatcher;
|
||||||
private System.Timers.Timer? reloadDebounceTimer;
|
private System.Timers.Timer? reloadDebounceTimer;
|
||||||
@@ -50,8 +54,9 @@ public partial class PluginWindow : Window, IPluginWindow
|
|||||||
public bool IsRunning { get; private set; } = true;
|
public bool IsRunning { get; private set; } = true;
|
||||||
public PluginMetadata PluginMetadata { get; private set; }
|
public PluginMetadata PluginMetadata { get; private set; }
|
||||||
public string PluginFolderPath { 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();
|
InitializeComponent();
|
||||||
|
|
||||||
@@ -81,6 +86,14 @@ public partial class PluginWindow : Window, IPluginWindow
|
|||||||
};
|
};
|
||||||
ThemeChanged();
|
ThemeChanged();
|
||||||
}
|
}
|
||||||
|
else if (s.PropertyName == nameof(PluginSettings.Position))
|
||||||
|
{
|
||||||
|
UpdatePosition();
|
||||||
|
}
|
||||||
|
else if (s.PropertyName == nameof(PluginSettings.Size))
|
||||||
|
{
|
||||||
|
UpdateSize();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
settings.Theme.PropertyChanged += (se, ev) =>
|
settings.Theme.PropertyChanged += (se, ev) =>
|
||||||
@@ -90,11 +103,15 @@ public partial class PluginWindow : Window, IPluginWindow
|
|||||||
|
|
||||||
PluginMetadata = pluginMetadata;
|
PluginMetadata = pluginMetadata;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
this.screenBounds = screenBounds;
|
||||||
|
this.screenDeviceName = screenDeviceName;
|
||||||
|
|
||||||
Left = settings.Position.X;
|
System.Windows.Point position = ScreenUtilities.PercentToPosition(settings.Position, screenBounds);
|
||||||
Top = settings.Position.Y;
|
System.Windows.Point size = ScreenUtilities.PercentSizeToSize(settings.Size, screenBounds);
|
||||||
Width = settings.Size.X;
|
Left = position.X;
|
||||||
Height = settings.Size.Y;
|
Top = position.Y;
|
||||||
|
Width = size.X;
|
||||||
|
Height = size.Y;
|
||||||
|
|
||||||
PluginFolderPath = pluginFolderPath;
|
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;
|
this.pluginClassInstance = pluginClassInstance;
|
||||||
}
|
}
|
||||||
@@ -993,16 +1010,73 @@ public partial class PluginWindow : Window, IPluginWindow
|
|||||||
_ = StopPlugin(unloadAssembly: true);
|
_ = 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
|
#region Window Events
|
||||||
|
|
||||||
private void Window_LocationChanged(object sender, EventArgs e)
|
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)
|
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;
|
tileBar.CaptionHeight = ActualHeight - 10;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,16 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
|||||||
private System.Timers.Timer? reloadDebounceTimer;
|
private System.Timers.Timer? reloadDebounceTimer;
|
||||||
private bool isReloading = false;
|
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 bool IsRunning { get; private set; } = true;
|
||||||
public PluginMetadata PluginMetadata { get; private set; }
|
public PluginMetadata PluginMetadata { get; private set; }
|
||||||
public string PluginFolderPath { 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();
|
InitializeComponent();
|
||||||
|
|
||||||
@@ -63,6 +68,14 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
|||||||
};
|
};
|
||||||
ThemeChanged();
|
ThemeChanged();
|
||||||
}
|
}
|
||||||
|
else if (s.PropertyName == nameof(PluginSettings.Position))
|
||||||
|
{
|
||||||
|
UpdatePosition();
|
||||||
|
}
|
||||||
|
else if (s.PropertyName == nameof(PluginSettings.Size))
|
||||||
|
{
|
||||||
|
UpdateSize();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
settings.Theme.PropertyChanged += (se, ev) =>
|
settings.Theme.PropertyChanged += (se, ev) =>
|
||||||
@@ -72,11 +85,15 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
|||||||
|
|
||||||
PluginMetadata = pluginMetadata;
|
PluginMetadata = pluginMetadata;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
this.screenBounds = screenBounds;
|
||||||
|
this.screenDeviceName = screenDeviceName;
|
||||||
|
|
||||||
Left = settings.Position.X;
|
Point position = ScreenUtilities.PercentToPosition(settings.Position, screenBounds);
|
||||||
Top = settings.Position.Y;
|
Point size = ScreenUtilities.PercentSizeToSize(settings.Size, screenBounds);
|
||||||
Width = settings.Size.X;
|
Left = position.X;
|
||||||
Height = settings.Size.Y;
|
Top = position.Y;
|
||||||
|
Width = size.X;
|
||||||
|
Height = size.Y;
|
||||||
|
|
||||||
PluginFolderPath = pluginFolderPath;
|
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)
|
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)
|
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;
|
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="confirmDeleteTheme">Möchten Sie dieses Theme wirklich löschen?</system:String>
|
||||||
<system:String x:Key="enterLayoutName">Layoutnamen eingeben:</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="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="enterThemeName">Themenamen eingeben:</system:String>
|
||||||
<system:String x:Key="themeAlreadyExists">Theme existiert bereits!</system:String>
|
<system:String x:Key="themeAlreadyExists">Theme existiert bereits!</system:String>
|
||||||
<system:String x:Key="install">Installieren</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="enterLayoutName">Enter layout name</system:String>
|
||||||
<system:String x:Key="layoutAlreadyExists">Layout already exists!</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="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="enterThemeName">Enter theme name</system:String>
|
||||||
<system:String x:Key="themeAlreadyExists">Theme already exists!</system:String>
|
<system:String x:Key="themeAlreadyExists">Theme already exists!</system:String>
|
||||||
<system:String x:Key="cannotDeleteLastTheme">Can't delete last theme!</system:String>
|
<system:String x:Key="cannotDeleteLastTheme">Can't delete last theme!</system:String>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using DesktopMagic.Plugins;
|
using DesktopMagic.Plugins;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -22,7 +23,13 @@ public class DesktopMagicSettings : INotifyPropertyChanged
|
|||||||
init
|
init
|
||||||
{
|
{
|
||||||
themes = value;
|
themes = value;
|
||||||
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
|
themes.CollectionChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
foreach (Layout layout in layouts)
|
||||||
|
{
|
||||||
|
layout.UpdateTheme();
|
||||||
|
}
|
||||||
|
};
|
||||||
OnPropertyChanged();
|
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? ModIoAccessToken { get; set; }
|
||||||
|
|
||||||
public string? ReleaseInfoLastAppVersion { get; set; }
|
public string? ReleaseInfoLastAppVersion { get; set; }
|
||||||
@@ -70,7 +89,13 @@ public class DesktopMagicSettings : INotifyPropertyChanged
|
|||||||
|
|
||||||
public DesktopMagicSettings()
|
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(CurrentLayout));
|
||||||
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayoutName));
|
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayoutName));
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class Layout(string name) : INotifyPropertyChanged
|
|||||||
private string name = name;
|
private string name = name;
|
||||||
private string? currentThemeName = null;
|
private string? currentThemeName = null;
|
||||||
private Dictionary<uint, PluginSettings> plugins = [];
|
private Dictionary<uint, PluginSettings> plugins = [];
|
||||||
|
private double screenAspectRatio = 0;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Theme Theme
|
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()
|
public void UpdatePlugins()
|
||||||
{
|
{
|
||||||
plugins = plugins.ToDictionary();
|
plugins = plugins.ToDictionary();
|
||||||
|
|||||||
@@ -19,20 +19,24 @@ public class PluginSettings : INotifyPropertyChanged
|
|||||||
private List<SettingElement> settings = [];
|
private List<SettingElement> settings = [];
|
||||||
private Dictionary<string, JsonElement> state = [];
|
private Dictionary<string, JsonElement> state = [];
|
||||||
private bool enabled = false;
|
private bool enabled = false;
|
||||||
private Point position = new Point(100, 100);
|
private Point position = new Point(0.05, 0.05);
|
||||||
private Point size = new Point(300, 300);
|
private Point size = new Point(0.3, 0.3);
|
||||||
|
|
||||||
// Only for internal use to show the name of the plugin in the main window
|
// Only for internal use to show the name of the plugin in the main window
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public PluginMetadata Metadata { get; set; } = new();
|
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]
|
[JsonIgnore]
|
||||||
public Theme Theme
|
public Theme Theme
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
DesktopMagicSettings settings = MainWindowDataContext.GetSettings();
|
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
|
public Point Position
|
||||||
{
|
{
|
||||||
get => 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
|
public Point Size
|
||||||
{
|
{
|
||||||
get => size;
|
get => size;
|
||||||
|
|||||||
Reference in New Issue
Block a user