ux: improve Interactive Rebase window UX

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2025-10-29 18:31:54 +08:00
parent 79bc093990
commit a869442d78
5 changed files with 194 additions and 29 deletions

View File

@@ -21,8 +21,5 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter<Models.InteractiveRebaseAction, string> ToName =
new FuncValueConverter<Models.InteractiveRebaseAction, string>(v => v.ToString());
public static readonly FuncValueConverter<Models.InteractiveRebaseAction, bool> CanEditMessage =
new FuncValueConverter<Models.InteractiveRebaseAction, bool>(v => v == Models.InteractiveRebaseAction.Reword || v == Models.InteractiveRebaseAction.Squash);
}
}

View File

@@ -25,19 +25,6 @@ namespace SourceGit.ViewModels
get;
}
public bool CanSquashOrFixup
{
get => _canSquashOrFixup;
set
{
if (SetProperty(ref _canSquashOrFixup, value))
{
if (_action == Models.InteractiveRebaseAction.Squash || _action == Models.InteractiveRebaseAction.Fixup)
Action = Models.InteractiveRebaseAction.Pick;
}
}
}
public Models.InteractiveRebaseAction Action
{
get => _action;
@@ -64,6 +51,47 @@ namespace SourceGit.ViewModels
}
}
public string OriginalFullMessage
{
get;
set;
}
public bool CanSquashOrFixup
{
get => _canSquashOrFixup;
set
{
if (SetProperty(ref _canSquashOrFixup, value))
{
if (_action == Models.InteractiveRebaseAction.Squash || _action == Models.InteractiveRebaseAction.Fixup)
{
Action = Models.InteractiveRebaseAction.Pick;
FullMessage = OriginalFullMessage;
}
}
}
}
public bool CanReword
{
get => _canReword;
set
{
if (SetProperty(ref _canReword, value) && _action == Models.InteractiveRebaseAction.Reword)
{
Action = Models.InteractiveRebaseAction.Pick;
FullMessage = OriginalFullMessage;
}
}
}
public bool ShowEditMessageButton
{
get => _showEditMessageButton;
set => SetProperty(ref _showEditMessageButton, value);
}
public bool IsDropBeforeVisible
{
get => _isDropBeforeVisible;
@@ -81,6 +109,7 @@ namespace SourceGit.ViewModels
OriginalOrder = order;
Commit = c;
FullMessage = message;
OriginalFullMessage = message;
CanSquashOrFixup = canSquashOrFixup;
}
@@ -88,6 +117,8 @@ namespace SourceGit.ViewModels
private string _subject;
private string _fullMessage;
private bool _canSquashOrFixup = true;
private bool _canReword = true;
private bool _showEditMessageButton = false;
private bool _isDropBeforeVisible = false;
private bool _isDropAfterVisible = false;
}
@@ -307,6 +338,20 @@ namespace SourceGit.ViewModels
hasValidParent = item.Action != Models.InteractiveRebaseAction.Drop;
}
}
var hasPendingTarget = false;
for (var i = 0; i < Items.Count; i++)
{
var item = Items[i];
if (item.Action == Models.InteractiveRebaseAction.Pick || item.Action == Models.InteractiveRebaseAction.Edit)
item.FullMessage = item.OriginalFullMessage;
item.CanReword = !hasPendingTarget;
item.ShowEditMessageButton = item.CanReword && (item.Action == Models.InteractiveRebaseAction.Squash || item.Action == Models.InteractiveRebaseAction.Reword);
if (item.Action != Models.InteractiveRebaseAction.Drop)
hasPendingTarget = item.Action == Models.InteractiveRebaseAction.Squash || item.Action == Models.InteractiveRebaseAction.Fixup;
}
}
private Repository _repo = null;

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Text.RegularExpressions;
@@ -76,6 +77,15 @@ namespace SourceGit.Views
set => SetValue(LinkForegroundProperty, value);
}
public static readonly StyledProperty<bool> ShowStrikethroughProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, bool>(nameof(ShowStrikethrough), false);
public bool ShowStrikethrough
{
get => GetValue(ShowStrikethroughProperty);
set => SetValue(ShowStrikethroughProperty, value);
}
public static readonly StyledProperty<string> SubjectProperty =
AvaloniaProperty.Register<CommitSubjectPresenter, string>(nameof(Subject));
@@ -115,6 +125,7 @@ namespace SourceGit.Views
{
var height = Bounds.Height;
var width = Bounds.Width;
var maxX = 0.0;
foreach (var inline in _inlines)
{
if (inline.X > width)
@@ -126,12 +137,17 @@ namespace SourceGit.Views
var roundedRect = new RoundedRect(rect, new CornerRadius(4));
context.DrawRectangle(InlineCodeBackground, null, roundedRect);
context.DrawText(inline.Text, new Point(inline.X + 4, (height - inline.Text.Height) * 0.5));
maxX = Math.Min(width, inline.X + inline.Text.WidthIncludingTrailingWhitespace + 8);
}
else
{
context.DrawText(inline.Text, new Point(inline.X, (height - inline.Text.Height) * 0.5));
maxX = Math.Min(width, inline.X + inline.Text.WidthIncludingTrailingWhitespace);
}
}
if (ShowStrikethrough)
context.DrawLine(new Pen(Foreground), new Point(0, height * 0.5), new Point(maxX, height * 0.5));
}
}
@@ -164,7 +180,8 @@ namespace SourceGit.Views
_needRebuildInlines = true;
InvalidateVisual();
}
else if (change.Property == InlineCodeBackgroundProperty)
else if (change.Property == InlineCodeBackgroundProperty ||
change.Property == ShowStrikethroughProperty)
{
InvalidateVisual();
}

View File

@@ -98,7 +98,7 @@
<v:InteractiveRebaseListBox.ItemTemplate>
<DataTemplate DataType="vm:InteractiveRebaseItem">
<Grid Height="26"
<Grid Height="28"
Margin="8,0"
Background="Transparent"
ClipToBounds="True"
@@ -110,6 +110,7 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="CommitOrderColumn"/>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="108"/>
@@ -132,12 +133,18 @@
</StackPanel>
</Button>
<!-- Graph -->
<v:InteractiveRebasePath Grid.Column="2"
Fill="{DynamicResource Brush.FG1}"
Action="{Binding Action, Mode=OneWay}"
CanReword="{Binding CanReword, Mode=OneWay}"/>
<!-- Subject -->
<Grid Grid.Column="2" ColumnDefinitions="Auto,*" ClipToBounds="True">
<Grid Grid.Column="3" ColumnDefinitions="Auto,*" ClipToBounds="True">
<Button Grid.Column="0"
Classes="icon_button"
Margin="0,0,6,0" Padding="0"
IsVisible="{Binding Action, Converter={x:Static c:InteractiveRebaseActionConverters.CanEditMessage}}"
IsVisible="{Binding ShowEditMessageButton, Mode=OneWay}"
Click="OnOpenCommitMessageEditor">
<Path Width="14" Height="14" Margin="0,4,0,0" Data="{StaticResource Icons.Edit}"/>
</Button>
@@ -151,39 +158,41 @@
LinkForeground="{DynamicResource Brush.Link}"
Subject="{Binding Subject}"
IssueTrackers="{Binding $parent[v:InteractiveRebase].((vm:InteractiveRebase)DataContext).IssueTrackers}"
FontWeight="Normal"/>
FontWeight="Normal"
Opacity="{Binding CanReword, Converter={x:Static c:BoolConverters.IsMergedToOpacity}}"
ShowStrikethrough="{Binding Action, Mode=OneWay, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:InteractiveRebaseAction.Drop}}"/>
</Grid>
<!-- Author Avatar -->
<v:Avatar Grid.Column="3"
<v:Avatar Grid.Column="4"
Width="16" Height="16"
Margin="8,0,0,0"
VerticalAlignment="Center"
User="{Binding Commit.Author}"/>
<!-- Author Name -->
<Border Grid.Column="4" ClipToBounds="True">
<Border Grid.Column="5" ClipToBounds="True">
<TextBlock Margin="6,0,12,0" Text="{Binding Commit.Author.Name}"/>
</Border>
<!-- Commit SHA -->
<Border Grid.Column="5" ClipToBounds="True">
<Border Grid.Column="6" ClipToBounds="True">
<TextBlock Text="{Binding Commit.SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"/>
</Border>
<!-- Commit Time -->
<Border Grid.Column="6">
<Border Grid.Column="7">
<TextBlock Margin="16,0,8,0" Text="{Binding Commit.CommitterTimeStr}"/>
</Border>
<!-- Drop Indicator -->
<Rectangle Grid.Column="0" Grid.ColumnSpan="7"
<Rectangle Grid.Column="0" Grid.ColumnSpan="8"
Height="2"
VerticalAlignment="Top"
Fill="{DynamicResource Brush.Accent}"
IsVisible="{Binding IsDropBeforeVisible, Mode=OneWay}"
IsHitTestVisible="False"/>
<Rectangle Grid.Column="0" Grid.ColumnSpan="7"
<Rectangle Grid.Column="0" Grid.ColumnSpan="8"
Height="2"
VerticalAlignment="Bottom"
Fill="{DynamicResource Brush.Accent}"

View File

@@ -85,6 +85,101 @@ namespace SourceGit.Views
}
}
public class InteractiveRebasePath : Control
{
public static readonly StyledProperty<IBrush> FillProperty =
AvaloniaProperty.Register<InteractiveRebasePath, IBrush>(nameof(Fill), Brushes.Transparent);
public IBrush Fill
{
get => GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
public static readonly StyledProperty<Models.InteractiveRebaseAction> ActionProperty =
AvaloniaProperty.Register<InteractiveRebasePath, Models.InteractiveRebaseAction>(nameof(Action));
public Models.InteractiveRebaseAction Action
{
get => GetValue(ActionProperty);
set => SetValue(ActionProperty, value);
}
public static readonly StyledProperty<bool> CanRewordProperty =
AvaloniaProperty.Register<InteractiveRebasePath, bool>(nameof(CanReword));
public bool CanReword
{
get => GetValue(CanRewordProperty);
set => SetValue(CanRewordProperty, value);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var startW = 4;
var endW = Bounds.Width - 4;
var height = Bounds.Height;
var halfH = height * 0.5;
var action = Action;
var fill = Fill;
if (CanReword)
{
if (action == Models.InteractiveRebaseAction.Squash || action == Models.InteractiveRebaseAction.Fixup)
{
var center = new Point(startW, halfH);
context.DrawEllipse(fill, null, center, 4, 4);
context.DrawLine(new Pen(fill, 2), center, new Point(startW, height));
}
}
else
{
if (action == Models.InteractiveRebaseAction.Squash || action == Models.InteractiveRebaseAction.Fixup)
{
context.DrawEllipse(fill, null, new Point(startW, halfH), 4, 4);
context.DrawLine(new Pen(fill, 2), new Point(startW, 0), new Point(startW, height));
}
else if (action == Models.InteractiveRebaseAction.Drop)
{
context.DrawLine(new Pen(fill, 2), new Point(startW, 0), new Point(startW, height));
}
else
{
var geoPath = new StreamGeometry();
using (var ctx = geoPath.Open())
{
ctx.BeginFigure(new Point(startW, 0), false);
ctx.QuadraticBezierTo(new Point(startW, halfH), new Point(endW, halfH));
ctx.EndFigure(false);
}
context.DrawGeometry(null, new Pen(fill, 2), geoPath);
var geoArrow = new StreamGeometry();
using (var ctx = geoPath.Open())
{
ctx.BeginFigure(new Point(endW, halfH), true);
ctx.LineTo(new Point(endW - 4, halfH + 2));
ctx.LineTo(new Point(endW - 4, halfH - 2));
ctx.EndFigure(true);
}
context.DrawGeometry(fill, null, geoArrow);
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == FillProperty ||
change.Property == ActionProperty ||
change.Property == CanRewordProperty)
InvalidateVisual();
}
}
public partial class InteractiveRebase : ChromelessWindow
{
public InteractiveRebase()
@@ -312,7 +407,9 @@ namespace SourceGit.Views
CreateActionMenuItem(flyout, item, Models.InteractiveRebaseAction.Pick, Brushes.Green, "Use this commit", "P");
CreateActionMenuItem(flyout, item, Models.InteractiveRebaseAction.Edit, Brushes.Orange, "Stop for amending", "E");
CreateActionMenuItem(flyout, item, Models.InteractiveRebaseAction.Reword, Brushes.Orange, "Edit the commit message", "R");
if (item.CanReword)
CreateActionMenuItem(flyout, item, Models.InteractiveRebaseAction.Reword, Brushes.Orange, "Edit the commit message", "R");
if (item.CanSquashOrFixup)
{