diff --git a/Services/FloatingWindowService.cs b/Services/FloatingWindowService.cs index 8d2d0a0..ac084de 100644 --- a/Services/FloatingWindowService.cs +++ b/Services/FloatingWindowService.cs @@ -5,6 +5,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; +using ClassIsland.Core.Controls; using System; using System.Collections.Generic; using System.Linq; @@ -185,20 +186,18 @@ private void RefreshWindowButtons() foreach (var entry in GetOrderedEntries()) { - var iconBlock = new TextBlock + var iconBlock = new FluentIcon { - Text = ConvertIcon(entry.Icon), - FontSize = 18 * scale, - FontFamily = new FontFamily("Segoe Fluent Icons, Segoe MDL2 Assets"), + Glyph = ConvertIcon(entry.Icon), + FontSize = 22 * scale, HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - TextAlignment = TextAlignment.Center + VerticalAlignment = VerticalAlignment.Center }; var nameBlock = new TextBlock { Text = string.IsNullOrWhiteSpace(entry.Name) ? "触发" : entry.Name, - FontSize = 10 * scale, + FontSize = 12 * scale, HorizontalAlignment = HorizontalAlignment.Center, TextAlignment = TextAlignment.Center, TextWrapping = TextWrapping.Wrap, diff --git a/Settings/FloatingWindowTriggerSettings.cs b/Settings/FloatingWindowTriggerSettings.cs index 9091952..8fb96f0 100644 --- a/Settings/FloatingWindowTriggerSettings.cs +++ b/Settings/FloatingWindowTriggerSettings.cs @@ -1,59 +1,231 @@ +using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.Media; using ClassIsland.Core.Abstractions.Controls; +using ClassIsland.Core.Controls; +using FluentAvalonia.UI.Controls; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls.Primitives; using SystemTools.Triggers; namespace SystemTools.Settings; public class FloatingWindowTriggerSettings : TriggerSettingsControlBase { + private const int IconCodeStart = 0xE000; + private const int IconCodeEnd = 0xF4D3; + private readonly TextBox _iconTextBox; private readonly TextBox _nameTextBox; + private readonly List _iconTokens = new(); + + private ContentDialog? _iconPickerDialog; public FloatingWindowTriggerSettings() { - var panel = new StackPanel { Spacing = 10, Margin = new(10) }; + var panel = new StackPanel { Spacing = 10, Margin = new Thickness(10) }; panel.Children.Add(new TextBlock { Text = "悬浮窗按钮图标(示例:/uE7C3)", - TextWrapping = Avalonia.Media.TextWrapping.Wrap + TextWrapping = TextWrapping.Wrap }); - _iconTextBox = new TextBox + _iconTextBox = new TextBox { Watermark = "/uE7C3", HorizontalAlignment = HorizontalAlignment.Stretch }; + _iconTextBox.TextChanged += (_, _) => { Settings.Icon = _iconTextBox.Text ?? string.Empty; }; + + var iconRow = new Grid { - Watermark = "/uE7C3" + ColumnDefinitions = new ColumnDefinitions("*,Auto"), + ColumnSpacing = 8 }; - _iconTextBox.TextChanged += (_, _) => { Settings.Icon = _iconTextBox.Text ?? string.Empty; }; + iconRow.Children.Add(_iconTextBox); - panel.Children.Add(_iconTextBox); + var pickIconButton = new Button + { + Content = "选择图标", + VerticalAlignment = VerticalAlignment.Center + }; + pickIconButton.Click += async (_, _) => await OpenIconPickerDialogAsync(); + Grid.SetColumn(pickIconButton, 1); + iconRow.Children.Add(pickIconButton); + panel.Children.Add(iconRow); panel.Children.Add(new TextBlock { Text = "悬浮窗按钮名称(显示在图标下方)", - TextWrapping = Avalonia.Media.TextWrapping.Wrap + TextWrapping = TextWrapping.Wrap }); - _nameTextBox = new TextBox - { - Watermark = "例如:快捷抽取" - }; + _nameTextBox = new TextBox { Watermark = "例如:快捷抽取" }; _nameTextBox.TextChanged += (_, _) => { Settings.ButtonName = _nameTextBox.Text ?? string.Empty; }; panel.Children.Add(_nameTextBox); panel.Children.Add(new TextBlock { Text = "每个“从悬浮窗触发”触发器会在浮窗里生成一个按钮。", - TextWrapping = Avalonia.Media.TextWrapping.Wrap, - Foreground = Avalonia.Media.Brushes.Gray + TextWrapping = TextWrapping.Wrap, + Foreground = Brushes.Gray }); Content = panel; } + private async Task OpenIconPickerDialogAsync() + { + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel == null) + { + return; + } + + EnsureIconsLoaded(); + var rows = BuildVirtualizedRows(460); + + _iconPickerDialog = new ContentDialog + { + Title = "选择悬浮窗图标", + PrimaryButtonText = "关闭", + DefaultButton = ContentDialogButton.Primary, + Content = BuildIconPickerContent(rows) + }; + + await _iconPickerDialog.ShowAsync(topLevel); + _iconPickerDialog = null; + } + + private Control BuildIconPickerContent(ObservableCollection rows) + { + var listBox = new ListBox + { + ItemsSource = rows, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + + listBox.ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel()); + listBox.ItemTemplate = new FuncDataTemplate((row, _) => BuildIconRow(row)); + + listBox.Height = 520; + ScrollViewer.SetVerticalScrollBarVisibility(listBox, ScrollBarVisibility.Auto); + ScrollViewer.SetHorizontalScrollBarVisibility(listBox, ScrollBarVisibility.Disabled); + + return new Border + { + Padding = new Thickness(8), + Child = listBox + }; + } + + private Control BuildIconRow(IconRow? row) + { + var panel = new WrapPanel + { + Orientation = Orientation.Horizontal, + ItemWidth = 36, + ItemHeight = 36, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Top + }; + + if (row?.Tokens == null || row.Tokens.Count == 0) + { + return panel; + } + + foreach (var token in row.Tokens) + { + panel.Children.Add(BuildIconButton(token)); + } + + return panel; + } + + private Control BuildIconButton(string? token) + { + if (string.IsNullOrWhiteSpace(token)) + { + return new Border { Width = 34, Height = 34 }; + } + + var iconButton = new Button + { + Width = 34, + Height = 34, + Margin = new Thickness(1), + //ToolTip = token, + Padding = new Thickness(0), + Content = new FluentIcon + { + Glyph = ToGlyph(token), + FontSize = 21, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + iconButton.Click += (_, _) => SelectIcon(token); + return iconButton; + } + + private ObservableCollection BuildVirtualizedRows(double dialogWidth) + { + var columns = Math.Max(6, (int)((dialogWidth - 32) / 38)); + var rows = new ObservableCollection(); + + foreach (var chunk in _iconTokens.Chunk(columns)) + { + rows.Add(new IconRow(chunk.ToList())); + } + + return rows; + } + + private static string ToGlyph(string token) + { + if (token.Length > 2 && (token.StartsWith("/u", StringComparison.OrdinalIgnoreCase) || token.StartsWith("\\u", StringComparison.OrdinalIgnoreCase))) + { + if (int.TryParse(token[2..], System.Globalization.NumberStyles.HexNumber, null, out var code)) + { + return char.ConvertFromUtf32(code); + } + } + + return token; + } + + private void EnsureIconsLoaded() + { + if (_iconTokens.Count > 0) + { + return; + } + + for (var code = IconCodeStart; code <= IconCodeEnd; code++) + { + _iconTokens.Add($"/u{code:X4}"); + } + } + + private void SelectIcon(string token) + { + Settings.Icon = token; + _iconTextBox.Text = token; + _iconPickerDialog?.Hide(); + } + protected override void OnInitialized() { base.OnInitialized(); _iconTextBox.Text = Settings.Icon; _nameTextBox.Text = Settings.ButtonName; } + + private sealed record IconRow(IReadOnlyList Tokens); }