mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-24 18:44:38 +08:00
feat.融合桌面组件展示优化
This commit is contained in:
@@ -186,3 +186,7 @@ Fusion desktop placement must reuse the existing Lan Mountain desktop grid setti
|
|||||||
### Requirement: Snap individual windows to the grid
|
### Requirement: Snap individual windows to the grid
|
||||||
|
|
||||||
Fusion desktop no longer displays or depends on a full-screen grid window. Each component window uses the grid only as an individual placement constraint. Dragging remains free while the pointer is moving; on release, the window snaps to the nearest cell that can contain its saved cell span, clamps inside the current screen grid, and persists `X`, `Y`, `GridRow`, `GridColumn`, `GridWidthCells`, and `GridHeightCells`.
|
Fusion desktop no longer displays or depends on a full-screen grid window. Each component window uses the grid only as an individual placement constraint. Dragging remains free while the pointer is moving; on release, the window snaps to the nearest cell that can contain its saved cell span, clamps inside the current screen grid, and persists `X`, `Y`, `GridRow`, `GridColumn`, `GridWidthCells`, and `GridHeightCells`.
|
||||||
|
|
||||||
|
### Requirement: Preview area preserves widget proportions
|
||||||
|
|
||||||
|
The fused desktop component library preview area must size the selected widget from its component cell span instead of compressing every widget into a fixed preview box. The preview stage should stretch with the resizable library window, calculate the largest usable widget preview that fits the available stage, preserve the `MinWidthCells` / `MinHeightCells` ratio, and assign explicit preview control width and height before displaying the widget.
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ internal static class NativeDependencyBootstrapper
|
|||||||
"libSkiaSharp.dll"
|
"libSkiaSharp.dll"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static void Prepare()
|
public static bool TryPrepare()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsWindows())
|
if (!OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var nativeDirectory = GetNativeDirectory();
|
var nativeDirectory = GetNativeDirectory();
|
||||||
Directory.CreateDirectory(nativeDirectory);
|
Directory.CreateDirectory(nativeDirectory);
|
||||||
|
|
||||||
@@ -40,6 +42,14 @@ internal static class NativeDependencyBootstrapper
|
|||||||
{
|
{
|
||||||
NativeLibrary.Load(libraryPath);
|
NativeLibrary.Load(libraryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[NativeDependencyBootstrapper] Failed to prepare native dependencies: {ex}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetNativeDirectory()
|
private static string GetNativeDirectory()
|
||||||
|
|||||||
@@ -7,9 +7,19 @@ public static class Program
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
NativeDependencyBootstrapper.Prepare();
|
try
|
||||||
|
{
|
||||||
|
if (!NativeDependencyBootstrapper.TryPrepare())
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Program] Failed to prepare native dependencies, but continuing...");
|
||||||
|
}
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[Program] Unhandled exception: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ public sealed class PluginDesktopComponentOptions
|
|||||||
|
|
||||||
public string? DisplayNameLocalizationKey { get; init; }
|
public string? DisplayNameLocalizationKey { get; init; }
|
||||||
|
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
public string? DescriptionLocalizationKey { get; init; }
|
||||||
|
|
||||||
public PluginCornerRadiusPreset CornerRadiusPreset { get; init; } = PluginCornerRadiusPreset.Default;
|
public PluginCornerRadiusPreset CornerRadiusPreset { get; init; } = PluginCornerRadiusPreset.Default;
|
||||||
|
|
||||||
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; init; }
|
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; init; }
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ public sealed class PluginDesktopComponentRegistration
|
|||||||
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(options.DisplayNameLocalizationKey)
|
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(options.DisplayNameLocalizationKey)
|
||||||
? null
|
? null
|
||||||
: options.DisplayNameLocalizationKey.Trim();
|
: options.DisplayNameLocalizationKey.Trim();
|
||||||
|
Description = string.IsNullOrWhiteSpace(options.Description)
|
||||||
|
? null
|
||||||
|
: options.Description.Trim();
|
||||||
|
DescriptionLocalizationKey = string.IsNullOrWhiteSpace(options.DescriptionLocalizationKey)
|
||||||
|
? null
|
||||||
|
: options.DescriptionLocalizationKey.Trim();
|
||||||
ControlFactory = controlFactory;
|
ControlFactory = controlFactory;
|
||||||
IconKey = options.IconKey.Trim();
|
IconKey = options.IconKey.Trim();
|
||||||
Category = options.Category.Trim();
|
Category = options.Category.Trim();
|
||||||
@@ -45,6 +51,10 @@ public sealed class PluginDesktopComponentRegistration
|
|||||||
|
|
||||||
public string? DisplayNameLocalizationKey { get; }
|
public string? DisplayNameLocalizationKey { get; }
|
||||||
|
|
||||||
|
public string? Description { get; }
|
||||||
|
|
||||||
|
public string? DescriptionLocalizationKey { get; }
|
||||||
|
|
||||||
public Func<IServiceProvider, PluginDesktopComponentContext, Control> ControlFactory { get; }
|
public Func<IServiceProvider, PluginDesktopComponentContext, Control> ControlFactory { get; }
|
||||||
|
|
||||||
public string IconKey { get; }
|
public string IconKey { get; }
|
||||||
|
|||||||
216
LanMountainDesktop.Tests/FusedDesktopLibraryMetadataTests.cs
Normal file
216
LanMountainDesktop.Tests/FusedDesktopLibraryMetadataTests.cs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.ComponentSystem.Extensions;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
using LanMountainDesktop.Views.Components;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Tests;
|
||||||
|
|
||||||
|
public sealed class FusedDesktopLibraryMetadataTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _tempRoot = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
"LanMountainDesktop.Tests",
|
||||||
|
nameof(FusedDesktopLibraryMetadataTests),
|
||||||
|
Guid.NewGuid().ToString("N"));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PluginDesktopComponentDescriptionMetadata_ReachesRuntimeDescriptor()
|
||||||
|
{
|
||||||
|
const string componentId = "plugin.metadata.widget";
|
||||||
|
var registration = new PluginDesktopComponentRegistration(
|
||||||
|
_ => new Border(),
|
||||||
|
new PluginDesktopComponentOptions
|
||||||
|
{
|
||||||
|
ComponentId = componentId,
|
||||||
|
DisplayName = "Metadata Widget",
|
||||||
|
IconKey = "Apps",
|
||||||
|
Category = "Plugins",
|
||||||
|
Description = "Plugin supplied description.",
|
||||||
|
DescriptionLocalizationKey = "plugin.metadata.description"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("Plugin supplied description.", registration.Description);
|
||||||
|
Assert.Equal("plugin.metadata.description", registration.DescriptionLocalizationKey);
|
||||||
|
|
||||||
|
var registry = ComponentRegistry.CreateDefault().RegisterComponents(
|
||||||
|
[
|
||||||
|
new DesktopComponentDefinition(
|
||||||
|
registration.ComponentId,
|
||||||
|
registration.DisplayName,
|
||||||
|
registration.IconKey,
|
||||||
|
registration.Category,
|
||||||
|
registration.MinWidthCells,
|
||||||
|
registration.MinHeightCells,
|
||||||
|
registration.AllowStatusBarPlacement,
|
||||||
|
registration.AllowDesktopPlacement,
|
||||||
|
Description: registration.Description,
|
||||||
|
DescriptionLocalizationKey: registration.DescriptionLocalizationKey)
|
||||||
|
]);
|
||||||
|
var runtimeRegistry = new DesktopComponentRuntimeRegistry(
|
||||||
|
registry,
|
||||||
|
[
|
||||||
|
new DesktopComponentRuntimeRegistration(
|
||||||
|
componentId,
|
||||||
|
displayNameLocalizationKey: registration.DisplayNameLocalizationKey,
|
||||||
|
_ => new Border(),
|
||||||
|
cornerRadiusResolver: (Func<double, double>?)null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
Assert.True(runtimeRegistry.TryGetDescriptor(componentId, out var descriptor));
|
||||||
|
Assert.Equal("Plugin supplied description.", descriptor.Description);
|
||||||
|
Assert.Equal("plugin.metadata.description", descriptor.DescriptionLocalizationKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonComponentExtensionProvider_LoadsOptionalDescriptionMetadata()
|
||||||
|
{
|
||||||
|
var extensionDirectory = Path.Combine(_tempRoot, "extensions");
|
||||||
|
Directory.CreateDirectory(extensionDirectory);
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(extensionDirectory, "components.json"),
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "json.description.widget",
|
||||||
|
"displayName": "JSON Description Widget",
|
||||||
|
"iconKey": "Apps",
|
||||||
|
"category": "Extensions",
|
||||||
|
"description": "Description from JSON.",
|
||||||
|
"descriptionLocalizationKey": "json.description.widget.description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "json.default.widget",
|
||||||
|
"displayName": "JSON Default Widget",
|
||||||
|
"description": " ",
|
||||||
|
"descriptionLocalizationKey": " "
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""");
|
||||||
|
|
||||||
|
var definitions = JsonComponentExtensionProvider
|
||||||
|
.LoadProvidersFromDirectory(extensionDirectory)
|
||||||
|
.SelectMany(provider => provider.GetComponents())
|
||||||
|
.ToDictionary(definition => definition.Id, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var described = definitions["json.description.widget"];
|
||||||
|
Assert.Equal("Description from JSON.", described.Description);
|
||||||
|
Assert.Equal("json.description.widget.description", described.DescriptionLocalizationKey);
|
||||||
|
|
||||||
|
var defaults = definitions["json.default.widget"];
|
||||||
|
Assert.Null(defaults.Description);
|
||||||
|
Assert.Null(defaults.DescriptionLocalizationKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FusedDesktopLibraryLocalizationFiles_ContainRequiredKeys()
|
||||||
|
{
|
||||||
|
var requiredKeys = new[]
|
||||||
|
{
|
||||||
|
"fused_desktop.library.title",
|
||||||
|
"fused_desktop.library.add_button",
|
||||||
|
"fused_desktop.library.find_more",
|
||||||
|
"fused_desktop.library.empty_selection",
|
||||||
|
"fused_desktop.library.component_summary_format"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var language in new[] { "zh-CN", "en-US", "ja-JP", "ko-KR" })
|
||||||
|
{
|
||||||
|
var json = ReadRepositoryFile("LanMountainDesktop", "Localization", $"{language}.json");
|
||||||
|
var table = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||||
|
Assert.NotNull(table);
|
||||||
|
foreach (var key in requiredKeys)
|
||||||
|
{
|
||||||
|
Assert.True(table!.ContainsKey(key), $"{language} is missing {key}.");
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(table[key]), $"{language} has an empty {key}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FusedDesktopLibraryLifecycle_KeepsAddOpenAndUsesEditModeBoundary()
|
||||||
|
{
|
||||||
|
var appSource = ReadRepositoryFile("LanMountainDesktop", "App.axaml.cs");
|
||||||
|
var openSource = ExtractMethodSource(appSource, "OpenFusedDesktopComponentLibraryFromUi");
|
||||||
|
var closedSource = ExtractMethodSource(appSource, "OnFusedComponentLibraryWindowClosed");
|
||||||
|
var librarySource = ReadRepositoryFile("LanMountainDesktop", "Views", "FusedDesktopComponentLibraryWindow.axaml.cs");
|
||||||
|
var addSource = ExtractMethodSource(librarySource, "OnAddComponentRequested");
|
||||||
|
|
||||||
|
Assert.Contains("EnterEditMode()", openSource);
|
||||||
|
Assert.Contains("ExitEditMode()", closedSource);
|
||||||
|
Assert.Contains("AddComponent(componentId, this)", addSource);
|
||||||
|
Assert.DoesNotContain("Close()", addSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_tempRoot))
|
||||||
|
{
|
||||||
|
Directory.Delete(_tempRoot, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadRepositoryFile(params string[] segments)
|
||||||
|
{
|
||||||
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null)
|
||||||
|
{
|
||||||
|
var candidate = Path.Combine(new[] { directory.FullName }.Concat(segments).ToArray());
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
{
|
||||||
|
return File.ReadAllText(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "LanMountainDesktop.slnx")))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FileNotFoundException($"Could not locate repository file '{Path.Combine(segments)}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractMethodSource(string source, string methodName)
|
||||||
|
{
|
||||||
|
var methodIndex = source.IndexOf($"private void {methodName}(", StringComparison.Ordinal);
|
||||||
|
if (methodIndex < 0)
|
||||||
|
{
|
||||||
|
methodIndex = source.IndexOf($"private bool {methodName}(", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(methodIndex >= 0, $"Could not locate method '{methodName}'.");
|
||||||
|
|
||||||
|
var braceIndex = source.IndexOf('{', methodIndex);
|
||||||
|
Assert.True(braceIndex >= 0, $"Could not locate method body for '{methodName}'.");
|
||||||
|
|
||||||
|
var depth = 0;
|
||||||
|
for (var i = braceIndex; i < source.Length; i++)
|
||||||
|
{
|
||||||
|
if (source[i] == '{')
|
||||||
|
{
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
else if (source[i] == '}')
|
||||||
|
{
|
||||||
|
depth--;
|
||||||
|
if (depth == 0)
|
||||||
|
{
|
||||||
|
return source.Substring(methodIndex, i - methodIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Could not extract method '{methodName}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using LanMountainDesktop.Views;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Tests;
|
||||||
|
|
||||||
|
public sealed class FusedDesktopLibraryPreviewLayoutTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Calculate_PreservesLandscapeComponentRatio()
|
||||||
|
{
|
||||||
|
var metrics = FusedDesktopLibraryPreviewLayout.Calculate(
|
||||||
|
widthCells: 4,
|
||||||
|
heightCells: 2,
|
||||||
|
stageWidth: 520,
|
||||||
|
stageHeight: 320);
|
||||||
|
|
||||||
|
Assert.Equal(4, metrics.WidthCells);
|
||||||
|
Assert.Equal(2, metrics.HeightCells);
|
||||||
|
Assert.True(metrics.Width > metrics.Height);
|
||||||
|
Assert.Equal(2d, metrics.Width / metrics.Height, precision: 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Calculate_PreservesPortraitComponentRatio()
|
||||||
|
{
|
||||||
|
var metrics = FusedDesktopLibraryPreviewLayout.Calculate(
|
||||||
|
widthCells: 2,
|
||||||
|
heightCells: 4,
|
||||||
|
stageWidth: 520,
|
||||||
|
stageHeight: 320);
|
||||||
|
|
||||||
|
Assert.Equal(2, metrics.WidthCells);
|
||||||
|
Assert.Equal(4, metrics.HeightCells);
|
||||||
|
Assert.True(metrics.Height > metrics.Width);
|
||||||
|
Assert.Equal(0.5d, metrics.Width / metrics.Height, precision: 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Calculate_FitsPreviewInsideStageInsets()
|
||||||
|
{
|
||||||
|
var metrics = FusedDesktopLibraryPreviewLayout.Calculate(
|
||||||
|
widthCells: 4,
|
||||||
|
heightCells: 4,
|
||||||
|
stageWidth: 420,
|
||||||
|
stageHeight: 260);
|
||||||
|
|
||||||
|
Assert.Equal(metrics.Width, metrics.Height, precision: 3);
|
||||||
|
Assert.True(metrics.Width <= 420);
|
||||||
|
Assert.True(metrics.Height <= 260);
|
||||||
|
Assert.True(metrics.CellSize > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Calculate_UsesFallbackStageWhenBoundsAreNotMeasured()
|
||||||
|
{
|
||||||
|
var metrics = FusedDesktopLibraryPreviewLayout.Calculate(
|
||||||
|
widthCells: 4,
|
||||||
|
heightCells: 2,
|
||||||
|
stageWidth: 0,
|
||||||
|
stageHeight: 0);
|
||||||
|
|
||||||
|
Assert.True(metrics.Width > 0);
|
||||||
|
Assert.True(metrics.Height > 0);
|
||||||
|
Assert.Equal(2d, metrics.Width / metrics.Height, precision: 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,6 @@ public sealed record DesktopComponentDefinition(
|
|||||||
int MinHeightCells,
|
int MinHeightCells,
|
||||||
bool AllowStatusBarPlacement,
|
bool AllowStatusBarPlacement,
|
||||||
bool AllowDesktopPlacement,
|
bool AllowDesktopPlacement,
|
||||||
DesktopComponentResizeMode ResizeMode = DesktopComponentResizeMode.Proportional);
|
DesktopComponentResizeMode ResizeMode = DesktopComponentResizeMode.Proportional,
|
||||||
|
string? Description = null,
|
||||||
|
string? DescriptionLocalizationKey = null);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public sealed class JsonComponentExtensionProvider : IComponentExtensionProvider
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(filePath);
|
var json = File.ReadAllText(filePath);
|
||||||
var entries = JsonSerializer.Deserialize<List<ComponentExtensionEntry>>(json);
|
var entries = JsonSerializer.Deserialize<List<ComponentExtensionEntry>>(json, JsonOptions);
|
||||||
if (entries is null || entries.Count == 0)
|
if (entries is null || entries.Count == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -67,7 +67,9 @@ public sealed class JsonComponentExtensionProvider : IComponentExtensionProvider
|
|||||||
MinWidthCells: Math.Max(1, entry.MinWidthCells),
|
MinWidthCells: Math.Max(1, entry.MinWidthCells),
|
||||||
MinHeightCells: Math.Max(1, entry.MinHeightCells),
|
MinHeightCells: Math.Max(1, entry.MinHeightCells),
|
||||||
AllowStatusBarPlacement: entry.AllowStatusBarPlacement,
|
AllowStatusBarPlacement: entry.AllowStatusBarPlacement,
|
||||||
AllowDesktopPlacement: entry.AllowDesktopPlacement));
|
AllowDesktopPlacement: entry.AllowDesktopPlacement,
|
||||||
|
Description: NormalizeOptional(entry.Description),
|
||||||
|
DescriptionLocalizationKey: NormalizeOptional(entry.DescriptionLocalizationKey)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return definitions.Count == 0 ? null : new JsonComponentExtensionProvider(definitions);
|
return definitions.Count == 0 ? null : new JsonComponentExtensionProvider(definitions);
|
||||||
@@ -78,6 +80,16 @@ public sealed class JsonComponentExtensionProvider : IComponentExtensionProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptional(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
private sealed class ComponentExtensionEntry
|
private sealed class ComponentExtensionEntry
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = string.Empty;
|
public string Id { get; set; } = string.Empty;
|
||||||
@@ -95,5 +107,9 @@ public sealed class JsonComponentExtensionProvider : IComponentExtensionProvider
|
|||||||
public bool AllowStatusBarPlacement { get; set; }
|
public bool AllowStatusBarPlacement { get; set; }
|
||||||
|
|
||||||
public bool AllowDesktopPlacement { get; set; } = true;
|
public bool AllowDesktopPlacement { get; set; } = true;
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public string? DescriptionLocalizationKey { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1031,6 +1031,11 @@
|
|||||||
"component_library.components_none": "No components.",
|
"component_library.components_none": "No components.",
|
||||||
"component_library.drag_hint": "Drag to place",
|
"component_library.drag_hint": "Drag to place",
|
||||||
"component_library.preview_unavailable": "Preview unavailable",
|
"component_library.preview_unavailable": "Preview unavailable",
|
||||||
|
"fused_desktop.library.title": "Add widgets",
|
||||||
|
"fused_desktop.library.add_button": "Add widget",
|
||||||
|
"fused_desktop.library.find_more": "Find more widgets",
|
||||||
|
"fused_desktop.library.empty_selection": "Choose a category to view widgets.",
|
||||||
|
"fused_desktop.library.component_summary_format": "{0} - {1} x {2}",
|
||||||
"component.delete": "Delete",
|
"component.delete": "Delete",
|
||||||
"component.edit": "Edit",
|
"component.edit": "Edit",
|
||||||
"component.move": "Move",
|
"component.move": "Move",
|
||||||
|
|||||||
@@ -752,6 +752,11 @@
|
|||||||
"component_library.title": "ウィジェット",
|
"component_library.title": "ウィジェット",
|
||||||
"component_library.empty": "スワイプしてカテゴリを選択し、タップして開き、ウィジェットをデスクトップにドラッグします。",
|
"component_library.empty": "スワイプしてカテゴリを選択し、タップして開き、ウィジェットをデスクトップにドラッグします。",
|
||||||
"component_library.drag_hint": "ドラッグして配置",
|
"component_library.drag_hint": "ドラッグして配置",
|
||||||
|
"fused_desktop.library.title": "ウィジェットを追加",
|
||||||
|
"fused_desktop.library.add_button": "ウィジェットを追加",
|
||||||
|
"fused_desktop.library.find_more": "さらにウィジェットを探す",
|
||||||
|
"fused_desktop.library.empty_selection": "カテゴリを選択して追加できるウィジェットを表示します。",
|
||||||
|
"fused_desktop.library.component_summary_format": "{0} - {1} x {2}",
|
||||||
"component.delete": "削除",
|
"component.delete": "削除",
|
||||||
"component.edit": "編集",
|
"component.edit": "編集",
|
||||||
"component.editor.instance_scope": "変更はこのコンポーネントインスタンスにのみ適用されます。",
|
"component.editor.instance_scope": "変更はこのコンポーネントインスタンスにのみ適用されます。",
|
||||||
|
|||||||
@@ -799,6 +799,11 @@
|
|||||||
"component_library.title": "바탕화면 편집",
|
"component_library.title": "바탕화면 편집",
|
||||||
"component_library.empty": "좌우로 스와이프하여 카테고리를 선택하고 클릭하여 진입한 후 컴포넌트를 바탕화면에 드래그하여 배치하세요.",
|
"component_library.empty": "좌우로 스와이프하여 카테고리를 선택하고 클릭하여 진입한 후 컴포넌트를 바탕화면에 드래그하여 배치하세요.",
|
||||||
"component_library.drag_hint": "드래그하여 배치",
|
"component_library.drag_hint": "드래그하여 배치",
|
||||||
|
"fused_desktop.library.title": "위젯 추가",
|
||||||
|
"fused_desktop.library.add_button": "위젯 추가",
|
||||||
|
"fused_desktop.library.find_more": "더 많은 위젯 찾기",
|
||||||
|
"fused_desktop.library.empty_selection": "카테고리를 선택하여 추가 가능한 위젯을 확인하세요.",
|
||||||
|
"fused_desktop.library.component_summary_format": "{0} - {1} x {2}",
|
||||||
"component.delete": "삭제",
|
"component.delete": "삭제",
|
||||||
"component.edit": "편집",
|
"component.edit": "편집",
|
||||||
"component.editor.instance_scope": "설정은 현재 컴포넌트 인스턴스에만 적용됩니다.",
|
"component.editor.instance_scope": "설정은 현재 컴포넌트 인스턴스에만 적용됩니다.",
|
||||||
|
|||||||
@@ -962,6 +962,11 @@
|
|||||||
"component_library.components_none": "暂无组件",
|
"component_library.components_none": "暂无组件",
|
||||||
"component_library.drag_hint": "拖动放置",
|
"component_library.drag_hint": "拖动放置",
|
||||||
"component_library.preview_unavailable": "预览不可用",
|
"component_library.preview_unavailable": "预览不可用",
|
||||||
|
"fused_desktop.library.title": "添加小组件",
|
||||||
|
"fused_desktop.library.add_button": "添加小组件",
|
||||||
|
"fused_desktop.library.find_more": "查找更多小组件",
|
||||||
|
"fused_desktop.library.empty_selection": "选择一个分类以查看可添加组件。",
|
||||||
|
"fused_desktop.library.component_summary_format": "{0} - {1} x {2}",
|
||||||
"component.delete": "删除",
|
"component.delete": "删除",
|
||||||
"component.edit": "编辑",
|
"component.edit": "编辑",
|
||||||
"component.move": "移动",
|
"component.move": "移动",
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ public static class DesktopComponentRegistryFactory
|
|||||||
registration.AllowDesktopPlacement,
|
registration.AllowDesktopPlacement,
|
||||||
registration.ResizeMode == PluginDesktopComponentResizeMode.Free
|
registration.ResizeMode == PluginDesktopComponentResizeMode.Free
|
||||||
? DesktopComponentResizeMode.Free
|
? DesktopComponentResizeMode.Free
|
||||||
: DesktopComponentResizeMode.Proportional));
|
: DesktopComponentResizeMode.Proportional,
|
||||||
|
Description: registration.Description,
|
||||||
|
DescriptionLocalizationKey: registration.DescriptionLocalizationKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
return definitions;
|
return definitions;
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ public sealed class DesktopComponentRuntimeDescriptor
|
|||||||
|
|
||||||
public string? DisplayNameLocalizationKey { get; }
|
public string? DisplayNameLocalizationKey { get; }
|
||||||
|
|
||||||
|
public string? Description => Definition.Description;
|
||||||
|
|
||||||
|
public string? DescriptionLocalizationKey => Definition.DescriptionLocalizationKey;
|
||||||
|
|
||||||
public Control CreateControl(
|
public Control CreateControl(
|
||||||
double cellSize,
|
double cellSize,
|
||||||
TimeZoneService timeZoneService,
|
TimeZoneService timeZoneService,
|
||||||
|
|||||||
@@ -108,7 +108,9 @@
|
|||||||
Click="OnFindMoreComponentsClick">
|
Click="OnFindMoreComponentsClick">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
<fi:FluentIcon Icon="Globe" IconVariant="Regular" FontSize="14"/>
|
<fi:FluentIcon Icon="Globe" IconVariant="Regular" FontSize="14"/>
|
||||||
<TextBlock Text="查找更多小组件" FontSize="12"/>
|
<TextBlock x:Name="FindMoreComponentsTextBlock"
|
||||||
|
Text="查找更多小组件"
|
||||||
|
FontSize="12"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -121,13 +123,11 @@
|
|||||||
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||||
Opacity="0.35"/>
|
Opacity="0.35"/>
|
||||||
|
|
||||||
<ScrollViewer Grid.Column="1"
|
<Grid Grid.Column="1"
|
||||||
VerticalScrollBarVisibility="Auto"
|
Margin="28,8,8,10">
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
<Grid IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNotNull}}"
|
||||||
<StackPanel Margin="28,8,8,10">
|
RowDefinitions="Auto,Auto,*,Auto"
|
||||||
<Panel IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNotNull}}">
|
MinHeight="0">
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto"
|
|
||||||
MinHeight="330">
|
|
||||||
<TextBlock FontSize="24"
|
<TextBlock FontSize="24"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
@@ -152,17 +152,19 @@
|
|||||||
Background="{DynamicResource AdaptiveSurfaceOverlayBrush}"
|
Background="{DynamicResource AdaptiveSurfaceOverlayBrush}"
|
||||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Width="390"
|
MinWidth="360"
|
||||||
Height="230"
|
MinHeight="220"
|
||||||
Focusable="True"
|
Focusable="True"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Stretch"
|
||||||
|
SizeChanged="OnPreviewInteractionHostSizeChanged"
|
||||||
PointerPressed="OnPreviewPointerPressed"
|
PointerPressed="OnPreviewPointerPressed"
|
||||||
PointerReleased="OnPreviewPointerReleased"
|
PointerReleased="OnPreviewPointerReleased"
|
||||||
PointerCaptureLost="OnPreviewPointerCaptureLost"
|
PointerCaptureLost="OnPreviewPointerCaptureLost"
|
||||||
PointerWheelChanged="OnPreviewPointerWheelChanged"
|
PointerWheelChanged="OnPreviewPointerWheelChanged"
|
||||||
KeyDown="OnPreviewKeyDown">
|
KeyDown="OnPreviewKeyDown">
|
||||||
<Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
<Border x:Name="SelectedComponentPreviewFrame"
|
||||||
|
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
||||||
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
|
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
|
||||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
@@ -185,11 +187,12 @@
|
|||||||
Click="OnAddComponentClick">
|
Click="OnAddComponentClick">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<fi:FluentIcon Icon="Add" IconVariant="Regular" FontSize="16"/>
|
<fi:FluentIcon Icon="Add" IconVariant="Regular" FontSize="16"/>
|
||||||
<TextBlock Text="添加小组件" FontWeight="SemiBold"/>
|
<TextBlock x:Name="AddComponentButtonTextBlock"
|
||||||
|
Text="添加小组件"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Grid IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNull}}"
|
<Grid IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNull}}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
@@ -203,13 +206,13 @@
|
|||||||
FontSize="64"
|
FontSize="64"
|
||||||
Opacity="0.3"
|
Opacity="0.3"
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
|
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
|
||||||
<TextBlock HorizontalAlignment="Center"
|
<TextBlock x:Name="EmptySelectionTextBlock"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
FontSize="16"
|
FontSize="16"
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||||
Text="选择一个分类以查看可添加组件。"/>
|
Text="选择一个分类以查看可添加组件。"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</Grid>
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
private ComponentRegistry? _componentRegistry;
|
private ComponentRegistry? _componentRegistry;
|
||||||
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
|
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
|
||||||
private Control? _selectedPreviewControl;
|
private Control? _selectedPreviewControl;
|
||||||
|
private DesktopComponentDefinition? _selectedPreviewDefinition;
|
||||||
|
private FusedDesktopLibraryPreviewMetrics? _selectedPreviewMetrics;
|
||||||
private bool _isPreviewSwipeActive;
|
private bool _isPreviewSwipeActive;
|
||||||
private Point _previewSwipeStartPoint;
|
private Point _previewSwipeStartPoint;
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
|
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
|
||||||
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
|
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
|
||||||
|
|
||||||
|
ApplyLocalization();
|
||||||
LoadRegistry();
|
LoadRegistry();
|
||||||
LoadCategories();
|
LoadCategories();
|
||||||
|
|
||||||
@@ -57,6 +60,24 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyLocalization()
|
||||||
|
{
|
||||||
|
var languageCode = _settingsFacade.Region.Get().LanguageCode;
|
||||||
|
_viewModel.Title = L(languageCode, "fused_desktop.library.title", "Add widgets");
|
||||||
|
FindMoreComponentsTextBlock.Text = L(
|
||||||
|
languageCode,
|
||||||
|
"fused_desktop.library.find_more",
|
||||||
|
"Find more widgets");
|
||||||
|
AddComponentButtonTextBlock.Text = L(
|
||||||
|
languageCode,
|
||||||
|
"fused_desktop.library.add_button",
|
||||||
|
"Add widget");
|
||||||
|
EmptySelectionTextBlock.Text = L(
|
||||||
|
languageCode,
|
||||||
|
"fused_desktop.library.empty_selection",
|
||||||
|
"Choose a category to view widgets.");
|
||||||
|
}
|
||||||
|
|
||||||
private void OnCategoryListBoxContainerPrepared(object? sender, ContainerPreparedEventArgs e)
|
private void OnCategoryListBoxContainerPrepared(object? sender, ContainerPreparedEventArgs e)
|
||||||
{
|
{
|
||||||
_ = sender;
|
_ = sender;
|
||||||
@@ -136,10 +157,73 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition, string languageCode)
|
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition, string languageCode)
|
||||||
|
{
|
||||||
|
return new ComponentLibraryItemViewModel(
|
||||||
|
definition.Id,
|
||||||
|
ResolveComponentDisplayName(definition, languageCode),
|
||||||
|
ResolveComponentDescription(definition, languageCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveComponentDisplayName(DesktopComponentDefinition definition, string languageCode)
|
||||||
|
{
|
||||||
|
if (_componentRuntimeRegistry is not null &&
|
||||||
|
_componentRuntimeRegistry.TryGetDescriptor(definition.Id, out var descriptor) &&
|
||||||
|
!string.IsNullOrWhiteSpace(descriptor.DisplayNameLocalizationKey))
|
||||||
|
{
|
||||||
|
return L(languageCode, descriptor.DisplayNameLocalizationKey, definition.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return definition.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveComponentDescription(DesktopComponentDefinition definition, string languageCode)
|
||||||
|
{
|
||||||
|
if (_componentRuntimeRegistry is not null &&
|
||||||
|
_componentRuntimeRegistry.TryGetDescriptor(definition.Id, out var descriptor))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(descriptor.DescriptionLocalizationKey))
|
||||||
|
{
|
||||||
|
return L(
|
||||||
|
languageCode,
|
||||||
|
descriptor.DescriptionLocalizationKey,
|
||||||
|
descriptor.Description ?? CreateComponentFallbackDescription(definition, languageCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(descriptor.Description))
|
||||||
|
{
|
||||||
|
return descriptor.Description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(definition.DescriptionLocalizationKey))
|
||||||
|
{
|
||||||
|
return L(
|
||||||
|
languageCode,
|
||||||
|
definition.DescriptionLocalizationKey,
|
||||||
|
definition.Description ?? CreateComponentFallbackDescription(definition, languageCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(definition.Description))
|
||||||
|
{
|
||||||
|
return definition.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateComponentFallbackDescription(definition, languageCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateComponentFallbackDescription(DesktopComponentDefinition definition, string languageCode)
|
||||||
{
|
{
|
||||||
var categoryTitle = GetLocalizedCategoryTitle(languageCode, definition.Category);
|
var categoryTitle = GetLocalizedCategoryTitle(languageCode, definition.Category);
|
||||||
var description = $"{categoryTitle} - {Math.Max(1, definition.MinWidthCells)} x {Math.Max(1, definition.MinHeightCells)}";
|
var fallbackFormat = L(
|
||||||
return new ComponentLibraryItemViewModel(definition.Id, definition.DisplayName, description);
|
languageCode,
|
||||||
|
"fused_desktop.library.component_summary_format",
|
||||||
|
"{0} - {1} x {2}");
|
||||||
|
return string.Format(
|
||||||
|
System.Globalization.CultureInfo.CurrentCulture,
|
||||||
|
fallbackFormat,
|
||||||
|
categoryTitle,
|
||||||
|
Math.Max(1, definition.MinWidthCells),
|
||||||
|
Math.Max(1, definition.MinHeightCells));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
@@ -154,6 +238,8 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
if (CategoryListBox.SelectedItem is not ComponentLibraryCategoryViewModel selectedCategory)
|
if (CategoryListBox.SelectedItem is not ComponentLibraryCategoryViewModel selectedCategory)
|
||||||
{
|
{
|
||||||
_viewModel.SelectedComponent = null;
|
_viewModel.SelectedComponent = null;
|
||||||
|
_selectedPreviewDefinition = null;
|
||||||
|
_selectedPreviewMetrics = null;
|
||||||
SetSelectedPreviewControl(null);
|
SetSelectedPreviewControl(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,14 +260,18 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
if (_selectedCategoryDefinitions.Count == 0)
|
if (_selectedCategoryDefinitions.Count == 0)
|
||||||
{
|
{
|
||||||
_viewModel.SelectedComponent = null;
|
_viewModel.SelectedComponent = null;
|
||||||
|
_selectedPreviewDefinition = null;
|
||||||
|
_selectedPreviewMetrics = null;
|
||||||
SetSelectedPreviewControl(null);
|
SetSelectedPreviewControl(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectedComponentIndex = NormalizeComponentIndex(_selectedComponentIndex);
|
_selectedComponentIndex = NormalizeComponentIndex(_selectedComponentIndex);
|
||||||
var selectedDefinition = _selectedCategoryDefinitions[_selectedComponentIndex];
|
var selectedDefinition = _selectedCategoryDefinitions[_selectedComponentIndex];
|
||||||
|
_selectedPreviewDefinition = selectedDefinition;
|
||||||
|
_selectedPreviewMetrics = null;
|
||||||
_viewModel.SelectedComponent = CreateComponentItem(selectedDefinition, _settingsFacade.Region.Get().LanguageCode);
|
_viewModel.SelectedComponent = CreateComponentItem(selectedDefinition, _settingsFacade.Region.Get().LanguageCode);
|
||||||
SetSelectedPreviewControl(CreateStaticPreviewControl(selectedDefinition));
|
RefreshSelectedPreviewControl(force: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int NormalizeComponentIndex(int index)
|
private int NormalizeComponentIndex(int index)
|
||||||
@@ -274,7 +364,45 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Control? CreateStaticPreviewControl(DesktopComponentDefinition definition)
|
private void OnPreviewInteractionHostSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
RefreshSelectedPreviewControl(force: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshSelectedPreviewControl(bool force)
|
||||||
|
{
|
||||||
|
if (_selectedPreviewDefinition is null)
|
||||||
|
{
|
||||||
|
_selectedPreviewMetrics = null;
|
||||||
|
SetSelectedPreviewControl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metrics = FusedDesktopLibraryPreviewLayout.Calculate(
|
||||||
|
_selectedPreviewDefinition,
|
||||||
|
PreviewInteractionHost.Bounds.Size);
|
||||||
|
if (!force &&
|
||||||
|
_selectedPreviewMetrics is { } previousMetrics &&
|
||||||
|
ArePreviewMetricsClose(previousMetrics, metrics))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedPreviewMetrics = metrics;
|
||||||
|
if (!force && _selectedPreviewControl is not null)
|
||||||
|
{
|
||||||
|
ApplyPreviewMetricsToControl(_selectedPreviewControl, metrics);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSelectedPreviewControl(CreateStaticPreviewControl(_selectedPreviewDefinition, metrics));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control? CreateStaticPreviewControl(
|
||||||
|
DesktopComponentDefinition definition,
|
||||||
|
FusedDesktopLibraryPreviewMetrics metrics)
|
||||||
{
|
{
|
||||||
if (_componentRuntimeRegistry is null ||
|
if (_componentRuntimeRegistry is null ||
|
||||||
!_componentRuntimeRegistry.TryGetDescriptor(definition.Id, out var descriptor))
|
!_componentRuntimeRegistry.TryGetDescriptor(definition.Id, out var descriptor))
|
||||||
@@ -285,7 +413,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var control = descriptor.CreateControl(
|
var control = descriptor.CreateControl(
|
||||||
ResolvePreviewCellSize(definition),
|
metrics.CellSize,
|
||||||
_timeZoneService,
|
_timeZoneService,
|
||||||
_weatherDataService,
|
_weatherDataService,
|
||||||
_recommendationInfoService,
|
_recommendationInfoService,
|
||||||
@@ -293,6 +421,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
_settingsFacade,
|
_settingsFacade,
|
||||||
placementId: null,
|
placementId: null,
|
||||||
renderMode: DesktopComponentRenderMode.LibraryPreview);
|
renderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||||
|
ApplyPreviewMetricsToControl(control, metrics);
|
||||||
ComponentPreviewRuntimeQuiescer.Attach(control);
|
ComponentPreviewRuntimeQuiescer.Attach(control);
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
@@ -306,16 +435,30 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double ResolvePreviewCellSize(DesktopComponentDefinition definition)
|
private static void ApplyPreviewMetricsToControl(
|
||||||
|
Control control,
|
||||||
|
FusedDesktopLibraryPreviewMetrics metrics)
|
||||||
{
|
{
|
||||||
const double maxWidth = 360d;
|
control.Width = metrics.Width;
|
||||||
const double maxHeight = 240d;
|
control.Height = metrics.Height;
|
||||||
return Math.Clamp(
|
control.HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center;
|
||||||
Math.Min(
|
control.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
|
||||||
maxWidth / Math.Max(1, definition.MinWidthCells),
|
if (control is IDesktopComponentWidget sizedComponent)
|
||||||
maxHeight / Math.Max(1, definition.MinHeightCells)),
|
{
|
||||||
32d,
|
sizedComponent.ApplyCellSize(metrics.CellSize);
|
||||||
96d);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ArePreviewMetricsClose(
|
||||||
|
FusedDesktopLibraryPreviewMetrics first,
|
||||||
|
FusedDesktopLibraryPreviewMetrics second)
|
||||||
|
{
|
||||||
|
const double tolerance = 0.5d;
|
||||||
|
return first.WidthCells == second.WidthCells &&
|
||||||
|
first.HeightCells == second.HeightCells &&
|
||||||
|
Math.Abs(first.CellSize - second.CellSize) <= tolerance &&
|
||||||
|
Math.Abs(first.Width - second.Width) <= tolerance &&
|
||||||
|
Math.Abs(first.Height - second.Height) <= tolerance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetSelectedPreviewControl(Control? control)
|
private void SetSelectedPreviewControl(Control? control)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
xmlns:controls="using:LanMountainDesktop.Views"
|
xmlns:controls="using:LanMountainDesktop.Views"
|
||||||
xmlns:fi="using:FluentIcons.Avalonia"
|
xmlns:fi="using:FluentIcons.Avalonia"
|
||||||
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryWindow"
|
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryWindow"
|
||||||
Width="740"
|
Width="860"
|
||||||
Height="500"
|
Height="560"
|
||||||
MinWidth="600"
|
MinWidth="720"
|
||||||
MinHeight="440"
|
MinHeight="500"
|
||||||
CanResize="True"
|
CanResize="True"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
WindowDecorations="None"
|
WindowDecorations="None"
|
||||||
@@ -19,10 +19,9 @@
|
|||||||
Background="Transparent">
|
Background="Transparent">
|
||||||
<Border x:Name="PanelShell"
|
<Border x:Name="PanelShell"
|
||||||
Classes="surface-translucent-strong"
|
Classes="surface-translucent-strong"
|
||||||
Width="720"
|
HorizontalAlignment="Stretch"
|
||||||
MaxWidth="720"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalAlignment="Center"
|
Margin="10"
|
||||||
VerticalAlignment="Center"
|
|
||||||
Padding="0"
|
Padding="0"
|
||||||
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||||
ClipToBounds="True">
|
ClipToBounds="True">
|
||||||
@@ -32,7 +31,8 @@
|
|||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
PointerPressed="OnWindowTitleBarPointerPressed">
|
PointerPressed="OnWindowTitleBarPointerPressed">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBlock VerticalAlignment="Center"
|
<TextBlock x:Name="WindowTitleTextBlock"
|
||||||
|
VerticalAlignment="Center"
|
||||||
FontSize="22"
|
FontSize="22"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
|
|||||||
@@ -6,16 +6,20 @@ using Avalonia.Input;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using LanMountainDesktop.Appearance;
|
using LanMountainDesktop.Appearance;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
using LanMountainDesktop.Services.Settings;
|
||||||
using LanMountainDesktop.Settings.Core;
|
using LanMountainDesktop.Settings.Core;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
|
|
||||||
public partial class FusedDesktopComponentLibraryWindow : Window
|
public partial class FusedDesktopComponentLibraryWindow : Window
|
||||||
{
|
{
|
||||||
|
private static readonly LocalizationService LocalizationService = new();
|
||||||
|
|
||||||
public FusedDesktopComponentLibraryWindow()
|
public FusedDesktopComponentLibraryWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
ApplyFluentCornerRadius();
|
ApplyFluentCornerRadius();
|
||||||
|
ApplyLocalization();
|
||||||
|
|
||||||
LibraryControl.AddComponentRequested += OnAddComponentRequested;
|
LibraryControl.AddComponentRequested += OnAddComponentRequested;
|
||||||
KeyDown += OnWindowKeyDown;
|
KeyDown += OnWindowKeyDown;
|
||||||
@@ -24,6 +28,17 @@ public partial class FusedDesktopComponentLibraryWindow : Window
|
|||||||
mainWindow?.RegisterFusedLibraryWindow(this);
|
mainWindow?.RegisterFusedLibraryWindow(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyLocalization()
|
||||||
|
{
|
||||||
|
var languageCode = HostSettingsFacadeProvider.GetOrCreate().Region.Get().LanguageCode;
|
||||||
|
var title = LocalizationService.GetString(
|
||||||
|
languageCode,
|
||||||
|
"fused_desktop.library.title",
|
||||||
|
"Add widgets");
|
||||||
|
Title = title;
|
||||||
|
WindowTitleTextBlock.Text = title;
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyFluentCornerRadius()
|
private void ApplyFluentCornerRadius()
|
||||||
{
|
{
|
||||||
if (RootGrid is null)
|
if (RootGrid is null)
|
||||||
|
|||||||
79
LanMountainDesktop/Views/FusedDesktopLibraryPreviewLayout.cs
Normal file
79
LanMountainDesktop/Views/FusedDesktopLibraryPreviewLayout.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views;
|
||||||
|
|
||||||
|
internal readonly record struct FusedDesktopLibraryPreviewMetrics(
|
||||||
|
int WidthCells,
|
||||||
|
int HeightCells,
|
||||||
|
double CellSize,
|
||||||
|
double Width,
|
||||||
|
double Height);
|
||||||
|
|
||||||
|
internal static class FusedDesktopLibraryPreviewLayout
|
||||||
|
{
|
||||||
|
internal const double DefaultStageWidth = 460d;
|
||||||
|
internal const double DefaultStageHeight = 300d;
|
||||||
|
|
||||||
|
private const double StageHorizontalInset = 48d;
|
||||||
|
private const double StageVerticalInset = 42d;
|
||||||
|
private const double MinCellSize = 32d;
|
||||||
|
private const double MaxCellSize = 128d;
|
||||||
|
|
||||||
|
public static FusedDesktopLibraryPreviewMetrics Calculate(
|
||||||
|
DesktopComponentDefinition definition,
|
||||||
|
Size stageSize)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(definition);
|
||||||
|
|
||||||
|
return Calculate(
|
||||||
|
definition.MinWidthCells,
|
||||||
|
definition.MinHeightCells,
|
||||||
|
stageSize.Width,
|
||||||
|
stageSize.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FusedDesktopLibraryPreviewMetrics Calculate(
|
||||||
|
int widthCells,
|
||||||
|
int heightCells,
|
||||||
|
double stageWidth,
|
||||||
|
double stageHeight)
|
||||||
|
{
|
||||||
|
var normalizedWidthCells = Math.Max(1, widthCells);
|
||||||
|
var normalizedHeightCells = Math.Max(1, heightCells);
|
||||||
|
var normalizedStageWidth = NormalizeStageLength(stageWidth, DefaultStageWidth);
|
||||||
|
var normalizedStageHeight = NormalizeStageLength(stageHeight, DefaultStageHeight);
|
||||||
|
|
||||||
|
var availableWidth = Math.Max(1d, normalizedStageWidth - StageHorizontalInset);
|
||||||
|
var availableHeight = Math.Max(1d, normalizedStageHeight - StageVerticalInset);
|
||||||
|
var fitCellSize = Math.Min(
|
||||||
|
availableWidth / normalizedWidthCells,
|
||||||
|
availableHeight / normalizedHeightCells);
|
||||||
|
|
||||||
|
if (!double.IsFinite(fitCellSize) || fitCellSize <= 0d)
|
||||||
|
{
|
||||||
|
fitCellSize = Math.Min(
|
||||||
|
(DefaultStageWidth - StageHorizontalInset) / normalizedWidthCells,
|
||||||
|
(DefaultStageHeight - StageVerticalInset) / normalizedHeightCells);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cellSize = fitCellSize >= MinCellSize
|
||||||
|
? Math.Min(fitCellSize, MaxCellSize)
|
||||||
|
: Math.Max(1d, fitCellSize);
|
||||||
|
|
||||||
|
return new FusedDesktopLibraryPreviewMetrics(
|
||||||
|
normalizedWidthCells,
|
||||||
|
normalizedHeightCells,
|
||||||
|
cellSize,
|
||||||
|
normalizedWidthCells * cellSize,
|
||||||
|
normalizedHeightCells * cellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double NormalizeStageLength(double value, double fallback)
|
||||||
|
{
|
||||||
|
return double.IsFinite(value) && value > 1d
|
||||||
|
? value
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user