diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 35475ca5..89a6606c 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -179,10 +179,8 @@ SHA Signer: Open in Browser - Description - Paste (Replace all) + Enter commit message. Please use an empty-line to seperate subject and description! SUBJECT - Enter commit subject Repository Configure COMMIT TEMPLATE Built-in parameters: diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 8056fa60..24c7c32b 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -183,10 +183,8 @@ 提交指纹 签名者 : 浏览器中查看 - 详细描述 - 粘贴(替换全部) + 请输入提交的信息。注意:主题与具体描述中间需要空白行分隔! 主题 - 填写提交信息主题 仓库配置 提交信息模板 内置变量: diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 319a5454..436b594a 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -183,10 +183,8 @@ 提交編號 簽署人: 在瀏覽器中檢視 - 詳細描述 - 貼上 (全部取代) + 請輸入提交訊息。注意:主題與詳細訊息之間必須留一行空行。 標題 - 填寫提交訊息標題 存放庫設定 提交訊息範本 內建參數: diff --git a/src/Views/CommitMessageEditor.axaml b/src/Views/CommitMessageEditor.axaml index 4687aebb..c5345c3c 100644 --- a/src/Views/CommitMessageEditor.axaml +++ b/src/Views/CommitMessageEditor.axaml @@ -36,7 +36,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/CommitMessageToolBox.axaml b/src/Views/CommitMessageToolBox.axaml new file mode 100644 index 00000000..ede5d9b6 --- /dev/null +++ b/src/Views/CommitMessageToolBox.axaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitMessageTextBox.axaml.cs b/src/Views/CommitMessageToolBox.axaml.cs similarity index 55% rename from src/Views/CommitMessageTextBox.axaml.cs rename to src/Views/CommitMessageToolBox.axaml.cs index 95519014..3ec92ee9 100644 --- a/src/Views/CommitMessageTextBox.axaml.cs +++ b/src/Views/CommitMessageToolBox.axaml.cs @@ -1,75 +1,246 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; using Avalonia; using Avalonia.Controls; -using Avalonia.Input; +using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using AvaloniaEdit.Rendering; + namespace SourceGit.Views { - public class EnhancedTextBox : TextBox + public class CommitMessageTextEditor : TextEditor { - public static readonly RoutedEvent PreviewKeyDownEvent = - RoutedEvent.Register(nameof(KeyEventArgs), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + public static readonly StyledProperty CommitMessageProperty = + AvaloniaProperty.Register(nameof(CommitMessage), string.Empty); - public event EventHandler PreviewKeyDown + public string CommitMessage { - add { AddHandler(PreviewKeyDownEvent, value); } - remove { RemoveHandler(PreviewKeyDownEvent, value); } + get => GetValue(CommitMessageProperty); + set => SetValue(CommitMessageProperty, value); } - protected override Type StyleKeyOverride => typeof(TextBox); + public static readonly StyledProperty SubjectLengthProperty = + AvaloniaProperty.Register(nameof(SubjectLength), 0); - public void Paste(string text) + public int SubjectLength { - OnTextInput(new TextInputEventArgs() { Text = text }); + get => GetValue(SubjectLengthProperty); + set => SetValue(SubjectLengthProperty, value); } - protected override void OnKeyDown(KeyEventArgs e) + public static readonly StyledProperty SubjectLineBrushProperty = + AvaloniaProperty.Register(nameof(SubjectLineBrush), Brushes.Gray); + + public IBrush SubjectLineBrush { - var dump = new KeyEventArgs() + get => GetValue(SubjectLineBrushProperty); + set => SetValue(SubjectLineBrushProperty, value); + } + + protected override Type StyleKeyOverride => typeof(TextEditor); + + public CommitMessageTextEditor() : base(new TextArea(), new TextDocument()) + { + IsReadOnly = false; + WordWrap = true; + ShowLineNumbers = false; + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + + TextArea.TextView.Margin = new Thickness(4, 2); + TextArea.TextView.Options.EnableHyperlinks = false; + TextArea.TextView.Options.EnableEmailHyperlinks = false; + TextArea.TextView.Options.AllowScrollBelowDocument = false; + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var w = Bounds.Width; + var pen = new Pen(SubjectLineBrush) { DashStyle = DashStyle.Dash }; + + if (SubjectLength == 0 || CommitMessage.Trim().Length == 0) { - RoutedEvent = PreviewKeyDownEvent, - Route = RoutingStrategies.Direct, - Source = e.Source, - Key = e.Key, - KeyModifiers = e.KeyModifiers, - PhysicalKey = e.PhysicalKey, - KeySymbol = e.KeySymbol, + var placeholder = new FormattedText( + App.Text("CommitMessageTextBox.Placeholder"), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily), + FontSize, + Brushes.Gray); + + context.DrawText(placeholder, new Point(4, 2)); + + var y = 6 + placeholder.Height; + context.DrawLine(pen, new Point(0, y), new Point(w, y)); + return; + } + + if (TextArea.TextView is not { VisualLinesValid: true } view) + return; + + var lines = new List(); + foreach (var line in view.VisualLines) + { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + + lines.Add(line); + } + + if (lines.Count == 0) + return; + + lines.Sort((l, r) => l.StartOffset - r.StartOffset); + + var lastSubjectLine = lines[0]; + for (var i = 1; i < lines.Count; i++) + { + if (lines[i].StartOffset > SubjectLength) + break; + + lastSubjectLine = lines[i]; + } + + var endY = lastSubjectLine.GetTextLineVisualYPosition(lastSubjectLine.TextLines[^1], VisualYPosition.LineBottom) - view.VerticalOffset + 4; + context.DrawLine(pen, new Point(0, endY), new Point(w, endY)); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + TextArea.TextView.LayoutUpdated += OnTextViewLayoutUpdated; + TextArea.TextView.ContextRequested += OnTextViewContextRequested; + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + TextArea.TextView.ContextRequested -= OnTextViewContextRequested; + TextArea.TextView.LayoutUpdated -= OnTextViewLayoutUpdated; + + base.OnUnloaded(e); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == CommitMessageProperty) + { + if (!_isEditing) + Text = CommitMessage; + + var chars = CommitMessage.ToCharArray(); + var lastLinebreakIndex = 0; + var lastLinebreakCount = 0; + var foundSubjectEnd = false; + for (var i = 0; i < chars.Length; i++) + { + var ch = chars[i]; + if (ch == '\r') + continue; + + if (ch == '\n') + { + if (lastLinebreakCount > 0) + { + SetCurrentValue(SubjectLengthProperty, lastLinebreakIndex); + foundSubjectEnd = true; + break; + } + else + { + lastLinebreakIndex = i; + lastLinebreakCount = 1; + } + } + else + { + lastLinebreakCount = 0; + } + } + + if (!foundSubjectEnd) + SetCurrentValue(SubjectLengthProperty, CommitMessage?.Length ?? 0); + + InvalidateVisual(); + } + } + + protected override void OnTextChanged(EventArgs e) + { + base.OnTextChanged(e); + + _isEditing = true; + SetCurrentValue(CommitMessageProperty, Text); + _isEditing = false; + } + + private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) + { + var selection = TextArea.Selection; + var hasSelected = selection is { IsEmpty: false }; + + var copy = new MenuItem(); + copy.Header = App.Text("Copy"); + copy.Icon = App.CreateMenuIcon("Icons.Copy"); + copy.IsEnabled = hasSelected; + copy.Click += (o, ev) => + { + Copy(); + ev.Handled = true; }; - RaiseEvent(dump); + var cut = new MenuItem(); + cut.Header = App.Text("Cut"); + cut.Icon = App.CreateMenuIcon("Icons.Cut"); + cut.IsEnabled = hasSelected; + cut.Click += (o, ev) => + { + Cut(); + ev.Handled = true; + }; - if (dump.Handled) - e.Handled = true; - else - base.OnKeyDown(e); + var paste = new MenuItem(); + paste.Header = App.Text("Paste"); + paste.Icon = App.CreateMenuIcon("Icons.Paste"); + paste.Click += (o, ev) => + { + Paste(); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(copy); + menu.Items.Add(cut); + menu.Items.Add(paste); + menu.Open(TextArea.TextView); + e.Handled = true; } + + private void OnTextViewLayoutUpdated(object sender, EventArgs e) + { + InvalidateVisual(); + } + + private bool _isEditing = false; } - public partial class CommitMessageTextBox : UserControl + public partial class CommitMessageToolBox : UserControl { - public enum TextChangeWay - { - None, - FromSource, - FromEditor, - } - public static readonly StyledProperty ShowAdvancedOptionsProperty = - AvaloniaProperty.Register(nameof(ShowAdvancedOptions)); - - public static readonly StyledProperty TextProperty = - AvaloniaProperty.Register(nameof(Text), string.Empty); - - public static readonly StyledProperty SubjectProperty = - AvaloniaProperty.Register(nameof(Subject), string.Empty); - - public static readonly StyledProperty DescriptionProperty = - AvaloniaProperty.Register(nameof(Description), string.Empty); + AvaloniaProperty.Register(nameof(ShowAdvancedOptions)); public bool ShowAdvancedOptions { @@ -77,102 +248,20 @@ namespace SourceGit.Views set => SetValue(ShowAdvancedOptionsProperty, value); } - public string Text + public static readonly StyledProperty CommitMessageProperty = + AvaloniaProperty.Register(nameof(CommitMessage), string.Empty); + + public string CommitMessage { - get => GetValue(TextProperty); - set => SetValue(TextProperty, value); + get => GetValue(CommitMessageProperty); + set => SetValue(CommitMessageProperty, value); } - public string Subject - { - get => GetValue(SubjectProperty); - set => SetValue(SubjectProperty, value); - } - - public string Description - { - get => GetValue(DescriptionProperty); - set => SetValue(DescriptionProperty, value); - } - - public CommitMessageTextBox() + public CommitMessageToolBox() { InitializeComponent(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == TextProperty && _changingWay == TextChangeWay.None) - { - _changingWay = TextChangeWay.FromSource; - var normalized = Text.ReplaceLineEndings("\n"); - var parts = normalized.Split("\n\n", 2); - if (parts.Length != 2) - parts = [normalized, string.Empty]; - SetCurrentValue(SubjectProperty, parts[0].ReplaceLineEndings(" ")); - SetCurrentValue(DescriptionProperty, parts[1]); - _changingWay = TextChangeWay.None; - } - else if ((change.Property == SubjectProperty || change.Property == DescriptionProperty) && _changingWay == TextChangeWay.None) - { - _changingWay = TextChangeWay.FromEditor; - SetCurrentValue(TextProperty, $"{Subject}\n\n{Description}"); - _changingWay = TextChangeWay.None; - } - } - - private async void OnSubjectTextBoxPreviewKeyDown(object _, KeyEventArgs e) - { - if (e.Key == Key.Enter || (e.Key == Key.Right && SubjectEditor.CaretIndex == Subject.Length)) - { - DescriptionEditor.Focus(); - DescriptionEditor.CaretIndex = 0; - e.Handled = true; - } - else if (e.Key == Key.V && e.KeyModifiers == (OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) - { - e.Handled = true; - - var text = await App.GetClipboardTextAsync(); - if (!string.IsNullOrWhiteSpace(text)) - { - text = text.Trim(); - - if (SubjectEditor.CaretIndex == Subject.Length) - { - var parts = text.Split('\n', 2); - if (parts.Length != 2) - { - SubjectEditor.Paste(text); - } - else - { - SubjectEditor.Paste(parts[0]); - DescriptionEditor.Focus(); - DescriptionEditor.CaretIndex = 0; - DescriptionEditor.Paste(parts[1].Trim()); - } - } - else - { - SubjectEditor.Paste(text.ReplaceLineEndings(" ")); - } - } - } - } - - private void OnDescriptionTextBoxPreviewKeyDown(object _, KeyEventArgs e) - { - if ((e.Key == Key.Back || e.Key == Key.Left) && DescriptionEditor.CaretIndex == 0) - { - SubjectEditor.Focus(); - SubjectEditor.CaretIndex = Subject.Length; - e.Handled = true; - } - } - private async void OnOpenCommitMessagePicker(object sender, RoutedEventArgs e) { if (sender is Button button && DataContext is ViewModels.WorkingCopy vm && ShowAdvancedOptions) @@ -351,39 +440,11 @@ namespace SourceGit.Views _ => string.Empty }; - var vm = new ViewModels.ConventionalCommitMessageBuilder(conventionalTypesOverride, text => Text = text); + var vm = new ViewModels.ConventionalCommitMessageBuilder(conventionalTypesOverride, text => CommitMessage = text); var builder = new ConventionalCommitMessageBuilder() { DataContext = vm }; await builder.ShowDialog(owner); e.Handled = true; } - - private async void CopyAllText(object sender, RoutedEventArgs e) - { - await App.CopyTextAsync(Text); - e.Handled = true; - } - - private async void PasteAndReplaceAllText(object sender, RoutedEventArgs e) - { - try - { - var text = await App.GetClipboardTextAsync(); - if (!string.IsNullOrEmpty(text)) - { - var parts = text.ReplaceLineEndings("\n").Split("\n", 2); - var subject = parts[0]; - Text = parts.Length > 1 ? $"{subject}\n\n{parts[1].Trim()}" : subject; - } - } - catch - { - // Ignore exceptions. - } - - e.Handled = true; - } - - private TextChangeWay _changingWay = TextChangeWay.None; } } diff --git a/src/Views/LauncherPage.axaml b/src/Views/LauncherPage.axaml index 9dda0ae9..fc5484bb 100644 --- a/src/Views/LauncherPage.axaml +++ b/src/Views/LauncherPage.axaml @@ -65,7 +65,7 @@ CornerRadius="0,0,8,8" ClipToBounds="True" IsVisible="{Binding Popup, Converter={x:Static ObjectConverters.IsNotNull}}"> - + diff --git a/src/Views/LauncherPage.axaml.cs b/src/Views/LauncherPage.axaml.cs index 6ab150ef..13c523be 100644 --- a/src/Views/LauncherPage.axaml.cs +++ b/src/Views/LauncherPage.axaml.cs @@ -15,23 +15,27 @@ namespace SourceGit.Views private async void OnPopupSureByHotKey(object sender, RoutedEventArgs e) { - var children = this.GetLogicalDescendants(); + var children = PopupPanel.GetLogicalDescendants(); foreach (var child in children) { - if (child is TextBox { IsFocused: true, Tag: StealHotKey steal } textBox && + if (child is Control { IsKeyboardFocusWithin: true, Tag: StealHotKey steal } control && steal is { Key: Key.Enter, KeyModifiers: KeyModifiers.None }) { var fake = new KeyEventArgs() { RoutedEvent = KeyDownEvent, Route = RoutingStrategies.Direct, - Source = textBox, + Source = control, Key = Key.Enter, KeyModifiers = KeyModifiers.None, PhysicalKey = PhysicalKey.Enter, }; - textBox.RaiseEvent(fake); + if (control is AvaloniaEdit.TextEditor editor) + editor.TextArea.TextView.RaiseEvent(fake); + else + control.RaiseEvent(fake); + e.Handled = false; return; } diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index 49f4da6c..ef472c32 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -275,7 +275,7 @@ - + diff --git a/src/Views/Reword.axaml b/src/Views/Reword.axaml index 909a3e17..2db145d9 100644 --- a/src/Views/Reword.axaml +++ b/src/Views/Reword.axaml @@ -18,6 +18,6 @@ - + diff --git a/src/Views/Squash.axaml b/src/Views/Squash.axaml index 939591b9..247e5524 100644 --- a/src/Views/Squash.axaml +++ b/src/Views/Squash.axaml @@ -27,6 +27,6 @@ - + diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index ce940299..15bbd442 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -239,7 +239,7 @@ Background="Transparent"/> - +