refactor: rewrite the commit message editor

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2025-11-04 15:48:22 +08:00
parent 9f541bdc1a
commit 785927da19
14 changed files with 323 additions and 322 deletions

View File

@@ -179,10 +179,8 @@
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.CommitDetail.Info.Signer" xml:space="preserve">Signer:</x:String>
<x:String x:Key="Text.CommitDetail.Info.WebLinks" xml:space="preserve">Open in Browser</x:String>
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">Description</x:String>
<x:String x:Key="Text.CommitMessageTextBox.PasteAndReplaceAll" xml:space="preserve">Paste (Replace all)</x:String>
<x:String x:Key="Text.CommitMessageTextBox.Placeholder" xml:space="preserve">Enter commit message. Please use an empty-line to seperate subject and description!</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectCount" xml:space="preserve">SUBJECT</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">Enter commit subject</x:String>
<x:String x:Key="Text.Configure" xml:space="preserve">Repository Configure</x:String>
<x:String x:Key="Text.Configure.CommitMessageTemplate" xml:space="preserve">COMMIT TEMPLATE</x:String>
<x:String x:Key="Text.Configure.CommitMessageTemplate.BuiltinVars" xml:space="preserve">Built-in parameters:

View File

@@ -183,10 +183,8 @@
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">提交指纹</x:String>
<x:String x:Key="Text.CommitDetail.Info.Signer" xml:space="preserve">签名者 </x:String>
<x:String x:Key="Text.CommitDetail.Info.WebLinks" xml:space="preserve">浏览器中查看</x:String>
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">详细描述</x:String>
<x:String x:Key="Text.CommitMessageTextBox.PasteAndReplaceAll" xml:space="preserve">粘贴(替换全部)</x:String>
<x:String x:Key="Text.CommitMessageTextBox.Placeholder" xml:space="preserve">请输入提交的信息。注意:主题与具体描述中间需要空白行分隔!</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectCount" xml:space="preserve">主题</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">填写提交信息主题</x:String>
<x:String x:Key="Text.Configure" xml:space="preserve">仓库配置</x:String>
<x:String x:Key="Text.Configure.CommitMessageTemplate" xml:space="preserve">提交信息模板</x:String>
<x:String x:Key="Text.Configure.CommitMessageTemplate.BuiltinVars" xml:space="preserve">内置变量:

View File

@@ -183,10 +183,8 @@
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">提交編號</x:String>
<x:String x:Key="Text.CommitDetail.Info.Signer" xml:space="preserve">簽署人:</x:String>
<x:String x:Key="Text.CommitDetail.Info.WebLinks" xml:space="preserve">在瀏覽器中檢視</x:String>
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">詳細描述</x:String>
<x:String x:Key="Text.CommitMessageTextBox.PasteAndReplaceAll" xml:space="preserve">貼上 (全部取代)</x:String>
<x:String x:Key="Text.CommitMessageTextBox.Placeholder" xml:space="preserve">請輸入提交訊息。注意:主題與詳細訊息之間必須留一行空行。</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectCount" xml:space="preserve">標題</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">填寫提交訊息標題</x:String>
<x:String x:Key="Text.Configure" xml:space="preserve">存放庫設定</x:String>
<x:String x:Key="Text.Configure.CommitMessageTemplate" xml:space="preserve">提交訊息範本</x:String>
<x:String x:Key="Text.Configure.CommitMessageTemplate.BuiltinVars" xml:space="preserve">內建參數:

View File

@@ -36,7 +36,7 @@
</Grid>
<StackPanel Grid.Row="1" Orientation="Vertical" Margin="8">
<v:CommitMessageTextBox x:Name="Editor" Height="400"/>
<v:CommitMessageToolBox x:Name="Editor" Height="400"/>
<Button Classes="flat primary"
Width="80"
Margin="0,8,0,4"

View File

@@ -43,11 +43,7 @@ namespace SourceGit.Views
_onSave = msg => File.WriteAllText(file, msg);
_shouldExitApp = true;
var content = File.ReadAllText(file).ReplaceLineEndings("\n").Trim();
var parts = content.Split('\n', 2);
Editor.SubjectEditor.Text = parts[0];
if (parts.Length > 1)
Editor.DescriptionEditor.Text = parts[1].Trim();
Editor.CommitMessage = File.ReadAllText(file).ReplaceLineEndings("\n").Trim();
}
public void AsBuiltin(string conventionalTypesOverride, string msg, Action<string> onSave)
@@ -57,10 +53,7 @@ namespace SourceGit.Views
_onSave = onSave;
_shouldExitApp = false;
var parts = msg.Split('\n', 2);
Editor.SubjectEditor.Text = parts[0];
if (parts.Length > 1)
Editor.DescriptionEditor.Text = parts[1].Trim();
Editor.CommitMessage = msg;
}
protected override void OnClosed(EventArgs e)
@@ -73,7 +66,7 @@ namespace SourceGit.Views
private void SaveAndClose(object _1, RoutedEventArgs _2)
{
_onSave?.Invoke(Editor.Text);
_onSave?.Invoke(Editor.CommitMessage);
_exitCode = 0;
Close();
}

View File

@@ -1,133 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:c="using:SourceGit.Converters"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CommitMessageTextBox"
x:Name="ThisControl">
<Border Background="{DynamicResource Brush.Contents}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"
CornerRadius="4">
<Grid RowDefinitions="Auto,1,*,1,24">
<v:EnhancedTextBox Grid.Row="0"
x:Name="SubjectEditor"
Classes="no_border"
Margin="0"
Padding="4"
CornerRadius="4,4,0,0"
BorderThickness="0"
Background="Transparent"
AcceptsReturn="False"
TextWrapping="Wrap"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
Text="{Binding #ThisControl.Subject, Mode=TwoWay}"
Watermark="{DynamicResource Text.CommitMessageTextBox.SubjectPlaceholder}"
PreviewKeyDown="OnSubjectTextBoxPreviewKeyDown"
Tag="{Binding Source={x:Static v:StealHotKey.Enter}}"/>
<Rectangle Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsHitTestVisible="False"
Fill="{DynamicResource Brush.Border2}"/>
<v:EnhancedTextBox Grid.Row="2"
x:Name="DescriptionEditor"
Classes="no_border"
Margin="0"
Padding="4"
BorderThickness="0"
Background="Transparent"
VerticalContentAlignment="Top"
AcceptsReturn="True"
AcceptsTab="True"
TextWrapping="Wrap"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Text="{Binding #ThisControl.Description, Mode=TwoWay}"
Watermark="{DynamicResource Text.CommitMessageTextBox.MessagePlaceholder}"
PreviewKeyDown="OnDescriptionTextBoxPreviewKeyDown"
Tag="{Binding Source={x:Static v:StealHotKey.Enter}}"/>
<Rectangle Grid.Row="3"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsHitTestVisible="False"
Fill="{DynamicResource Brush.Border2}"/>
<Border Grid.Row="4"
Background="{DynamicResource Brush.Window}"
BorderThickness="0"
CornerRadius="0,0,4,4">
<Grid ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,*" Margin="0,4">
<Button Grid.Column="0"
Classes="icon_button"
Width="24"
Margin="0,0,4,0" Padding="0"
Click="OnOpenCommitMessagePicker"
IsVisible="{Binding #ThisControl.ShowAdvancedOptions}"
ToolTip.Tip="{DynamicResource Text.WorkingCopy.CommitMessageHelper}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Menu}"/>
</Button>
<Button Grid.Column="1"
Classes="icon_button"
Width="24"
Margin="0,0,4,0" Padding="0"
Click="OnOpenOpenAIHelper"
IsVisible="{Binding #ThisControl.ShowAdvancedOptions}"
ToolTip.Tip="{DynamicResource Text.AIAssistant.Tip}">
<Path Width="13" Height="13" Data="{StaticResource Icons.AIAssist}"/>
</Button>
<Button Grid.Column="2"
Classes="icon_button"
Width="24"
Margin="0,0,4,0" Padding="0"
Click="OnOpenConventionalCommitHelper"
ToolTip.Tip="{DynamicResource Text.ConventionalCommit}">
<Path Width="13" Height="13" Margin="0,1,0,0" Data="{StaticResource Icons.CommitMessageGenerator}"/>
</Button>
<Button Grid.Column="3"
Classes="icon_button"
Width="24"
Margin="0,0,4,0" Padding="0"
Click="CopyAllText"
ToolTip.Tip="{DynamicResource Text.CopyAllText}">
<Path Width="13" Height="13" Data="{StaticResource Icons.Copy}"/>
</Button>
<Button Grid.Column="4"
Classes="icon_button"
Width="24"
Margin="0,0,4,0" Padding="0"
Click="PasteAndReplaceAllText"
ToolTip.Tip="{DynamicResource Text.CommitMessageTextBox.PasteAndReplaceAll}">
<Path Width="13" Height="13" Data="{StaticResource Icons.Paste}"/>
</Button>
<StackPanel Grid.Column="5"
Margin="0,0,6,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Classes="info_label" FontSize="13" HorizontalAlignment="Left" Text="{DynamicResource Text.CommitMessageTextBox.SubjectCount}"/>
<TextBlock Margin="8,0,0,0" FontSize="11" Text="{Binding #ThisControl.Subject.Length}" IsVisible="{Binding #ThisControl.Subject.Length, Converter={x:Static c:IntConverters.IsSubjectLengthGood}}" VerticalAlignment="Center"/>
<TextBlock Margin="8,0,0,0" FontSize="11" Foreground="DarkGoldenrod" Text="{Binding #ThisControl.Subject.Length}" IsVisible="{Binding #ThisControl.Subject.Length, Converter={x:Static c:IntConverters.IsSubjectLengthBad}}" VerticalAlignment="Center"/>
<TextBlock FontSize="11" Text="/" VerticalAlignment="Center"/>
<TextBlock FontSize="11" Text="{Binding Source={x:Static vm:Preferences.Instance}, Path=SubjectGuideLength}" VerticalAlignment="Center"/>
<Path Width="10" Height="10" Margin="4,0,0,0" Data="{StaticResource Icons.Error}" Fill="DarkGoldenrod" IsVisible="{Binding #ThisControl.Subject.Length, Converter={x:Static c:IntConverters.IsSubjectLengthBad}}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,82 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:c="using:SourceGit.Converters"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.CommitMessageToolBox"
x:Name="ThisControl">
<Border Background="{DynamicResource Brush.Contents}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border2}"
CornerRadius="4">
<Grid RowDefinitions="*,1,24">
<v:CommitMessageTextEditor Grid.Row="0"
x:Name="Editor"
CommitMessage="{Binding #ThisControl.CommitMessage, Mode=TwoWay}"
SubjectLineBrush="{DynamicResource Brush.Border2}"
Foreground="{DynamicResource Brush.FG1}"
FontFamily="{DynamicResource Fonts.Default}"
FontSize="{Binding Source={x:Static vm:Preferences.Instance}, Path=EditorFontSize}"
Tag="{Binding Source={x:Static v:StealHotKey.Enter}}"/>
<Rectangle Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsHitTestVisible="False"
Fill="{DynamicResource Brush.Border2}"/>
<Border Grid.Row="2"
Background="{DynamicResource Brush.Window}"
BorderThickness="0"
CornerRadius="0,0,4,4">
<Grid ColumnDefinitions="Auto,Auto,Auto,*" Margin="0,4">
<Button Grid.Column="0"
Classes="icon_button"
Width="24"
Margin="0,0,4,0" Padding="0"
Click="OnOpenCommitMessagePicker"
IsVisible="{Binding #ThisControl.ShowAdvancedOptions}"
ToolTip.Tip="{DynamicResource Text.WorkingCopy.CommitMessageHelper}">
<Path Width="12" Height="12" Data="{StaticResource Icons.Menu}"/>
</Button>
<Button Grid.Column="1"
Classes="icon_button"
Width="24"
Margin="0,0,4,0" Padding="0"
Click="OnOpenOpenAIHelper"
IsVisible="{Binding #ThisControl.ShowAdvancedOptions}"
ToolTip.Tip="{DynamicResource Text.AIAssistant.Tip}">
<Path Width="13" Height="13" Data="{StaticResource Icons.AIAssist}"/>
</Button>
<Button Grid.Column="2"
Classes="icon_button"
Width="24"
Margin="0,0,4,0" Padding="0"
Click="OnOpenConventionalCommitHelper"
ToolTip.Tip="{DynamicResource Text.ConventionalCommit}">
<Path Width="13" Height="13" Margin="0,1,0,0" Data="{StaticResource Icons.CommitMessageGenerator}"/>
</Button>
<StackPanel Grid.Column="3"
Margin="0,0,6,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Classes="info_label" FontSize="13" HorizontalAlignment="Left" Text="{DynamicResource Text.CommitMessageTextBox.SubjectCount}"/>
<TextBlock Margin="8,0,0,0" FontSize="11" Text="{Binding #Editor.SubjectLength}" IsVisible="{Binding #Editor.SubjectLength, Converter={x:Static c:IntConverters.IsSubjectLengthGood}}" VerticalAlignment="Center"/>
<TextBlock Margin="8,0,0,0" FontSize="11" Foreground="DarkGoldenrod" Text="{Binding #Editor.SubjectLength}" IsVisible="{Binding #Editor.SubjectLength, Converter={x:Static c:IntConverters.IsSubjectLengthBad}}" VerticalAlignment="Center"/>
<TextBlock FontSize="11" Text="/" VerticalAlignment="Center"/>
<TextBlock FontSize="11" Text="{Binding Source={x:Static vm:Preferences.Instance}, Path=SubjectGuideLength}" VerticalAlignment="Center"/>
<Path Width="10" Height="10" Margin="4,0,0,0" Data="{StaticResource Icons.Error}" Fill="DarkGoldenrod" IsVisible="{Binding #Editor.SubjectLength, Converter={x:Static c:IntConverters.IsSubjectLengthBad}}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>

View File

@@ -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<KeyEventArgs> PreviewKeyDownEvent =
RoutedEvent.Register<EnhancedTextBox, KeyEventArgs>(nameof(KeyEventArgs), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public static readonly StyledProperty<string> CommitMessageProperty =
AvaloniaProperty.Register<CommitMessageTextEditor, string>(nameof(CommitMessage), string.Empty);
public event EventHandler<KeyEventArgs> 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<int> SubjectLengthProperty =
AvaloniaProperty.Register<CommitMessageTextEditor, int>(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<IBrush> SubjectLineBrushProperty =
AvaloniaProperty.Register<CommitMessageTextEditor, IBrush>(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<VisualLine>();
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<bool> ShowAdvancedOptionsProperty =
AvaloniaProperty.Register<CommitMessageTextBox, bool>(nameof(ShowAdvancedOptions));
public static readonly StyledProperty<string> TextProperty =
AvaloniaProperty.Register<CommitMessageTextBox, string>(nameof(Text), string.Empty);
public static readonly StyledProperty<string> SubjectProperty =
AvaloniaProperty.Register<CommitMessageTextBox, string>(nameof(Subject), string.Empty);
public static readonly StyledProperty<string> DescriptionProperty =
AvaloniaProperty.Register<CommitMessageTextBox, string>(nameof(Description), string.Empty);
AvaloniaProperty.Register<CommitMessageToolBox, bool>(nameof(ShowAdvancedOptions));
public bool ShowAdvancedOptions
{
@@ -77,102 +248,20 @@ namespace SourceGit.Views
set => SetValue(ShowAdvancedOptionsProperty, value);
}
public string Text
public static readonly StyledProperty<string> CommitMessageProperty =
AvaloniaProperty.Register<CommitMessageToolBox, string>(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;
}
}

View File

@@ -65,7 +65,7 @@
CornerRadius="0,0,8,8"
ClipToBounds="True"
IsVisible="{Binding Popup, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentControl Content="{Binding Popup}" Background="{DynamicResource Brush.Popup}">
<ContentControl x:Name="PopupPanel" Content="{Binding Popup}" Background="{DynamicResource Brush.Popup}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:Popup">
<StackPanel Orientation="Vertical" Background="{DynamicResource Brush.Popup}">

View File

@@ -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;
}

View File

@@ -275,7 +275,7 @@
</Border>
</Grid>
<v:CommitMessageTextBox Margin="0,4,0,0" Height="200" Text="{Binding Content, Mode=TwoWay}"/>
<v:CommitMessageToolBox Margin="0,4,0,0" Height="200" CommitMessage="{Binding Content, Mode=TwoWay}"/>
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>

View File

@@ -18,6 +18,6 @@
<TextBlock Text="{Binding Head.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}" Foreground="DarkOrange"/>
</StackPanel>
<v:CommitMessageTextBox Height="120" Margin="8,5,8,0" Text="{Binding Message, Mode=TwoWay}"/>
<v:CommitMessageToolBox Height="120" Margin="8,5,8,0" CommitMessage="{Binding Message, Mode=TwoWay}"/>
</StackPanel>
</UserControl>

View File

@@ -27,6 +27,6 @@
<TextBlock Grid.Column="3" Margin="8,0,0,0" Text="{Binding Target.Subject}" TextTrimming="CharacterEllipsis"/>
</Grid>
<v:CommitMessageTextBox Height="120" Margin="0,4,0,0" Text="{Binding Message, Mode=TwoWay}"/>
<v:CommitMessageToolBox Height="120" Margin="0,4,0,0" CommitMessage="{Binding Message, Mode=TwoWay}"/>
</StackPanel>
</UserControl>

View File

@@ -239,7 +239,7 @@
Background="Transparent"/>
<!-- Commit Message -->
<v:CommitMessageTextBox Grid.Row="2" ShowAdvancedOptions="True" Text="{Binding CommitMessage, Mode=TwoWay}"/>
<v:CommitMessageToolBox Grid.Row="2" ShowAdvancedOptions="True" CommitMessage="{Binding CommitMessage, Mode=TwoWay}"/>
<!-- Commit Options -->
<Grid Grid.Row="3" Margin="0,6,0,0" ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto,Auto,Auto">