Skip to content

Commit 40cf560

Browse files
committed
refactor: rewrite CommitMessageTextBox
1 parent 91ba181 commit 40cf560

File tree

5 files changed

+153
-123
lines changed

5 files changed

+153
-123
lines changed

src/Resources/Locales/en_US.axaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@
105105
<x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">PARENTS</x:String>
106106
<x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">REFS</x:String>
107107
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">SHA</x:String>
108-
<x:String x:Key="Text.CommitMessageTextBox.Placeholder" xml:space="preserve">Enter commit subject &amp; message</x:String>
109-
<x:String x:Key="Text.CommitMessageTextBox.Tip" xml:space="preserve">Git uses an empty line to separate the subject and extra message body.</x:String>
108+
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">Enter commit subject</x:String>
109+
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">Description</x:String>
110110
<x:String x:Key="Text.Configure" xml:space="preserve">Repository Configure</x:String>
111111
<x:String x:Key="Text.Configure.Email" xml:space="preserve">Email Address</x:String>
112112
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">Email address</x:String>

src/Resources/Locales/zh_CN.axaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@
108108
<x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">父提交</x:String>
109109
<x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">相关引用</x:String>
110110
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">提交指纹</x:String>
111-
<x:String x:Key="Text.CommitMessageTextBox.Placeholder" xml:space="preserve">填写提交信息</x:String>
112-
<x:String x:Key="Text.CommitMessageTextBox.Tip" xml:space="preserve">Git使用空白行来划分提交信息中的主题与内容</x:String>
111+
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">填写提交信息主题</x:String>
112+
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">详细描述</x:String>
113113
<x:String x:Key="Text.Configure" xml:space="preserve">仓库配置</x:String>
114114
<x:String x:Key="Text.Configure.Email" xml:space="preserve">电子邮箱</x:String>
115115
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">邮箱地址</x:String>

src/Resources/Locales/zh_TW.axaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@
108108
<x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">父提交</x:String>
109109
<x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">相關引用</x:String>
110110
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">提交指紋</x:String>
111-
<x:String x:Key="Text.CommitMessageTextBox.Placeholder" xml:space="preserve">填寫提交資訊</x:String>
112-
<x:String x:Key="Text.CommitMessageTextBox.Tip" xml:space="preserve">Git使用空白行來劃分提交資訊中的主題與內容</x:String>
111+
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">填寫提交信息主題</x:String>
112+
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">詳細描述</x:String>
113113
<x:String x:Key="Text.Configure" xml:space="preserve">倉庫配置</x:String>
114114
<x:String x:Key="Text.Configure.Email" xml:space="preserve">電子郵箱</x:String>
115115
<x:String x:Key="Text.Configure.Email.Placeholder" xml:space="preserve">郵箱地址</x:String>

src/Views/CommitMessageTextBox.axaml

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,77 @@
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
33
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
44
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5-
xmlns:ae="using:AvaloniaEdit"
65
xmlns:c="using:SourceGit.Converters"
76
xmlns:vm="using:SourceGit.ViewModels"
7+
xmlns:v="using:SourceGit.Views"
88
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
99
x:Class="SourceGit.Views.CommitMessageTextBox"
1010
x:Name="ThisControl">
1111
<Border Background="{DynamicResource Brush.Contents}"
1212
BorderThickness="1"
1313
BorderBrush="{DynamicResource Brush.Border2}"
1414
CornerRadius="4">
15-
<Grid RowDefinitions="*,Auto">
16-
<Grid Grid.Row="0">
17-
<Rectangle x:Name="SubjectGuideLine"
18-
Height="0.8"
19-
HorizontalAlignment="Stretch"
20-
VerticalAlignment="Top"
15+
<Grid RowDefinitions="Auto,1,*">
16+
<Grid Grid.Row="0" ColumnDefinitions="*,1,Auto">
17+
<v:EnhancedTextBox Grid.Column="0"
18+
x:Name="SubjectEditor"
19+
Classes="no_border"
20+
Margin="0"
21+
Padding="4"
22+
CornerRadius="4,4,0,0"
23+
BorderThickness="0"
24+
Background="Transparent"
25+
AcceptsReturn="False"
26+
TextWrapping="Wrap"
27+
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
28+
ScrollViewer.VerticalScrollBarVisibility="Disabled"
29+
Text="{Binding #ThisControl.Subject, Mode=TwoWay}"
30+
Watermark="{DynamicResource Text.CommitMessageTextBox.SubjectPlaceholder}"
31+
PreviewKeyDown="OnSubjectTextBoxPreviewKeyDown"/>
32+
33+
<Rectangle Grid.Column="1"
34+
Width="1"
35+
HorizontalAlignment="Center"
36+
VerticalAlignment="Stretch"
2137
IsHitTestVisible="False"
2238
Fill="{DynamicResource Brush.Border2}"/>
2339

24-
<ae:TextEditor x:Name="TextEditor"
25-
Foreground="{DynamicResource Brush.FG1}"
26-
Background="Transparent"
27-
Padding="4"
28-
BorderThickness="0"
29-
WordWrap="True"
30-
Document="{Binding #ThisControl.Document}"
31-
TextChanged="OnTextEditorTextChanged"
32-
LayoutUpdated="OnTextEditorLayoutUpdated"/>
33-
34-
<TextBlock Text="{DynamicResource Text.CommitMessageTextBox.Placeholder}"
35-
Foreground="{DynamicResource Brush.FG2}"
36-
HorizontalAlignment="Left"
37-
VerticalAlignment="Top"
38-
Margin="3"
39-
IsVisible="{Binding #ThisControl.Text, Converter={x:Static StringConverters.IsNullOrEmpty}}"
40-
IsHitTestVisible="False"/>
40+
<StackPanel Grid.Column="2"
41+
Margin="8,0"
42+
VerticalAlignment="Center"
43+
Orientation="Horizontal">
44+
<TextBlock Classes="monospace" Margin="2,0,0,0" FontSize="11" Text="{Binding #ThisControl.Subject.Length}" IsVisible="{Binding #ThisControl.Subject.Length, Converter={x:Static c:IntConverters.IsSubjectLengthGood}}"/>
45+
<TextBlock Classes="monospace" Margin="2,0,0,0" FontSize="11" Foreground="DarkGoldenrod" Text="{Binding #ThisControl.Subject.Length}" IsVisible="{Binding #ThisControl.Subject.Length, Converter={x:Static c:IntConverters.IsSubjectLengthBad}}"/>
46+
<TextBlock Classes="monospace" FontSize="11" Text="/"/>
47+
<TextBlock Classes="monospace" FontSize="11" Text="{Binding Source={x:Static vm:Preference.Instance}, Path=SubjectGuideLength}"/>
48+
<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}}"/>
49+
</StackPanel>
4150
</Grid>
4251

43-
<Border Grid.Row="1" CornerRadius="0,0,4,4" BorderThickness="0,1,0,0" BorderBrush="{DynamicResource Brush.Border2}" Background="{DynamicResource Brush.Window}" ClipToBounds="True">
44-
<Grid ColumnDefinitions="Auto,*,Auto">
45-
<Border Grid.Column="0" Background="Transparent" Width="16" Height="16" ToolTip.Tip="{DynamicResource Text.CommitMessageTextBox.Tip}">
46-
<Path Height="12" Width="12" Margin="4,0,0,0" Data="{DynamicResource Icons.Info}"/>
47-
</Border>
48-
49-
<StackPanel Grid.Column="2" Orientation="Horizontal" Margin="4,2">
50-
<TextBlock Text="Subject:" FontSize="10" Foreground="{DynamicResource Brush.FG2}"/>
51-
<TextBlock Classes="monospace" Margin="2,0,0,0" FontSize="11" Text="{Binding #ThisControl.SubjectLength}" IsVisible="{Binding #ThisControl.SubjectLength, Converter={x:Static c:IntConverters.IsSubjectLengthGood}}"/>
52-
<TextBlock Classes="monospace" Margin="2,0,0,0" FontSize="11" Foreground="DarkGoldenrod" Text="{Binding #ThisControl.SubjectLength}" IsVisible="{Binding #ThisControl.SubjectLength, Converter={x:Static c:IntConverters.IsSubjectLengthBad}}"/>
53-
<TextBlock Classes="monospace" FontSize="11" Text="/"/>
54-
<TextBlock Classes="monospace" FontSize="11" Text="{Binding Source={x:Static vm:Preference.Instance}, Path=SubjectGuideLength}"/>
55-
<Path Width="10" Height="10" Margin="4,0,0,0" Data="{StaticResource Icons.Error}" Fill="DarkGoldenrod" IsVisible="{Binding #ThisControl.SubjectLength, Converter={x:Static c:IntConverters.IsSubjectLengthBad}}"/>
56-
<TextBlock Margin="8,0,0,0" Text="Total:" FontSize="11" Foreground="{DynamicResource Brush.FG2}"/>
57-
<TextBlock Classes="monospace" Margin="2,0,0,0" FontSize="11" Text="{Binding #ThisControl.TotalLength}"/>
58-
</StackPanel>
59-
</Grid>
60-
</Border>
52+
<Rectangle Grid.Row="1"
53+
Height="1"
54+
HorizontalAlignment="Stretch"
55+
VerticalAlignment="Center"
56+
IsHitTestVisible="False"
57+
Fill="{DynamicResource Brush.Border2}"/>
58+
59+
<v:EnhancedTextBox Grid.Row="2"
60+
x:Name="DescriptionEditor"
61+
Classes="no_border"
62+
Margin="0"
63+
Padding="4"
64+
CornerRadius="0,0,4,4"
65+
BorderThickness="0"
66+
Background="Transparent"
67+
VerticalContentAlignment="Top"
68+
AcceptsReturn="True"
69+
AcceptsTab="True"
70+
TextWrapping="Wrap"
71+
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
72+
ScrollViewer.VerticalScrollBarVisibility="Auto"
73+
Text="{Binding #ThisControl.Description, Mode=TwoWay}"
74+
Watermark="{DynamicResource Text.CommitMessageTextBox.MessagePlaceholder}"
75+
PreviewKeyDown="OnDescriptionTextBoxPreviewKeyDown"/>
6176
</Grid>
6277
</Border>
6378
</UserControl>

src/Views/CommitMessageTextBox.axaml.cs

Lines changed: 90 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,122 +2,137 @@
22

33
using Avalonia;
44
using Avalonia.Controls;
5-
6-
using AvaloniaEdit.Document;
7-
using AvaloniaEdit.Rendering;
5+
using Avalonia.Input;
6+
using Avalonia.Interactivity;
87

98
namespace SourceGit.Views
109
{
10+
public class EnhancedTextBox : TextBox
11+
{
12+
public static readonly RoutedEvent<KeyEventArgs> PreviewKeyDownEvent =
13+
RoutedEvent.Register<ChangeCollectionView, KeyEventArgs>(nameof(KeyEventArgs), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
14+
15+
public event EventHandler<KeyEventArgs> PreviewKeyDown
16+
{
17+
add { AddHandler(PreviewKeyDownEvent, value); }
18+
remove { RemoveHandler(PreviewKeyDownEvent, value); }
19+
}
20+
21+
protected override Type StyleKeyOverride => typeof(TextBox);
22+
23+
protected override void OnKeyDown(KeyEventArgs e)
24+
{
25+
var dump = new KeyEventArgs()
26+
{
27+
RoutedEvent = PreviewKeyDownEvent,
28+
Route = RoutingStrategies.Direct,
29+
Source = e.Source,
30+
Key = e.Key,
31+
KeyModifiers = e.KeyModifiers,
32+
PhysicalKey = e.PhysicalKey,
33+
KeySymbol = e.KeySymbol,
34+
};
35+
36+
RaiseEvent(dump);
37+
38+
if (dump.Handled)
39+
e.Handled = true;
40+
else
41+
base.OnKeyDown(e);
42+
}
43+
}
44+
1145
public partial class CommitMessageTextBox : UserControl
1246
{
47+
public enum TextChangeWay
48+
{
49+
None,
50+
FromSource,
51+
FromEditor,
52+
}
53+
1354
public static readonly StyledProperty<string> TextProperty =
1455
AvaloniaProperty.Register<CommitMessageTextBox, string>(nameof(Text), string.Empty);
1556

16-
public static readonly DirectProperty<CommitMessageTextBox, int> SubjectLengthProperty =
17-
AvaloniaProperty.RegisterDirect<CommitMessageTextBox, int>(nameof(SubjectLength), o => o.SubjectLength);
57+
public static readonly StyledProperty<string> SubjectProperty =
58+
AvaloniaProperty.Register<CommitMessageTextBox, string>(nameof(Subject), string.Empty);
59+
60+
public static readonly StyledProperty<string> DescriptionProperty =
61+
AvaloniaProperty.Register<CommitMessageTextBox, string>(nameof(Description), string.Empty);
1862

19-
public static readonly DirectProperty<CommitMessageTextBox, int> TotalLengthProperty =
20-
AvaloniaProperty.RegisterDirect<CommitMessageTextBox, int>(nameof(TotalLength), o => o.TotalLength);
21-
2263
public string Text
2364
{
2465
get => GetValue(TextProperty);
2566
set => SetValue(TextProperty, value);
2667
}
2768

28-
public int SubjectLength
69+
public string Subject
2970
{
30-
get => _subjectLength;
31-
private set => SetAndRaise(SubjectLengthProperty, ref _subjectLength, value);
71+
get => GetValue(SubjectProperty);
72+
set => SetValue(SubjectProperty, value);
3273
}
3374

34-
public int TotalLength
75+
public string Description
3576
{
36-
get => _totalLength;
37-
private set => SetAndRaise(TotalLengthProperty, ref _totalLength, value);
77+
get => GetValue(DescriptionProperty);
78+
set => SetValue(DescriptionProperty, value);
3879
}
3980

40-
public TextDocument Document
41-
{
42-
get;
43-
}
44-
4581
public CommitMessageTextBox()
4682
{
47-
Document = new TextDocument(Text);
4883
InitializeComponent();
4984
}
50-
85+
5186
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
5287
{
5388
base.OnPropertyChanged(change);
5489

55-
if (change.Property == TextProperty && !_isDocumentTextChanging)
56-
Document.Text = Text;
57-
}
58-
59-
private void OnTextEditorLayoutUpdated(object sender, EventArgs e)
60-
{
61-
var view = TextEditor.TextArea?.TextView;
62-
if (view is { VisualLinesValid: true })
90+
if (change.Property == TextProperty && _changingWay == TextChangeWay.None)
6391
{
64-
if (_subjectEndLineNumber == 0)
92+
_changingWay = TextChangeWay.FromSource;
93+
var normalized = Text.ReplaceLineEndings("\n").Trim();
94+
var subjectEnd = normalized.IndexOf("\n\n", StringComparison.Ordinal);
95+
if (subjectEnd == -1)
6596
{
66-
SubjectGuideLine.Margin = new Thickness(0, view.DefaultLineHeight + 3, 0, 0);
67-
SubjectGuideLine.IsVisible = true;
68-
return;
97+
SetCurrentValue(SubjectProperty, normalized.ReplaceLineEndings(" "));
98+
SetCurrentValue(DescriptionProperty, string.Empty);
6999
}
70-
71-
foreach (var line in view.VisualLines)
100+
else
72101
{
73-
var lineNumber = line.FirstDocumentLine.LineNumber;
74-
if (lineNumber == _subjectEndLineNumber)
75-
{
76-
var y = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - view.VerticalOffset + 3;
77-
SubjectGuideLine.Margin = new Thickness(0, y, 0, 0);
78-
SubjectGuideLine.IsVisible = true;
79-
return;
80-
}
102+
SetCurrentValue(SubjectProperty, normalized.Substring(0, subjectEnd).ReplaceLineEndings(" "));
103+
SetCurrentValue(DescriptionProperty, normalized.Substring(subjectEnd + 2));
81104
}
105+
_changingWay = TextChangeWay.None;
106+
}
107+
else if ((change.Property == SubjectProperty || change.Property == DescriptionProperty) && _changingWay == TextChangeWay.None)
108+
{
109+
_changingWay = TextChangeWay.FromEditor;
110+
SetCurrentValue(TextProperty, $"{Subject}\n\n{Description}");
111+
_changingWay = TextChangeWay.None;
82112
}
83-
84-
SubjectGuideLine.IsVisible = false;
85113
}
86114

87-
private void OnTextEditorTextChanged(object sender, EventArgs e)
115+
private void OnSubjectTextBoxPreviewKeyDown(object sender, KeyEventArgs e)
88116
{
89-
var text = Document.Text;
90-
_isDocumentTextChanging = true;
91-
SetCurrentValue(TextProperty, text);
92-
TotalLength = text.Trim().Length;
93-
_isDocumentTextChanging = false;
94-
95-
var foundData = false;
96-
for (var i = 0; i < Document.LineCount; i++)
117+
if ((e.Key == Key.Enter && e.KeyModifiers == KeyModifiers.None)
118+
|| (e.Key == Key.Right && SubjectEditor.CaretIndex == Subject.Length))
97119
{
98-
var line = Document.Lines[i];
99-
if (line.Length == 0)
100-
{
101-
if (foundData)
102-
{
103-
SubjectLength = text[..line.Offset].ReplaceLineEndings(" ").Trim().Length;
104-
return;
105-
}
106-
}
107-
else
108-
{
109-
foundData = true;
110-
}
111-
112-
_subjectEndLineNumber = line.LineNumber;
120+
DescriptionEditor.Focus();
121+
DescriptionEditor.CaretIndex = 0;
122+
e.Handled = true;
123+
}
124+
}
125+
126+
private void OnDescriptionTextBoxPreviewKeyDown(object sender, KeyEventArgs e)
127+
{
128+
if ((e.Key == Key.Back || e.Key == Key.Left) && DescriptionEditor.CaretIndex == 0)
129+
{
130+
SubjectEditor.Focus();
131+
SubjectEditor.CaretIndex = Subject.Length;
132+
e.Handled = true;
113133
}
114-
115-
SubjectLength = text.ReplaceLineEndings(" ").Trim().Length;
116134
}
117135

118-
private bool _isDocumentTextChanging = false;
119-
private int _subjectEndLineNumber = 0;
120-
private int _totalLength = 0;
121-
private int _subjectLength = 0;
136+
private TextChangeWay _changingWay = TextChangeWay.None;
122137
}
123138
}

0 commit comments

Comments
 (0)