Files
Wino-Mail/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml.cs
T

450 lines
16 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
2026-03-07 23:33:25 +01:00
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
2026-03-07 23:33:25 +01:00
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Wino.Mail.Controls;
public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditorCommandControl
{
2026-03-07 23:33:25 +01:00
[GeneratedDependencyProperty]
public partial IEditorCommandTarget? CommandTarget { get; set; }
2026-03-07 23:33:25 +01:00
[GeneratedDependencyProperty]
public partial object? PaneCustomContent { get; set; }
2026-03-07 23:33:25 +01:00
[GeneratedDependencyProperty]
public partial object? InsertCustomContent { get; set; }
2026-03-07 23:33:25 +01:00
[GeneratedDependencyProperty]
public partial object? OptionsCustomContent { get; set; }
2026-03-07 23:33:25 +01:00
[GeneratedDependencyProperty]
public partial EditorColorOption? SelectedTextColorOption { get; set; }
2026-03-07 23:33:25 +01:00
[GeneratedDependencyProperty]
public partial EditorColorOption? SelectedHighlightColorOption { get; set; }
private bool _isApplyingState;
private IEditorCommandTarget? _subscribedTarget;
private static readonly SolidColorBrush TransparentBrush = new(EditorColorOption.ParseColorValue(null));
private IReadOnlyList<EditorColorOption> _textColorOptions = Array.Empty<EditorColorOption>();
private IReadOnlyList<EditorColorOption> _highlightColorOptions = Array.Empty<EditorColorOption>();
public Brush SelectedTextColorBrush => SelectedTextColorOption?.Brush ?? TransparentBrush;
public Brush SelectedHighlightColorBrush => SelectedHighlightColorOption?.Brush ?? TransparentBrush;
public EditorTabbedCommandBarControl()
{
InitializeComponent();
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
public void AttachCommandTarget(IEditorCommandTarget? target)
{
if (_subscribedTarget == target)
{
return;
}
if (_subscribedTarget != null)
{
_subscribedTarget.StateChanged -= CommandTarget_StateChanged;
_subscribedTarget.CapabilitiesChanged -= CommandTarget_CapabilitiesChanged;
}
_subscribedTarget = target;
if (_subscribedTarget != null)
{
_subscribedTarget.StateChanged += CommandTarget_StateChanged;
_subscribedTarget.CapabilitiesChanged += CommandTarget_CapabilitiesChanged;
ApplyCapabilities(_subscribedTarget.Capabilities);
ApplyState(_subscribedTarget.CurrentState);
}
}
public void DetachCommandTarget()
{
if (_subscribedTarget == null)
{
return;
}
_subscribedTarget.StateChanged -= CommandTarget_StateChanged;
_subscribedTarget.CapabilitiesChanged -= CommandTarget_CapabilitiesChanged;
_subscribedTarget = null;
}
2026-03-07 23:33:25 +01:00
partial void OnCommandTargetChanged(IEditorCommandTarget? newValue)
{
2026-03-07 23:33:25 +01:00
AttachCommandTarget(newValue);
}
2026-03-07 23:33:25 +01:00
partial void OnSelectedTextColorOptionChanged(EditorColorOption? newValue) => Bindings.Update();
partial void OnSelectedHighlightColorOptionChanged(EditorColorOption? newValue) => Bindings.Update();
private void OnLoaded(object sender, RoutedEventArgs e)
{
AttachCommandTarget(CommandTarget);
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
DetachCommandTarget();
}
private void CommandTarget_StateChanged(object? sender, EditorState e)
{
ApplyState(e);
}
private void CommandTarget_CapabilitiesChanged(object? sender, EditorCapabilities e)
{
ApplyCapabilities(e);
}
private void ApplyCapabilities(EditorCapabilities capabilities)
{
FontFamilyComboBox.ItemsSource = capabilities.Fonts;
FontSizeComboBox.ItemsSource = capabilities.FontSizes;
AlignmentComboBox.ItemsSource = capabilities.Alignments;
ParagraphStyleComboBox.ItemsSource = capabilities.ParagraphStyles;
2026-03-07 23:33:25 +01:00
_textColorOptions = capabilities.TextColors;
_highlightColorOptions = capabilities.HighlightColors;
TextColorComboBox.ItemsSource = _textColorOptions;
HighlightColorComboBox.ItemsSource = _highlightColorOptions;
LineHeightComboBox.ItemsSource = capabilities.LineHeights;
}
private void ApplyState(EditorState state)
{
_isApplyingState = true;
BoldButton.IsChecked = state.IsBold;
ItalicButton.IsChecked = state.IsItalic;
UnderlineButton.IsChecked = state.IsUnderline;
StrikeButton.IsChecked = state.IsStrikethrough;
BulletListButton.IsChecked = state.IsUnorderedList;
OrderedListButton.IsChecked = state.IsOrderedList;
IndentButton.IsEnabled = state.CanIndent;
OutdentButton.IsEnabled = state.CanOutdent;
RemoveLinkButton.IsEnabled = !string.IsNullOrWhiteSpace(state.LinkUrl);
RemoveLinkButton.Visibility = RemoveLinkButton.IsEnabled ? Visibility.Visible : Visibility.Collapsed;
BuiltInToolbarButton.IsChecked = state.IsBuiltInToolbarVisible;
SpellCheckButton.IsChecked = state.IsSpellCheckEnabled;
AlignmentComboBox.SelectedItem = state.Alignment;
FontFamilyComboBox.SelectedItem = MatchStringItem(FontFamilyComboBox.ItemsSource, state.FontFamily);
FontSizeComboBox.SelectedItem = MatchValueItem<int>(FontSizeComboBox.ItemsSource, state.FontSize);
LineHeightComboBox.SelectedItem = MatchStringItem(LineHeightComboBox.ItemsSource, state.LineHeight);
ParagraphStyleComboBox.SelectedItem = MatchParagraphItem(state.ParagraphStyle);
2026-03-07 23:33:25 +01:00
SelectedTextColorOption = ResolveColorOption(_textColorOptions, state.TextColor);
SelectedHighlightColorOption = ResolveColorOption(_highlightColorOptions, state.HighlightColor);
TextColorComboBox.SelectedItem = SelectedTextColorOption;
HighlightColorComboBox.SelectedItem = SelectedHighlightColorOption;
_isApplyingState = false;
}
private static object? MatchStringItem(object? itemsSource, string? value)
{
if (itemsSource is IEnumerable<string> strings)
{
return strings.FirstOrDefault(item => string.Equals(item, value, StringComparison.OrdinalIgnoreCase));
}
return null;
}
private static object? MatchValueItem<T>(object? itemsSource, T? value) where T : struct
{
if (!value.HasValue || itemsSource is not IEnumerable<T> values)
{
return null;
}
foreach (var item in values)
{
if (EqualityComparer<T>.Default.Equals(item, value.Value))
{
return item;
}
}
return null;
}
private object? MatchParagraphItem(string? tag)
{
if (ParagraphStyleComboBox.ItemsSource is not IEnumerable<EditorParagraphStyleOption> styles)
{
return null;
}
return styles.FirstOrDefault(item => string.Equals(item.Tag, tag, StringComparison.OrdinalIgnoreCase));
}
2026-03-07 23:33:25 +01:00
private static EditorColorOption? MatchColorItem(IEnumerable<EditorColorOption> colors, string? value)
{
2026-03-07 23:33:25 +01:00
var normalizedValue = value ?? string.Empty;
var matchedByValue = colors.FirstOrDefault(item => string.Equals(item.Value, normalizedValue, StringComparison.OrdinalIgnoreCase));
if (matchedByValue != null)
{
2026-03-07 23:33:25 +01:00
return matchedByValue;
}
var targetColor = EditorColorOption.ParseColorValue(value);
return colors.FirstOrDefault(item => item.Brush.Color.Equals(targetColor));
}
private static EditorColorOption? ResolveColorOption(IEnumerable<EditorColorOption> colors, string? value)
{
var colorOptions = colors.ToList();
var matchedColor = MatchColorItem(colors, value);
if (matchedColor != null)
{
return matchedColor;
}
if (string.IsNullOrWhiteSpace(value))
{
return colorOptions.FirstOrDefault(item => string.IsNullOrWhiteSpace(item.Value));
}
var targetColor = EditorColorOption.ParseColorValue(value);
var selectableColors = colorOptions
.Where(item => !string.IsNullOrWhiteSpace(item.Value))
.ToList();
if (selectableColors.Count == 0)
{
return colorOptions.FirstOrDefault();
}
2026-03-07 23:33:25 +01:00
return selectableColors
.OrderBy(item => GetColorDistance(item.Brush.Color, targetColor))
.First();
}
private static int GetColorDistance(Color left, Color right)
{
var redDiff = left.R - right.R;
var greenDiff = left.G - right.G;
var blueDiff = left.B - right.B;
return (redDiff * redDiff) + (greenDiff * greenDiff) + (blueDiff * blueDiff);
}
private async Task ExecuteAsync(EditorCommand command)
{
if (_isApplyingState || CommandTarget == null)
{
return;
}
await CommandTarget.ExecuteCommandAsync(command);
}
private async void BoldButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleBold());
private async void ItalicButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleItalic());
private async void UnderlineButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleUnderline());
private async void StrikeButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleStrikethrough());
private async void BulletListButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleUnorderedList());
private async void OrderedListButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleOrderedList());
private async void IndentButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.Indent());
private async void OutdentButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.Outdent());
private async void ImageButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.InsertImage());
private async void EmojiButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.InsertEmoji());
private async void RemoveLinkButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.RemoveLink());
private async void AlignmentComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || AlignmentComboBox.SelectedItem is not EditorTextAlignment alignment)
{
return;
}
await ExecuteAsync(EditorCommand.SetAlignment(alignment));
}
private async void FontFamilyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || FontFamilyComboBox.SelectedItem is not string fontFamily || string.IsNullOrWhiteSpace(fontFamily))
{
return;
}
await ExecuteAsync(EditorCommand.SetFontFamily(fontFamily));
}
private async void FontSizeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || FontSizeComboBox.SelectedItem is not int fontSize)
{
return;
}
await ExecuteAsync(EditorCommand.SetFontSize(fontSize));
}
private async void ParagraphStyleComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || ParagraphStyleComboBox.SelectedItem is not EditorParagraphStyleOption paragraphStyle)
{
return;
}
await ExecuteAsync(EditorCommand.SetParagraphStyle(paragraphStyle.Tag));
}
private async void TextColorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
2026-03-07 23:33:25 +01:00
SelectedTextColorOption = TextColorComboBox.SelectedItem as EditorColorOption;
if (_isApplyingState || SelectedTextColorOption == null)
{
return;
}
2026-03-07 23:33:25 +01:00
await ExecuteAsync(EditorCommand.SetTextColor(SelectedTextColorOption.Value));
}
private async void HighlightColorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
2026-03-07 23:33:25 +01:00
SelectedHighlightColorOption = HighlightColorComboBox.SelectedItem as EditorColorOption;
if (_isApplyingState || SelectedHighlightColorOption == null)
{
return;
}
2026-03-07 23:33:25 +01:00
await ExecuteAsync(EditorCommand.SetHighlightColor(SelectedHighlightColorOption.Value));
}
private async void LineHeightComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || LineHeightComboBox.SelectedItem is not string lineHeight || string.IsNullOrWhiteSpace(lineHeight))
{
return;
}
await ExecuteAsync(EditorCommand.SetLineHeight(lineHeight));
}
private async void BuiltInToolbarButton_Click(object sender, RoutedEventArgs e)
{
await ExecuteAsync(EditorCommand.ToggleBuiltInToolbar(BuiltInToolbarButton.IsChecked == true));
}
private async void SpellCheckButton_Click(object sender, RoutedEventArgs e)
{
await ExecuteAsync(EditorCommand.ToggleSpellCheck(SpellCheckButton.IsChecked == true));
}
private async void LinkButton_Click(object sender, RoutedEventArgs e)
{
if (CommandTarget == null)
{
return;
}
var currentState = CommandTarget.CurrentState;
var urlTextBox = new TextBox
{
Header = "URL",
Text = currentState.LinkUrl ?? string.Empty,
PlaceholderText = "https://example.com"
};
var textTextBox = new TextBox
{
Header = "Text",
Text = currentState.SelectedText ?? string.Empty,
PlaceholderText = "Link text"
};
var openInNewWindow = new CheckBox
{
Content = "Open in new window",
IsChecked = true
};
var dialog = new ContentDialog
{
XamlRoot = XamlRoot,
Title = "Insert link",
PrimaryButtonText = "Apply",
CloseButtonText = "Cancel",
DefaultButton = ContentDialogButton.Primary,
Content = new StackPanel
{
Spacing = 12,
Children =
{
urlTextBox,
textTextBox,
openInNewWindow
}
}
};
if (await dialog.ShowAsync() == ContentDialogResult.Primary && !string.IsNullOrWhiteSpace(urlTextBox.Text))
{
await ExecuteAsync(EditorCommand.InsertLink(new EditorLinkCommandArgs(urlTextBox.Text.Trim(), textTextBox.Text.Trim(), openInNewWindow.IsChecked == true)));
}
}
private async void TableButton_Click(object sender, RoutedEventArgs e)
{
var rowsBox = new NumberBox
{
Header = "Rows",
Minimum = 1,
Maximum = 10,
SmallChange = 1,
Value = 2
};
var columnsBox = new NumberBox
{
Header = "Columns",
Minimum = 1,
Maximum = 10,
SmallChange = 1,
Value = 2
};
var dialog = new ContentDialog
{
XamlRoot = XamlRoot,
Title = "Insert table",
PrimaryButtonText = "Insert",
CloseButtonText = "Cancel",
DefaultButton = ContentDialogButton.Primary,
Content = new StackPanel
{
Spacing = 12,
Children =
{
rowsBox,
columnsBox
}
}
};
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
await ExecuteAsync(EditorCommand.InsertTable(new EditorTableCommandArgs((int)Math.Max(1, rowsBox.Value), (int)Math.Max(1, columnsBox.Value))));
}
}
2026-03-07 23:33:25 +01:00
}