feature: enable multiple-selection for changes collection view in Changes Detail/Stash Changes/Branch Compare/Revision Compare (#1826)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2025-10-21 16:05:11 +08:00
parent a70182e7b7
commit fe471ac89d
14 changed files with 590 additions and 206 deletions

View File

@@ -72,5 +72,20 @@ namespace SourceGit.Commands
Args = $"checkout --no-overlay {revision} -- {file.Quoted()}";
return await ExecAsync().ConfigureAwait(false);
}
public async Task<bool> MultipleFilesWithRevisionAsync(List<string> files, string revision)
{
var builder = new StringBuilder();
builder
.Append("checkout --no-overlay ")
.Append(revision)
.Append(" --");
foreach (var f in files)
builder.Append(' ').Append(f.Quoted());
Args = builder.ToString();
return await ExecAsync().ConfigureAwait(false);
}
}
}

View File

@@ -128,6 +128,13 @@ namespace SourceGit.ViewModels
return Native.OS.GetAbsPath(_repo, path);
}
public async Task SaveChangesAsPatchAsync(List<Models.Change> changes, string saveTo)
{
var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes, _based.Head, _to.Head, saveTo);
if (succ)
App.SendNotification(_repo, App.Text("SaveAsPatchSuccess"));
}
private void Refresh()
{
IsLoading = true;

View File

@@ -239,7 +239,7 @@ namespace SourceGit.ViewModels
public async Task ResetToThisRevisionAsync(string path)
{
var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'");
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, $"{_commit.SHA}");
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, _commit.SHA);
log.Complete();
}
@@ -254,6 +254,41 @@ namespace SourceGit.ViewModels
log.Complete();
}
public async Task ResetMultipleToThisRevisionAsync(List<Models.Change> changes)
{
var files = new List<string>();
foreach (var c in changes)
files.Add(c.Path);
var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}'");
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(files, _commit.SHA);
log.Complete();
}
public async Task ResetMultipleToParentRevisionAsync(List<Models.Change> changes)
{
var renamed = new List<string>();
var modified = new List<string>();
foreach (var c in changes)
{
if (c.Index == Models.ChangeState.Renamed)
renamed.Add(c.OriginalPath);
else
modified.Add(c.Path);
}
var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}~1'");
if (modified.Count > 0)
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(modified, $"{_commit.SHA}~1");
if (renamed.Count > 0)
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(renamed, $"{_commit.SHA}~1");
log.Complete();
}
public async Task<List<Models.Object>> GetRevisionFilesUnderFolderAsync(string parentFolder)
{
return await new Commands.QueryRevisionObjects(_repo.FullPath, _commit.SHA, parentFolder)

View File

@@ -127,14 +127,11 @@ namespace SourceGit.ViewModels
return Native.OS.GetAbsPath(_repo, path);
}
public void SaveAsPatch(string saveTo)
public async Task SaveChangesAsPatchAsync(List<Models.Change> changes, string saveTo)
{
Task.Run(async () =>
{
var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo);
if (succ)
App.SendNotification(_repo, App.Text("SaveAsPatchSuccess"));
});
var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes ?? _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo);
if (succ)
App.SendNotification(_repo, App.Text("SaveAsPatchSuccess"));
}
public void ClearSearchFilter()

View File

@@ -158,7 +158,7 @@ namespace SourceGit.ViewModels
_repo.ShowPopup(new DropStash(_repo, stash));
}
public async Task SaveStashAsPathAsync(Models.Stash stash, string saveTo)
public async Task SaveStashAsPatchAsync(Models.Stash stash, string saveTo)
{
var opts = new List<Models.DiffOption>();
var changes = await new Commands.CompareRevisions(_repo.FullPath, $"{stash.SHA}^", stash.SHA)
@@ -211,6 +211,42 @@ namespace SourceGit.ViewModels
log.Complete();
}
public async Task CheckoutMultipleFileAsync(List<Models.Change> changes)
{
var untracked = new List<string>();
var added = new List<string>();
var modified = new List<string>();
foreach (var c in changes)
{
if (_untracked.Contains(c) && _selectedStash.Parents.Count == 3)
untracked.Add(c.Path);
else if (c.Index == Models.ChangeState.Added && _selectedStash.Parents.Count > 1)
added.Add(c.Path);
else
modified.Add(c.Path);
}
var log = _repo.CreateLog($"Reset File to '{_selectedStash.Name}'");
if (untracked.Count > 0)
await new Commands.Checkout(_repo.FullPath)
.Use(log)
.MultipleFilesWithRevisionAsync(untracked, _selectedStash.Parents[2]);
if (added.Count > 0)
await new Commands.Checkout(_repo.FullPath)
.Use(log)
.MultipleFilesWithRevisionAsync(added, _selectedStash.Parents[1]);
if (modified.Count > 0)
await new Commands.Checkout(_repo.FullPath)
.Use(log)
.MultipleFilesWithRevisionAsync(modified, _selectedStash.SHA);
log.Complete();
}
private void RefreshVisible()
{
if (string.IsNullOrEmpty(_searchFilter))

View File

@@ -1,8 +1,10 @@
using System;
using System.IO;
using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform.Storage;
namespace SourceGit.Views
{
@@ -15,61 +17,127 @@ namespace SourceGit.Views
private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e)
{
if (DataContext is ViewModels.BranchCompare { SelectedChanges: { Count: 1 } selected } vm &&
if (DataContext is ViewModels.BranchCompare { SelectedChanges: { Count: > 0 } selected } vm &&
sender is ChangeCollectionView view)
{
var repo = vm.RepositoryPath;
var change = selected[0];
var menu = new ContextMenu();
var repo = vm.RepositoryPath;
var openWithMerger = new MenuItem();
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
openWithMerger.Click += (_, ev) =>
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) =>
{
new Commands.DiffTool(repo, new Models.DiffOption(vm.Base.Head, vm.To.Head, change)).Open();
ev.Handled = true;
};
menu.Items.Add(openWithMerger);
var storageProvider = this.StorageProvider;
if (storageProvider == null)
return;
if (change.Index != Models.ChangeState.Deleted)
{
var full = Path.GetFullPath(Path.Combine(repo, change.Path));
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = File.Exists(full);
explore.Click += (_, ev) =>
var options = new FilePickerSaveOptions();
options.Title = App.Text("FileCM.SaveAsPatch");
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
Native.OS.OpenInFileManager(full, true);
var saveTo = storageFile.Path.LocalPath;
await vm.SaveChangesAsPatchAsync(selected, saveTo);
}
e.Handled = true;
};
if (selected.Count == 1)
{
var change = selected[0];
var openWithMerger = new MenuItem();
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
openWithMerger.Click += (_, ev) =>
{
new Commands.DiffTool(repo, new Models.DiffOption(vm.Base.Head, vm.To.Head, change)).Open();
ev.Handled = true;
};
menu.Items.Add(explore);
menu.Items.Add(openWithMerger);
if (change.Index != Models.ChangeState.Deleted)
{
var full = Path.GetFullPath(Path.Combine(repo, change.Path));
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = File.Exists(full);
explore.Click += (_, ev) =>
{
Native.OS.OpenInFileManager(full, true);
ev.Handled = true;
};
menu.Items.Add(explore);
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(change.Path);
ev.Handled = true;
};
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(Native.OS.GetAbsPath(repo, change.Path));
ev.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
}
else
{
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
var builder = new StringBuilder();
foreach (var c in selected)
builder.AppendLine(c.Path);
await App.CopyTextAsync(builder.ToString());
ev.Handled = true;
};
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
var builder = new StringBuilder();
foreach (var c in selected)
builder.AppendLine(Native.OS.GetAbsPath(repo, c.Path));
await App.CopyTextAsync(builder.ToString());
ev.Handled = true;
};
menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(change.Path);
ev.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copyPath);
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(Native.OS.GetAbsPath(repo, change.Path));
ev.Handled = true;
};
menu.Items.Add(copyFullPath);
menu.Open(view);
}
@@ -89,17 +157,24 @@ namespace SourceGit.Views
if (DataContext is not ViewModels.BranchCompare vm)
return;
if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges })
if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges })
return;
var change = selectedChanges[0];
if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C)
{
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
await App.CopyTextAsync(vm.GetAbsPath(change.Path));
var builder = new StringBuilder();
var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (selectedChanges.Count == 1)
{
builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path);
}
else
await App.CopyTextAsync(change.Path);
{
foreach (var c in selectedChanges)
builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path);
}
await App.CopyTextAsync(builder.ToString());
e.Handled = true;
}
}

View File

@@ -33,7 +33,7 @@
<v:ChangeCollectionContainer Focusable="True"
ItemsSource="{Binding Rows}"
SelectedItems="{Binding SelectedRows, Mode=TwoWay}"
SelectionMode="{Binding #ThisControl.SelectionMode}"
SelectionMode="Multiple"
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:ChangeTreeNode">
@@ -78,7 +78,7 @@
<v:ChangeCollectionContainer Focusable="True"
ItemsSource="{Binding Changes}"
SelectedItems="{Binding SelectedChanges, Mode=TwoWay}"
SelectionMode="{Binding #ThisControl.SelectionMode}"
SelectionMode="Multiple"
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Change">
@@ -110,7 +110,7 @@
<v:ChangeCollectionContainer Focusable="True"
ItemsSource="{Binding Changes}"
SelectedItems="{Binding SelectedChanges, Mode=TwoWay}"
SelectionMode="{Binding #ThisControl.SelectionMode}"
SelectionMode="Multiple"
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Change">

View File

@@ -61,15 +61,6 @@ namespace SourceGit.Views
set => SetValue(IsUnstagedChangeProperty, value);
}
public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
AvaloniaProperty.Register<ChangeCollectionView, SelectionMode>(nameof(SelectionMode));
public SelectionMode SelectionMode
{
get => GetValue(SelectionModeProperty);
set => SetValue(SelectionModeProperty, value);
}
public static readonly StyledProperty<Models.ChangeViewMode> ViewModeProperty =
AvaloniaProperty.Register<ChangeCollectionView, Models.ChangeViewMode>(nameof(ViewMode), Models.ChangeViewMode.Tree);

View File

@@ -45,8 +45,7 @@
<!-- Changes -->
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<v:ChangeCollectionView SelectionMode="Single"
ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=CommitChangeViewMode}"
<v:ChangeCollectionView ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=CommitChangeViewMode}"
EnableCompactFolders="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableCompactFoldersInChangesTree}"
Changes="{Binding VisibleChanges}"
SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}"

View File

@@ -1,4 +1,5 @@
using System;
using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
@@ -17,27 +18,20 @@ namespace SourceGit.Views
{
e.Handled = true;
if (sender is not ChangeCollectionView view)
if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } changes } view)
return;
var detailView = this.FindAncestorOfType<CommitDetail>();
if (detailView == null)
return;
var changes = view.SelectedChanges ?? [];
var container = view.FindDescendantOfType<ChangeCollectionContainer>();
if (container is { SelectedItems.Count: 1, SelectedItem: ViewModels.ChangeTreeNode { IsFolder: true } node })
{
var menu = detailView.CreateChangeContextMenuByFolder(node, changes);
menu.Open(view);
return;
}
if (changes.Count == 1)
{
var menu = detailView.CreateChangeContextMenu(changes[0]);
menu.Open(view);
}
detailView.CreateChangeContextMenuByFolder(node, changes)?.Open(view);
else if (changes.Count > 1)
detailView.CreateMultipleChangesContextMenu(changes)?.Open(view);
else
detailView.CreateChangeContextMenu(changes[0])?.Open(view);
}
private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e)
@@ -45,18 +39,30 @@ namespace SourceGit.Views
if (DataContext is not ViewModels.CommitDetail vm)
return;
if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges })
if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges } view)
return;
var change = selectedChanges[0];
if (e.Key == Key.C &&
e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control))
{
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
await App.CopyTextAsync(vm.GetAbsPath(change.Path));
var builder = new StringBuilder();
var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
var container = view.FindDescendantOfType<ChangeCollectionContainer>();
if (container is { SelectedItems.Count: 1, SelectedItem: ViewModels.ChangeTreeNode { IsFolder: true } node })
{
builder.Append(copyAbsPath ? vm.GetAbsPath(node.FullPath) : node.FullPath);
}
else if (selectedChanges.Count == 1)
{
builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path);
}
else
await App.CopyTextAsync(change.Path);
{
foreach (var c in selectedChanges)
builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path);
}
await App.CopyTextAsync(builder.ToString());
e.Handled = true;
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
@@ -97,6 +98,98 @@ namespace SourceGit.Views
return menu;
}
public ContextMenu CreateMultipleChangesContextMenu(List<Models.Change> changes)
{
if (DataContext is not ViewModels.CommitDetail { Repository: { } repo, Commit: { } commit } vm)
return null;
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) =>
{
var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
if (storageProvider == null)
return;
var options = new FilePickerSaveOptions();
options.Title = App.Text("FileCM.SaveAsPatch");
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
var saveTo = storageFile.Path.LocalPath;
await vm.SaveChangesAsPatchAsync(changes, saveTo);
}
e.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" });
if (!repo.IsBare)
{
var resetToThisRevision = new MenuItem();
resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision");
resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout");
resetToThisRevision.Click += async (_, ev) =>
{
await vm.ResetMultipleToThisRevisionAsync(changes);
ev.Handled = true;
};
var resetToFirstParent = new MenuItem();
resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision");
resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout");
resetToFirstParent.IsEnabled = commit.Parents.Count > 0;
resetToFirstParent.Click += async (_, ev) =>
{
await vm.ResetMultipleToParentRevisionAsync(changes);
ev.Handled = true;
};
menu.Items.Add(resetToThisRevision);
menu.Items.Add(resetToFirstParent);
menu.Items.Add(new MenuItem { Header = "-" });
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
var builder = new StringBuilder();
foreach (var c in changes)
builder.AppendLine(c.Path);
await App.CopyTextAsync(builder.ToString());
ev.Handled = true;
};
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, e) =>
{
var builder = new StringBuilder();
foreach (var c in changes)
builder.AppendLine(Native.OS.GetAbsPath(repo.FullPath, c.Path));
await App.CopyTextAsync(builder.ToString());
e.Handled = true;
};
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
return menu;
}
public ContextMenu CreateChangeContextMenu(Models.Change change)
{
if (DataContext is not ViewModels.CommitDetail { Repository: { } repo, Commit: { } commit } vm)

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
@@ -17,59 +18,126 @@ namespace SourceGit.Views
private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e)
{
if (DataContext is ViewModels.RevisionCompare { SelectedChanges: { Count: 1 } selected } vm &&
if (DataContext is ViewModels.RevisionCompare { SelectedChanges: { Count: > 0 } selected } vm &&
sender is ChangeCollectionView view)
{
var change = selected[0];
var changeFullPath = vm.GetAbsPath(change.Path);
var menu = new ContextMenu();
var openWithMerger = new MenuItem();
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
openWithMerger.Click += (_, ev) =>
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) =>
{
vm.OpenChangeWithExternalDiffTool(change);
ev.Handled = true;
};
menu.Items.Add(openWithMerger);
var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
if (storageProvider == null)
return;
if (change.Index != Models.ChangeState.Deleted)
{
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = File.Exists(changeFullPath);
explore.Click += (_, ev) =>
var options = new FilePickerSaveOptions();
options.Title = App.Text("FileCM.SaveAsPatch");
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
Native.OS.OpenInFileManager(changeFullPath, true);
var saveTo = storageFile.Path.LocalPath;
await vm.SaveChangesAsPatchAsync(selected, saveTo);
}
e.Handled = true;
};
var menu = new ContextMenu();
if (selected.Count == 1)
{
var change = selected[0];
var changeFullPath = vm.GetAbsPath(change.Path);
var openWithMerger = new MenuItem();
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
openWithMerger.Click += (_, ev) =>
{
vm.OpenChangeWithExternalDiffTool(change);
ev.Handled = true;
};
menu.Items.Add(explore);
menu.Items.Add(openWithMerger);
if (change.Index != Models.ChangeState.Deleted)
{
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = File.Exists(changeFullPath);
explore.Click += (_, ev) =>
{
Native.OS.OpenInFileManager(changeFullPath, true);
ev.Handled = true;
};
menu.Items.Add(explore);
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(change.Path);
ev.Handled = true;
};
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(changeFullPath);
ev.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
}
else
{
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
var builder = new StringBuilder();
foreach (var c in selected)
builder.AppendLine(c.Path);
await App.CopyTextAsync(builder.ToString());
ev.Handled = true;
};
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
var builder = new StringBuilder();
foreach (var c in selected)
builder.AppendLine(vm.GetAbsPath(c.Path));
await App.CopyTextAsync(builder.ToString());
ev.Handled = true;
};
menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(change.Path);
ev.Handled = true;
};
menu.Items.Add(copyPath);
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(changeFullPath);
ev.Handled = true;
};
menu.Items.Add(copyFullPath);
menu.Open(view);
}
@@ -86,8 +154,8 @@ namespace SourceGit.Views
private async void OnSaveAsPatch(object sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null)
var storage = TopLevel.GetTopLevel(this)?.StorageProvider;
if (storage == null)
return;
if (DataContext is not ViewModels.RevisionCompare vm)
@@ -98,9 +166,9 @@ namespace SourceGit.Views
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
var storageFile = await storage.SaveFilePickerAsync(options);
if (storageFile != null)
vm.SaveAsPatch(storageFile.Path.LocalPath);
await vm.SaveChangesAsPatchAsync(null, storageFile.Path.LocalPath);
e.Handled = true;
}
@@ -110,17 +178,24 @@ namespace SourceGit.Views
if (DataContext is not ViewModels.RevisionCompare vm)
return;
if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges })
if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges })
return;
var change = selectedChanges[0];
if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C)
{
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
await App.CopyTextAsync(vm.GetAbsPath(change.Path));
var builder = new StringBuilder();
var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (selectedChanges.Count == 1)
{
builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path);
}
else
await App.CopyTextAsync(change.Path);
{
foreach (var c in selectedChanges)
builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path);
}
await App.CopyTextAsync(builder.ToString());
e.Handled = true;
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
@@ -84,7 +85,7 @@ namespace SourceGit.Views
var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
await vm.SaveStashAsPathAsync(stash, storageFile.Path.LocalPath);
await vm.SaveStashAsPatchAsync(stash, storageFile.Path.LocalPath);
ev.Handled = true;
};
@@ -123,70 +124,119 @@ namespace SourceGit.Views
private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e)
{
if (DataContext is ViewModels.StashesPage { SelectedChanges: { Count: 1 } selected } vm &&
if (DataContext is ViewModels.StashesPage { SelectedChanges: { Count: > 0 } selected } vm &&
sender is ChangeCollectionView view)
{
var change = selected[0];
var fullPath = vm.GetAbsPath(change.Path);
var openWithMerger = new MenuItem();
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
openWithMerger.Click += (_, ev) =>
if (selected.Count == 1)
{
vm.OpenChangeWithExternalDiffTool(change);
ev.Handled = true;
};
var change = selected[0];
var fullPath = vm.GetAbsPath(change.Path);
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = File.Exists(fullPath);
explore.Click += (_, ev) =>
var openWithMerger = new MenuItem();
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
openWithMerger.Click += (_, ev) =>
{
vm.OpenChangeWithExternalDiffTool(change);
ev.Handled = true;
};
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = File.Exists(fullPath);
explore.Click += (_, ev) =>
{
Native.OS.OpenInFileManager(fullPath, true);
ev.Handled = true;
};
var resetToThisRevision = new MenuItem();
resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision");
resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout");
resetToThisRevision.Click += async (_, ev) =>
{
await vm.CheckoutSingleFileAsync(change);
ev.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(change.Path);
ev.Handled = true;
};
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(fullPath);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(openWithMerger);
menu.Items.Add(explore);
menu.Items.Add(new MenuItem { Header = "-" });
menu.Items.Add(resetToThisRevision);
menu.Items.Add(new MenuItem { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
menu.Open(view);
}
else
{
Native.OS.OpenInFileManager(fullPath, true);
ev.Handled = true;
};
var resetToThisRevision = new MenuItem();
resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision");
resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout");
resetToThisRevision.Click += async (_, ev) =>
{
await vm.CheckoutMultipleFileAsync(selected);
ev.Handled = true;
};
var resetToThisRevision = new MenuItem();
resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision");
resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout");
resetToThisRevision.Click += async (_, ev) =>
{
await vm.CheckoutSingleFileAsync(change);
ev.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
var builder = new StringBuilder();
foreach (var c in selected)
builder.AppendLine(c.Path);
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
copyPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(change.Path);
ev.Handled = true;
};
await App.CopyTextAsync(builder.ToString());
ev.Handled = true;
};
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
await App.CopyTextAsync(fullPath);
ev.Handled = true;
};
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
copyFullPath.Click += async (_, ev) =>
{
var builder = new StringBuilder();
foreach (var c in selected)
builder.AppendLine(vm.GetAbsPath(c.Path));
var menu = new ContextMenu();
menu.Items.Add(openWithMerger);
menu.Items.Add(explore);
menu.Items.Add(new MenuItem { Header = "-" });
menu.Items.Add(resetToThisRevision);
menu.Items.Add(new MenuItem { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
menu.Open(view);
await App.CopyTextAsync(builder.ToString());
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(resetToThisRevision);
menu.Items.Add(new MenuItem { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
menu.Open(view);
}
}
e.Handled = true;
@@ -197,17 +247,24 @@ namespace SourceGit.Views
if (DataContext is not ViewModels.StashesPage vm)
return;
if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges })
if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges })
return;
var change = selectedChanges[0];
if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C)
{
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
await App.CopyTextAsync(vm.GetAbsPath(change.Path));
var builder = new StringBuilder();
var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (selectedChanges.Count == 1)
{
builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path);
}
else
await App.CopyTextAsync(change.Path);
{
foreach (var c in selectedChanges)
builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path);
}
await App.CopyTextAsync(builder.ToString());
e.Handled = true;
}
}

View File

@@ -126,7 +126,6 @@
<v:ChangeCollectionView Grid.Row="1"
x:Name="UnstagedChangesView"
IsUnstagedChange="True"
SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}"
ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=UnstagedChangeViewMode}"
EnableCompactFolders="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableCompactFoldersInChangesTree}"
@@ -181,7 +180,6 @@
<v:ChangeCollectionView Grid.Row="1"
x:Name="StagedChangesView"
IsUnstagedChange="False"
SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}"
ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=StagedChangeViewMode}"
EnableCompactFolders="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableCompactFoldersInChangesTree}"