Skip to content

Commit 0e83ccd

Browse files
authored
enhance: extract reusable CopyButton control with copy feedback (#2668)
* enhance: extract reusable CopyButton control with copy feedback * fix: `CopyButton` should not show copied feedback when `CopyText` is empty * code_style: use `DirectProperty` for `CopyText` in `CopyButton`
1 parent 29b9db9 commit 0e83ccd

5 files changed

Lines changed: 127 additions & 80 deletions

File tree

src/Views/CommitBaseInfo.axaml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,7 @@
6565
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Height="24">
6666
<TextBlock Text="{Binding SHA}" Margin="12,0,4,0" FontFamily="{DynamicResource Fonts.Monospace}" VerticalAlignment="Center"/>
6767

68-
<Button Classes="icon_button" Width="24" Cursor="Hand" Click="OnCopyCommitSHA" ToolTip.Tip="{DynamicResource Text.Copy}">
69-
<Grid>
70-
<Path Width="12" Height="12" Data="{StaticResource Icons.Copy}" IsVisible="{Binding #ThisControl.IsSHACopied, Mode=OneWay, Converter={x:Static BoolConverters.Not}}"/>
71-
<Path Width="14" Height="14" Margin="0,2,0,0" Data="{StaticResource Icons.Check}" Fill="Green" IsVisible="{Binding #ThisControl.IsSHACopied, Mode=OneWay}"/>
72-
</Grid>
73-
</Button>
68+
<v:CopyButton Width="24" Cursor="Hand" CopyText="{Binding SHA}" ToolTip.Tip="{DynamicResource Text.Copy}"/>
7469

7570
<Button Classes="icon_button" Width="24" Cursor="Hand" Click="OnOpenContainsIn" IsVisible="{Binding #ThisControl.SupportsContainsIn}" ToolTip.Tip="{DynamicResource Text.CommitDetail.Info.ContainsIn}">
7671
<Path Width="14" Height="14" Margin="0,1,0,0" Data="{StaticResource Icons.Milestone}"/>

src/Views/CommitBaseInfo.axaml.cs

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Avalonia.Controls;
66
using Avalonia.Input;
77
using Avalonia.Interactivity;
8-
using Avalonia.Threading;
98

109
namespace SourceGit.Views
1110
{
@@ -47,17 +46,6 @@ public List<Models.CommitLink> WebLinks
4746
set => SetAndRaise(WebLinksProperty, ref _webLinks, value);
4847
}
4948

50-
public static readonly DirectProperty<CommitBaseInfo, bool> IsSHACopiedProperty =
51-
AvaloniaProperty.RegisterDirect<CommitBaseInfo, bool>(
52-
nameof(IsSHACopied),
53-
static o => o.IsSHACopied);
54-
55-
public bool IsSHACopied
56-
{
57-
get => _isSHACopied;
58-
private set => SetAndRaise(IsSHACopiedProperty, ref _isSHACopied, value);
59-
}
60-
6149
public static readonly DirectProperty<CommitBaseInfo, bool> SupportsContainsInProperty =
6250
AvaloniaProperty.RegisterDirect<CommitBaseInfo, bool>(
6351
nameof(SupportsContainsIn),
@@ -80,45 +68,6 @@ protected override void OnDataContextChanged(EventArgs e)
8068
SupportsContainsIn = DataContext is ViewModels.CommitDetail;
8169
}
8270

83-
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
84-
{
85-
base.OnPropertyChanged(change);
86-
87-
if (change.Property == ContentProperty)
88-
{
89-
IsSHACopied = false;
90-
_iconResetTimer?.Stop();
91-
}
92-
}
93-
94-
protected override void OnLoaded(RoutedEventArgs e)
95-
{
96-
base.OnLoaded(e);
97-
98-
_iconResetTimer = new DispatcherTimer();
99-
_iconResetTimer.Interval = TimeSpan.FromSeconds(1);
100-
_iconResetTimer.Tag = this;
101-
_iconResetTimer.Tick += static (o, _) =>
102-
{
103-
if (o is DispatcherTimer { Tag: CommitBaseInfo view } timer)
104-
{
105-
if (view.IsSHACopied)
106-
view.IsSHACopied = false;
107-
108-
timer.IsEnabled = false;
109-
}
110-
};
111-
_iconResetTimer.IsEnabled = false;
112-
}
113-
114-
protected override void OnUnloaded(RoutedEventArgs e)
115-
{
116-
_iconResetTimer.Tag = null;
117-
_iconResetTimer.IsEnabled = false;
118-
119-
base.OnUnloaded(e);
120-
}
121-
12271
private void OnDateTimeContextMenuRequested(object sender, ContextRequestedEventArgs e)
12372
{
12473
if (sender is DateTimePresenter presenter)
@@ -139,16 +88,6 @@ private void OnDateTimeContextMenuRequested(object sender, ContextRequestedEvent
13988
}
14089
}
14190

142-
private async void OnCopyCommitSHA(object sender, RoutedEventArgs e)
143-
{
144-
if (sender is Button { DataContext: Models.Commit commit })
145-
await this.CopyTextAsync(commit.SHA);
146-
147-
IsSHACopied = true;
148-
_iconResetTimer?.Start();
149-
e.Handled = true;
150-
}
151-
15291
private void OnOpenWebLink(object sender, RoutedEventArgs e)
15392
{
15493
if (DataContext is ViewModels.CommitDetail detail && sender is Control control)
@@ -323,7 +262,5 @@ sender is CommitRefsPresenter presenter &&
323262
private Models.CommitSignInfo _signInfo = null;
324263
private bool _supportsContainsIn = false;
325264
private List<Models.CommitLink> _webLinks = null;
326-
private bool _isSHACopied = false;
327-
private DispatcherTimer _iconResetTimer = null;
328265
}
329266
}

src/Views/CopyButton.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using System;
2+
3+
using Avalonia;
4+
using Avalonia.Controls;
5+
using Avalonia.Controls.Shapes;
6+
using Avalonia.Interactivity;
7+
using Avalonia.Media;
8+
using Avalonia.Threading;
9+
10+
namespace SourceGit.Views
11+
{
12+
public class CopyButton : Button
13+
{
14+
protected override Type StyleKeyOverride => typeof(Button);
15+
16+
public static readonly DirectProperty<CopyButton, string> CopyTextProperty =
17+
AvaloniaProperty.RegisterDirect<CopyButton, string>(
18+
nameof(CopyText),
19+
static o => o.CopyText,
20+
static (o, v) => o.CopyText = v);
21+
22+
public string CopyText
23+
{
24+
get => _copyText;
25+
set => SetAndRaise(CopyTextProperty, ref _copyText, value);
26+
}
27+
28+
public static readonly DirectProperty<CopyButton, bool> IsCopiedProperty =
29+
AvaloniaProperty.RegisterDirect<CopyButton, bool>(
30+
nameof(IsCopied),
31+
static o => o.IsCopied);
32+
33+
public bool IsCopied
34+
{
35+
get => _isCopied;
36+
private set => SetAndRaise(IsCopiedProperty, ref _isCopied, value);
37+
}
38+
39+
public CopyButton()
40+
{
41+
Classes.Add("icon_button");
42+
43+
_copyIcon = new Path() { Width = 12, Height = 12 };
44+
_checkIcon = new Path()
45+
{
46+
Width = 14,
47+
Height = 14,
48+
Margin = new Thickness(0, 2, 0, 0),
49+
Fill = Brushes.Green,
50+
IsVisible = false,
51+
};
52+
53+
var grid = new Grid();
54+
grid.Children.Add(_copyIcon);
55+
grid.Children.Add(_checkIcon);
56+
Content = grid;
57+
}
58+
59+
protected override void OnLoaded(RoutedEventArgs e)
60+
{
61+
base.OnLoaded(e);
62+
63+
if (this.FindResource("Icons.Copy") is Geometry copyGeo)
64+
_copyIcon.Data = copyGeo;
65+
if (this.FindResource("Icons.Check") is Geometry checkGeo)
66+
_checkIcon.Data = checkGeo;
67+
68+
_resetTimer = new DispatcherTimer();
69+
_resetTimer.Interval = TimeSpan.FromSeconds(1);
70+
_resetTimer.Tag = this;
71+
_resetTimer.Tick += static (o, _) =>
72+
{
73+
if (o is DispatcherTimer { Tag: CopyButton btn } timer)
74+
{
75+
btn.IsCopied = false;
76+
timer.IsEnabled = false;
77+
}
78+
};
79+
_resetTimer.IsEnabled = false;
80+
}
81+
82+
protected override void OnUnloaded(RoutedEventArgs e)
83+
{
84+
_resetTimer.Tag = null;
85+
_resetTimer.IsEnabled = false;
86+
base.OnUnloaded(e);
87+
}
88+
89+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
90+
{
91+
base.OnPropertyChanged(change);
92+
93+
if (change.Property == IsCopiedProperty)
94+
{
95+
_copyIcon.IsVisible = !_isCopied;
96+
_checkIcon.IsVisible = _isCopied;
97+
}
98+
else if (change.Property == CopyTextProperty)
99+
{
100+
// Reset the copied state when CopyText changes (e.g. switching to a different commit)
101+
IsCopied = false;
102+
_resetTimer?.Stop();
103+
}
104+
}
105+
106+
protected override async void OnClick()
107+
{
108+
base.OnClick();
109+
110+
var text = CopyText;
111+
if (string.IsNullOrEmpty(text))
112+
return;
113+
114+
await this.CopyTextAsync(text);
115+
IsCopied = true;
116+
_resetTimer?.Start();
117+
}
118+
119+
private readonly Path _copyIcon;
120+
private readonly Path _checkIcon;
121+
private string _copyText = string.Empty;
122+
private bool _isCopied = false;
123+
private DispatcherTimer _resetTimer = null;
124+
}
125+
}

src/Views/LauncherPage.axaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,7 @@
166166
<Path Grid.Column="0" Width="14" Height="14" Data="{StaticResource Icons.Info}" Fill="Green" IsVisible="{Binding !IsError}"/>
167167
<TextBlock Grid.Column="1" Margin="8,0,0,0" FontWeight="Bold" FontSize="14" Text="{DynamicResource Text.Launcher.Error}" IsVisible="{Binding IsError}"/>
168168
<TextBlock Grid.Column="1" Margin="8,0,0,0" FontWeight="Bold" FontSize="14" Text="{DynamicResource Text.Launcher.Info}" IsVisible="{Binding !IsError}"/>
169-
<Button Grid.Column="2" Classes="icon_button" Width="16" Height="16" Click="OnCopyNotification">
170-
<Path Width="12" Height="12" Data="{StaticResource Icons.Copy}"/>
171-
</Button>
169+
<v:CopyButton Grid.Column="2" Width="16" Height="16" CopyText="{Binding Message}"/>
172170
<Button Grid.Column="3" Classes="icon_button" Width="16" Height="16" Margin="8,0,0,0" Click="OnDismissNotification">
173171
<Path Width="10" Height="10" Data="{StaticResource Icons.Close}"/>
174172
</Button>

src/Views/LauncherPage.axaml.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,6 @@ private void OnMaskClicked(object sender, PointerPressedEventArgs e)
7272
OnPopupCancel(sender, e);
7373
}
7474

75-
private async void OnCopyNotification(object sender, RoutedEventArgs e)
76-
{
77-
if (sender is Button { DataContext: Models.Notification notice })
78-
await this.CopyTextAsync(notice.Message);
79-
80-
e.Handled = true;
81-
}
82-
8375
private void OnDismissNotification(object sender, RoutedEventArgs e)
8476
{
8577
if (sender is Button { DataContext: Models.Notification notice } &&

0 commit comments

Comments
 (0)