Skip to content

Commit 779f062

Browse files
committed
enhance: extract reusable CopyButton control with copy feedback
1 parent 29b9db9 commit 779f062

5 files changed

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

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)