diff --git a/src/App.axaml b/src/App.axaml
index 7129fd7e..fff2ca0d 100644
--- a/src/App.axaml
+++ b/src/App.axaml
@@ -19,6 +19,7 @@
+
diff --git a/src/Models/TreeDataGridSelectionModel.cs b/src/Models/TreeDataGridSelectionModel.cs
new file mode 100644
index 00000000..071d3414
--- /dev/null
+++ b/src/Models/TreeDataGridSelectionModel.cs
@@ -0,0 +1,421 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Models.TreeDataGrid;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Selection;
+using Avalonia.Input;
+
+namespace SourceGit.Models
+{
+ public class TreeDataGridSelectionModel : TreeSelectionModelBase,
+ ITreeDataGridRowSelectionModel,
+ ITreeDataGridSelectionInteraction
+ where TModel : class
+ {
+ private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity);
+
+ private readonly ITreeDataGridSource _source;
+ private EventHandler _viewSelectionChanged;
+ private EventHandler _rowDoubleTapped;
+ private Point _pressedPoint = s_InvalidPoint;
+ private bool _raiseViewSelectionChanged;
+ private Func> _childrenGetter;
+
+ public TreeDataGridSelectionModel(ITreeDataGridSource source, Func> childrenGetter)
+ : base(source.Items)
+ {
+ _source = source;
+ _childrenGetter = childrenGetter;
+
+ SelectionChanged += (s, e) =>
+ {
+ if (!IsSourceCollectionChanging)
+ _viewSelectionChanged?.Invoke(this, e);
+ else
+ _raiseViewSelectionChanged = true;
+ };
+ }
+
+ public void Select(IEnumerable items)
+ {
+ var sets = new HashSet();
+ foreach (var item in items)
+ sets.Add(item);
+
+ using (BatchUpdate())
+ {
+ Clear();
+
+ int num = _source.Rows.Count;
+ for (int i = 0; i < num; ++i)
+ {
+ var m = _source.Rows[i].Model as TModel;
+ if (m != null && sets.Contains(m))
+ {
+ var idx = _source.Rows.RowIndexToModelIndex(i);
+ Select(idx);
+ }
+ }
+ }
+ }
+
+ event EventHandler ITreeDataGridSelectionInteraction.SelectionChanged
+ {
+ add => _viewSelectionChanged += value;
+ remove => _viewSelectionChanged -= value;
+ }
+
+ public event EventHandler RowDoubleTapped
+ {
+ add => _rowDoubleTapped += value;
+ remove => _rowDoubleTapped -= value;
+ }
+
+ IEnumerable ITreeDataGridSelection.Source
+ {
+ get => Source;
+ set => Source = value;
+ }
+
+ bool ITreeDataGridSelectionInteraction.IsRowSelected(IRow rowModel)
+ {
+ if (rowModel is IModelIndexableRow indexable)
+ return IsSelected(indexable.ModelIndexPath);
+ return false;
+ }
+
+ bool ITreeDataGridSelectionInteraction.IsRowSelected(int rowIndex)
+ {
+ if (rowIndex >= 0 && rowIndex < _source.Rows.Count)
+ {
+ if (_source.Rows[rowIndex] is IModelIndexableRow indexable)
+ return IsSelected(indexable.ModelIndexPath);
+ }
+
+ return false;
+ }
+
+ void ITreeDataGridSelectionInteraction.OnKeyDown(TreeDataGrid sender, KeyEventArgs e)
+ {
+ if (sender.RowsPresenter is null)
+ return;
+
+ if (!e.Handled)
+ {
+ var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
+ if (e.Key == Key.A && ctrl && !SingleSelect)
+ {
+ using (BatchUpdate())
+ {
+ Clear();
+
+ int num = _source.Rows.Count;
+ for (int i = 0; i < num; ++i)
+ {
+ var m = _source.Rows.RowIndexToModelIndex(i);
+ Select(m);
+ }
+ }
+ e.Handled = true;
+ }
+
+ var direction = e.Key.ToNavigationDirection();
+ var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
+ if (direction.HasValue)
+ {
+ var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(AnchorIndex);
+ sender.RowsPresenter.BringIntoView(anchorRowIndex);
+
+ var anchor = sender.TryGetRow(anchorRowIndex);
+ if (anchor is not null && !ctrl)
+ {
+ e.Handled = TryKeyExpandCollapse(sender, direction.Value, anchor);
+ }
+
+ if (!e.Handled && (!ctrl || shift))
+ {
+ e.Handled = MoveSelection(sender, direction.Value, shift, anchor);
+ }
+
+ if (!e.Handled && direction == NavigationDirection.Left
+ && anchor?.Rows is HierarchicalRows hierarchicalRows && anchorRowIndex > 0)
+ {
+ var newIndex = hierarchicalRows.GetParentRowIndex(AnchorIndex);
+ UpdateSelection(sender, newIndex, true);
+ FocusRow(sender, sender.RowsPresenter.BringIntoView(newIndex));
+ }
+
+ if (!e.Handled && direction == NavigationDirection.Right
+ && anchor?.Rows is HierarchicalRows hierarchicalRows2 && hierarchicalRows2[anchorRowIndex].IsExpanded)
+ {
+ var newIndex = anchorRowIndex + 1;
+ UpdateSelection(sender, newIndex, true);
+ sender.RowsPresenter.BringIntoView(newIndex);
+ }
+ }
+ }
+ }
+
+ void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e)
+ {
+ if (!e.Handled &&
+ e.Pointer.Type == PointerType.Mouse &&
+ e.Source is Control source &&
+ sender.TryGetRow(source, out var row) &&
+ _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex)
+ {
+ if (!IsSelected(modelIndex))
+ {
+ PointerSelect(sender, row, e);
+ }
+ else
+ {
+ var point = e.GetCurrentPoint(sender);
+ if (point.Properties.IsRightButtonPressed)
+ return;
+
+ if (e.KeyModifiers == KeyModifiers.Control)
+ {
+ Deselect(modelIndex);
+ }
+ else if (e.ClickCount == 2)
+ {
+ _rowDoubleTapped?.Invoke(this, e);
+ }
+ else
+ {
+ using (BatchUpdate())
+ {
+ Clear();
+ Select(modelIndex);
+ }
+ }
+ }
+
+ _pressedPoint = s_InvalidPoint;
+ }
+ else
+ {
+ if (!sender.TryGetRow(e.Source as Control, out var test))
+ Clear();
+
+ _pressedPoint = e.GetPosition(sender);
+ }
+ }
+
+ void ITreeDataGridSelectionInteraction.OnPointerReleased(TreeDataGrid sender, PointerReleasedEventArgs e)
+ {
+ if (!e.Handled &&
+ _pressedPoint != s_InvalidPoint &&
+ e.Source is Control source &&
+ sender.TryGetRow(source, out var row) &&
+ _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex)
+ {
+ if (!IsSelected(modelIndex))
+ {
+ var p = e.GetPosition(sender);
+ if (Math.Abs(p.X - _pressedPoint.X) <= 3 || Math.Abs(p.Y - _pressedPoint.Y) <= 3)
+ PointerSelect(sender, row, e);
+ }
+ }
+ }
+
+ protected override void OnSourceCollectionChangeFinished()
+ {
+ if (_raiseViewSelectionChanged)
+ {
+ _viewSelectionChanged?.Invoke(this, EventArgs.Empty);
+ _raiseViewSelectionChanged = false;
+ }
+ }
+
+ private void PointerSelect(TreeDataGrid sender, TreeDataGridRow row, PointerEventArgs e)
+ {
+ var point = e.GetCurrentPoint(sender);
+
+ var commandModifiers = TopLevel.GetTopLevel(sender)?.PlatformSettings?.HotkeyConfiguration.CommandModifiers;
+ var toggleModifier = commandModifiers is not null && e.KeyModifiers.HasFlag(commandModifiers);
+ var isRightButton = point.Properties.PointerUpdateKind is PointerUpdateKind.RightButtonPressed or
+ PointerUpdateKind.RightButtonReleased;
+
+ UpdateSelection(
+ sender,
+ row.RowIndex,
+ select: true,
+ rangeModifier: e.KeyModifiers.HasFlag(KeyModifiers.Shift),
+ toggleModifier: toggleModifier,
+ rightButton: isRightButton);
+ e.Handled = true;
+ }
+
+ private void UpdateSelection(TreeDataGrid treeDataGrid, int rowIndex, bool select = true, bool rangeModifier = false, bool toggleModifier = false, bool rightButton = false)
+ {
+ var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex);
+ if (modelIndex == default)
+ return;
+
+ var mode = SingleSelect ? SelectionMode.Single : SelectionMode.Multiple;
+ var multi = (mode & SelectionMode.Multiple) != 0;
+ var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
+ var range = multi && rangeModifier;
+
+ if (!select)
+ {
+ if (IsSelected(modelIndex) && !treeDataGrid.QueryCancelSelection())
+ Deselect(modelIndex);
+ }
+ else if (rightButton)
+ {
+ if (IsSelected(modelIndex) == false && !treeDataGrid.QueryCancelSelection())
+ SelectedIndex = modelIndex;
+ }
+ else if (range)
+ {
+ if (!treeDataGrid.QueryCancelSelection())
+ {
+ var anchor = RangeAnchorIndex;
+ var i = Math.Max(_source.Rows.ModelIndexToRowIndex(anchor), 0);
+ var step = i < rowIndex ? 1 : -1;
+
+ using (BatchUpdate())
+ {
+ Clear();
+
+ while (true)
+ {
+ var m = _source.Rows.RowIndexToModelIndex(i);
+ Select(m);
+ anchor = m;
+ if (i == rowIndex)
+ break;
+ i += step;
+ }
+ }
+ }
+ }
+ else if (multi && toggle)
+ {
+ if (!treeDataGrid.QueryCancelSelection())
+ {
+ if (IsSelected(modelIndex) == true)
+ Deselect(modelIndex);
+ else
+ Select(modelIndex);
+ }
+ }
+ else if (toggle)
+ {
+ if (!treeDataGrid.QueryCancelSelection())
+ SelectedIndex = (SelectedIndex == modelIndex) ? -1 : modelIndex;
+ }
+ else if (SelectedIndex != modelIndex || Count > 1)
+ {
+ if (!treeDataGrid.QueryCancelSelection())
+ SelectedIndex = modelIndex;
+ }
+ }
+
+ private bool TryKeyExpandCollapse(TreeDataGrid treeDataGrid, NavigationDirection direction, TreeDataGridRow focused)
+ {
+ if (treeDataGrid.RowsPresenter is null || focused.RowIndex < 0)
+ return false;
+
+ var row = _source.Rows[focused.RowIndex];
+
+ if (row is IExpander expander)
+ {
+ if (direction == NavigationDirection.Right && !expander.IsExpanded)
+ {
+ expander.IsExpanded = true;
+ return true;
+ }
+ else if (direction == NavigationDirection.Left && expander.IsExpanded)
+ {
+ expander.IsExpanded = false;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool MoveSelection(TreeDataGrid treeDataGrid, NavigationDirection direction, bool rangeModifier, TreeDataGridRow focused)
+ {
+ if (treeDataGrid.RowsPresenter is null || _source.Columns.Count == 0 || _source.Rows.Count == 0)
+ return false;
+
+ var currentRowIndex = focused?.RowIndex ?? _source.Rows.ModelIndexToRowIndex(SelectedIndex);
+ int newRowIndex;
+
+ if (direction == NavigationDirection.First || direction == NavigationDirection.Last)
+ {
+ newRowIndex = direction == NavigationDirection.First ? 0 : _source.Rows.Count - 1;
+ }
+ else
+ {
+ (var x, var y) = direction switch
+ {
+ NavigationDirection.Up => (0, -1),
+ NavigationDirection.Down => (0, 1),
+ NavigationDirection.Left => (-1, 0),
+ NavigationDirection.Right => (1, 0),
+ _ => (0, 0)
+ };
+
+ newRowIndex = Math.Max(0, Math.Min(currentRowIndex + y, _source.Rows.Count - 1));
+ }
+
+ if (newRowIndex != currentRowIndex)
+ UpdateSelection(treeDataGrid, newRowIndex, true, rangeModifier);
+
+ if (newRowIndex != currentRowIndex)
+ {
+ treeDataGrid.RowsPresenter?.BringIntoView(newRowIndex);
+ FocusRow(treeDataGrid, treeDataGrid.TryGetRow(newRowIndex));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private static void FocusRow(TreeDataGrid owner, Control control)
+ {
+ if (!owner.TryGetRow(control, out var row) || row.CellsPresenter is null)
+ return;
+
+ // Get the column index of the currently focused cell if possible: we'll try to focus the
+ // same column in the new row.
+ if (TopLevel.GetTopLevel(owner)?.FocusManager is { } focusManager &&
+ focusManager.GetFocusedElement() is Control currentFocus &&
+ owner.TryGetCell(currentFocus, out var currentCell) &&
+ row.TryGetCell(currentCell.ColumnIndex) is { } newCell &&
+ newCell.Focusable)
+ {
+ newCell.Focus();
+ }
+ else
+ {
+ // Otherwise, just focus the first focusable cell in the row.
+ foreach (var cell in row.CellsPresenter.GetRealizedElements())
+ {
+ if (cell.Focusable)
+ {
+ cell.Focus();
+ break;
+ }
+ }
+ }
+ }
+
+ protected override IEnumerable GetChildren(TModel node)
+ {
+ return _childrenGetter?.Invoke(node);
+ }
+ }
+}
diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml
index bc2bc9c0..ea98a86b 100644
--- a/src/Resources/Locales/en_US.axaml
+++ b/src/Resources/Locales/en_US.axaml
@@ -330,7 +330,7 @@
Remote :
Push Changes To Remote
Remote Branch :
- Tracking remote branch(--set-upstream)
+ Tracking remote branch
Push all tags
Push Tag To Remote
Push to all remotes
diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml
index 3dffd574..adb90a98 100644
--- a/src/Resources/Locales/zh_CN.axaml
+++ b/src/Resources/Locales/zh_CN.axaml
@@ -330,7 +330,7 @@
远程仓库 :
推送到远程仓库
远程分支 :
- 跟踪远程分支(--set-upstream)
+ 跟踪远程分支
同时推送标签
推送标签到远程仓库
推送到所有远程仓库
diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml
index 376f9424..f7eb7054 100644
--- a/src/Resources/Styles.axaml
+++ b/src/Resources/Styles.axaml
@@ -1075,4 +1075,58 @@
+
+
+
+
+
+
+
+
diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj
index 657b49f3..a3d38391 100644
--- a/src/SourceGit.csproj
+++ b/src/SourceGit.csproj
@@ -33,6 +33,7 @@
+
diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs
index 6fdcd93c..9fd320fd 100644
--- a/src/ViewModels/CommitDetail.cs
+++ b/src/ViewModels/CommitDetail.cs
@@ -4,6 +4,8 @@ using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
+using Avalonia.Controls.Models.TreeDataGrid;
+using Avalonia.Interactivity;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
@@ -48,48 +50,17 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _visibleChanges, value);
}
- public List ChangeTree
+ public List SelectedChanges
{
- get => _changeTree;
- set => SetProperty(ref _changeTree, value);
- }
-
- public Models.Change SelectedChange
- {
- get => _selectedChange;
+ get => _selectedChanges;
set
{
- if (SetProperty(ref _selectedChange, value))
+ if (SetProperty(ref _selectedChanges, value))
{
- if (value == null)
- {
- SelectedChangeNode = null;
+ if (value == null || value.Count != 1)
DiffContext = null;
- }
else
- {
- SelectedChangeNode = FileTreeNode.SelectByPath(_changeTree, value.Path);
- DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value), _diffContext);
- }
- }
- }
- }
-
- public FileTreeNode SelectedChangeNode
- {
- get => _selectedChangeNode;
- set
- {
- if (SetProperty(ref _selectedChangeNode, value))
- {
- if (value == null)
- {
- SelectedChange = null;
- }
- else
- {
- SelectedChange = value.Backend as Models.Change;
- }
+ DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value[0]), _diffContext);
}
}
}
@@ -106,26 +77,10 @@ namespace SourceGit.ViewModels
}
}
- public List RevisionFilesTree
+ public HierarchicalTreeDataGridSource RevisionFiles
{
- get => _revisionFilesTree;
- set => SetProperty(ref _revisionFilesTree, value);
- }
-
- public FileTreeNode SelectedRevisionFileNode
- {
- get => _selectedRevisionFileNode;
- set
- {
- if (SetProperty(ref _selectedRevisionFileNode, value) && value != null && !value.IsFolder)
- {
- RefreshViewRevisionFile(value.Backend as Models.Object);
- }
- else
- {
- ViewRevisionFileContent = null;
- }
- }
+ get => _revisionFiles;
+ private set => SetProperty(ref _revisionFiles, value);
}
public string SearchFileFilter
@@ -159,17 +114,14 @@ namespace SourceGit.ViewModels
_changes.Clear();
if (_visibleChanges != null)
_visibleChanges.Clear();
- if (_changeTree != null)
- _changeTree.Clear();
- _selectedChange = null;
- _selectedChangeNode = null;
+ if (_selectedChanges != null)
+ _selectedChanges.Clear();
_searchChangeFilter = null;
_diffContext = null;
+ if (_revisionFilesBackup != null)
+ _revisionFilesBackup.Clear();
if (_revisionFiles != null)
- _revisionFiles.Clear();
- if (_revisionFilesTree != null)
- _revisionFilesTree.Clear();
- _selectedRevisionFileNode = null;
+ _revisionFiles.Dispose();
_searchFileFilter = null;
_viewRevisionFileContent = null;
_cancelToken = null;
@@ -346,9 +298,14 @@ namespace SourceGit.ViewModels
{
_changes = null;
VisibleChanges = null;
- SelectedChange = null;
- RevisionFilesTree = null;
- SelectedRevisionFileNode = null;
+ SelectedChanges = null;
+
+ if (_revisionFiles != null)
+ {
+ _revisionFiles.Dispose();
+ _revisionFiles = null;
+ }
+
if (_commit == null)
return;
if (_cancelToken != null)
@@ -379,40 +336,34 @@ namespace SourceGit.ViewModels
}
}
- var tree = FileTreeNode.Build(visible);
+ var tree = FileTreeNode.Build(visible, true);
Dispatcher.UIThread.Invoke(() =>
{
Changes = changes;
VisibleChanges = visible;
- ChangeTree = tree;
});
});
Task.Run(() =>
{
- var files = cmdRevisionFiles.Result();
+ _revisionFilesBackup = cmdRevisionFiles.Result();
if (cmdRevisionFiles.Cancel.Requested)
return;
- var visible = files;
- if (!string.IsNullOrWhiteSpace(_searchFileFilter))
+ var visible = _revisionFilesBackup;
+ var isSearching = !string.IsNullOrWhiteSpace(_searchFileFilter);
+ if (isSearching)
{
visible = new List();
- foreach (var f in files)
+ foreach (var f in _revisionFilesBackup)
{
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase))
- {
visible.Add(f);
- }
}
}
- var tree = FileTreeNode.Build(visible);
- Dispatcher.UIThread.Invoke(() =>
- {
- _revisionFiles = files;
- RevisionFilesTree = tree;
- });
+ var tree = FileTreeNode.Build(visible, isSearching || visible.Count <= 100);
+ Dispatcher.UIThread.Invoke(() => BuildRevisionFilesSource(tree));
});
}
@@ -431,15 +382,11 @@ namespace SourceGit.ViewModels
foreach (var c in _changes)
{
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase))
- {
visible.Add(c);
- }
}
VisibleChanges = visible;
}
-
- ChangeTree = FileTreeNode.Build(_visibleChanges);
}
private void RefreshVisibleFiles()
@@ -447,24 +394,29 @@ namespace SourceGit.ViewModels
if (_revisionFiles == null)
return;
- var visible = _revisionFiles;
- if (!string.IsNullOrWhiteSpace(_searchFileFilter))
+ var visible = _revisionFilesBackup;
+ var isSearching = !string.IsNullOrWhiteSpace(_searchFileFilter);
+ if (isSearching)
{
visible = new List();
- foreach (var f in _revisionFiles)
+ foreach (var f in _revisionFilesBackup)
{
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase))
- {
visible.Add(f);
- }
}
}
- RevisionFilesTree = FileTreeNode.Build(visible);
+ BuildRevisionFilesSource(FileTreeNode.Build(visible, isSearching || visible.Count < 100));
}
private void RefreshViewRevisionFile(Models.Object file)
{
+ if (file == null)
+ {
+ ViewRevisionFileContent = null;
+ return;
+ }
+
switch (file.Type)
{
case Models.ObjectType.Blob:
@@ -541,6 +493,35 @@ namespace SourceGit.ViewModels
}
}
+ private void BuildRevisionFilesSource(List tree)
+ {
+ var source = new HierarchicalTreeDataGridSource(tree)
+ {
+ Columns =
+ {
+ new HierarchicalExpanderColumn(
+ new TemplateColumn("Icon", "FileTreeNodeExpanderTemplate", null, GridLength.Auto),
+ x => x.Children,
+ x => x.Children.Count > 0,
+ x => x.IsExpanded),
+ new TextColumn(
+ null,
+ x => string.Empty,
+ GridLength.Star)
+ }
+ };
+
+ source.Selection = new Models.TreeDataGridSelectionModel(source, x => x.Children);
+ source.RowSelection.SingleSelect = true;
+ source.RowSelection.SelectionChanged += (s, _) =>
+ {
+ if (s is Models.TreeDataGridSelectionModel selection)
+ RefreshViewRevisionFile(selection.SelectedItem?.Backend as Models.Object);
+ };
+
+ RevisionFiles = source;
+ }
+
private static readonly HashSet IMG_EXTS = new HashSet()
{
".ico", ".bmp", ".jpg", ".png", ".jpeg"
@@ -551,14 +532,11 @@ namespace SourceGit.ViewModels
private Models.Commit _commit = null;
private List _changes = null;
private List _visibleChanges = null;
- private List _changeTree = null;
- private Models.Change _selectedChange = null;
- private FileTreeNode _selectedChangeNode = null;
+ private List _selectedChanges = null;
private string _searchChangeFilter = string.Empty;
private DiffContext _diffContext = null;
- private List _revisionFiles = null;
- private List _revisionFilesTree = null;
- private FileTreeNode _selectedRevisionFileNode = null;
+ private List _revisionFilesBackup = null;
+ private HierarchicalTreeDataGridSource _revisionFiles = null;
private string _searchFileFilter = string.Empty;
private object _viewRevisionFileContent = null;
private Commands.Command.CancelToken _cancelToken = null;
diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs
index 3aad22ea..d05f0c57 100644
--- a/src/ViewModels/DiffContext.cs
+++ b/src/ViewModels/DiffContext.cs
@@ -85,6 +85,11 @@ namespace SourceGit.ViewModels
_content = previous._content;
}
+ if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null")
+ _title = _option.Path;
+ else
+ _title = $"{_option.OrgPath} → {_option.Path}";
+
LoadDiffContent();
}
@@ -175,11 +180,6 @@ namespace SourceGit.ViewModels
Dispatcher.UIThread.Post(() =>
{
- if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null")
- Title = _option.Path;
- else
- Title = $"{_option.OrgPath} → {_option.Path}";
-
FileModeChange = latest.FileModeChange;
Content = rs;
IsTextDiff = latest.TextDiff != null;
diff --git a/src/ViewModels/FileTreeNode.cs b/src/ViewModels/FileTreeNode.cs
index ca6d850f..8e3ba076 100644
--- a/src/ViewModels/FileTreeNode.cs
+++ b/src/ViewModels/FileTreeNode.cs
@@ -18,11 +18,10 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _isExpanded, value);
}
- public static List Build(List changes)
+ public static List Build(List changes, bool expanded)
{
var nodes = new List();
var folders = new Dictionary();
- var expanded = changes.Count <= 50;
foreach (var c in changes)
{
@@ -94,11 +93,10 @@ namespace SourceGit.ViewModels
return nodes;
}
- public static List Build(List files)
+ public static List Build(List files, bool expanded)
{
var nodes = new List();
var folders = new Dictionary();
- var expanded = files.Count <= 50;
foreach (var f in files)
{
diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs
index a2fd25ca..536d71bc 100644
--- a/src/ViewModels/RevisionCompare.cs
+++ b/src/ViewModels/RevisionCompare.cs
@@ -35,48 +35,17 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _visibleChanges, value);
}
- public List ChangeTree
+ public List SelectedChanges
{
- get => _changeTree;
- private set => SetProperty(ref _changeTree, value);
- }
-
- public Models.Change SelectedChange
- {
- get => _selectedChange;
+ get => _selectedChanges;
set
{
- if (SetProperty(ref _selectedChange, value))
+ if (SetProperty(ref _selectedChanges, value))
{
- if (value == null)
- {
- SelectedNode = null;
+ if (value != null && value.Count == 1)
+ DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, _endPoint, value[0]), _diffContext);
+ else
DiffContext = null;
- }
- else
- {
- SelectedNode = FileTreeNode.SelectByPath(_changeTree, value.Path);
- DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, _endPoint, value), _diffContext);
- }
- }
- }
- }
-
- public FileTreeNode SelectedNode
- {
- get => _selectedNode;
- set
- {
- if (SetProperty(ref _selectedNode, value))
- {
- if (value == null)
- {
- SelectedChange = null;
- }
- else
- {
- SelectedChange = value.Backend as Models.Change;
- }
}
}
}
@@ -126,17 +95,14 @@ namespace SourceGit.ViewModels
foreach (var c in _changes)
{
if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))
- {
visible.Add(c);
- }
}
}
- var tree = FileTreeNode.Build(visible);
+ var tree = FileTreeNode.Build(visible, true);
Dispatcher.UIThread.Invoke(() =>
{
VisibleChanges = visible;
- ChangeTree = tree;
});
});
}
@@ -148,10 +114,8 @@ namespace SourceGit.ViewModels
_changes.Clear();
if (_visibleChanges != null)
_visibleChanges.Clear();
- if (_changeTree != null)
- _changeTree.Clear();
- _selectedChange = null;
- _selectedNode = null;
+ if (_selectedChanges != null)
+ _selectedChanges.Clear();
_searchFilter = null;
_diffContext = null;
}
@@ -168,8 +132,12 @@ namespace SourceGit.ViewModels
SearchFilter = string.Empty;
}
- public ContextMenu CreateChangeContextMenu(Models.Change change)
+ public ContextMenu CreateChangeContextMenu()
{
+ if (_selectedChanges == null || _selectedChanges.Count != 1)
+ return null;
+
+ var change = _selectedChanges[0];
var menu = new ContextMenu();
var diffWithMerger = new MenuItem();
@@ -237,24 +205,18 @@ namespace SourceGit.ViewModels
foreach (var c in _changes)
{
if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))
- {
visible.Add(c);
- }
}
VisibleChanges = visible;
}
-
- ChangeTree = FileTreeNode.Build(_visibleChanges);
}
private string _repo = string.Empty;
private string _endPoint = string.Empty;
private List _changes = null;
private List _visibleChanges = null;
- private List _changeTree = null;
- private Models.Change _selectedChange = null;
- private FileTreeNode _selectedNode = null;
+ private List _selectedChanges = null;
private string _searchFilter = string.Empty;
private DiffContext _diffContext = null;
}
diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs
index 76c3be50..e8897d48 100644
--- a/src/ViewModels/WorkingCopy.cs
+++ b/src/ViewModels/WorkingCopy.cs
@@ -58,7 +58,23 @@ namespace SourceGit.ViewModels
public bool UseAmend
{
get => _useAmend;
- set => SetProperty(ref _useAmend, value);
+ set
+ {
+ if (SetProperty(ref _useAmend, value) && value)
+ {
+ var commits = new Commands.QueryCommits(_repo.FullPath, "-n 1", false).Result();
+ if (commits.Count == 0)
+ {
+ App.RaiseException(_repo.FullPath, "No commits to amend!!!");
+ _useAmend = false;
+ OnPropertyChanged();
+ }
+ else
+ {
+ CommitMessage = commits[0].FullMessage;
+ }
+ }
+ }
}
public List Unstaged
@@ -73,103 +89,58 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _staged, value);
}
- public int Count
+ public List SelectedUnstaged
{
- get => _count;
- }
-
- public Models.Change SelectedUnstagedChange
- {
- get => _selectedUnstagedChange;
+ get => _selectedUnstaged;
set
{
- if (SetProperty(ref _selectedUnstagedChange, value) && value != null)
+ if (SetProperty(ref _selectedUnstaged, value))
{
- SelectedStagedChange = null;
- SelectedStagedTreeNode = null;
- SetDetail(value, true);
- }
- }
- }
-
- public Models.Change SelectedStagedChange
- {
- get => _selectedStagedChange;
- set
- {
- if (SetProperty(ref _selectedStagedChange, value) && value != null)
- {
- SelectedUnstagedChange = null;
- SelectedUnstagedTreeNode = null;
- SetDetail(value, false);
- }
- }
- }
-
- public List UnstagedTree
- {
- get => _unstagedTree;
- private set => SetProperty(ref _unstagedTree, value);
- }
-
- public List StagedTree
- {
- get => _stagedTree;
- private set => SetProperty(ref _stagedTree, value);
- }
-
- public FileTreeNode SelectedUnstagedTreeNode
- {
- get => _selectedUnstagedTreeNode;
- set
- {
- if (SetProperty(ref _selectedUnstagedTreeNode, value))
- {
- if (value == null)
+ if (value == null || value.Count == 0)
{
- SelectedUnstagedChange = null;
+ if (_selectedStaged == null || _selectedStaged.Count == 0)
+ SetDetail(null);
}
else
{
- SelectedUnstagedChange = value.Backend as Models.Change;
- SelectedStagedTreeNode = null;
- SelectedStagedChange = null;
+ SelectedStaged = null;
- if (value.IsFolder)
- {
- SetDetail(null, true);
- }
+ if (value.Count == 1)
+ SetDetail(value[0]);
+ else
+ SetDetail(null);
}
}
}
}
- public FileTreeNode SelectedStagedTreeNode
+ public List SelectedStaged
{
- get => _selectedStagedTreeNode;
+ get => _selectedStaged;
set
{
- if (SetProperty(ref _selectedStagedTreeNode, value))
+ if (SetProperty(ref _selectedStaged, value))
{
- if (value == null)
+ if (value == null || value.Count == 0)
{
- SelectedStagedChange = null;
+ if (_selectedUnstaged == null || _selectedUnstaged.Count == 0)
+ SetDetail(null);
}
else
{
- SelectedStagedChange = value.Backend as Models.Change;
- SelectedUnstagedTreeNode = null;
- SelectedUnstagedChange = null;
+ SelectedUnstaged = null;
- if (value.IsFolder)
- {
- SetDetail(null, false);
- }
+ if (value.Count == 1)
+ SetDetail(value[0]);
+ else
+ SetDetail(null);
}
}
}
}
+ public int Count => _count;
+
public object DetailContext
{
get => _detailContext;
@@ -194,14 +165,10 @@ namespace SourceGit.ViewModels
_unstaged.Clear();
if (_staged != null)
_staged.Clear();
- if (_unstagedTree != null)
- _unstagedTree.Clear();
- if (_stagedTree != null)
- _stagedTree.Clear();
- _selectedUnstagedChange = null;
- _selectedStagedChange = null;
- _selectedUnstagedTreeNode = null;
- _selectedStagedTreeNode = null;
+ if (_selectedUnstaged != null)
+ _selectedUnstaged.Clear();
+ if (_selectedStaged != null)
+ _selectedStaged.Clear();
_detailContext = null;
_commitMessage = string.Empty;
}
@@ -210,20 +177,22 @@ namespace SourceGit.ViewModels
{
var unstaged = new List();
var staged = new List();
+ var selectedUnstaged = new List();
+ var selectedStaged = new List();
- var viewFile = string.Empty;
- var lastSelectedIsUnstaged = false;
- if (_selectedUnstagedChange != null)
+ var lastSelectedUnstaged = new HashSet();
+ var lastSelectedStaged = new HashSet();
+ if (_selectedUnstaged != null)
{
- viewFile = _selectedUnstagedChange.Path;
- lastSelectedIsUnstaged = true;
+ foreach (var c in _selectedUnstaged)
+ lastSelectedUnstaged.Add(c.Path);
}
- else if (_selectedStagedChange != null)
+ else if (_selectedStaged != null)
{
- viewFile = _selectedStagedChange.Path;
+ foreach (var c in _selectedStaged)
+ lastSelectedStaged.Add(c.Path);
}
- var viewChange = null as Models.Change;
var hasConflict = false;
foreach (var c in changes)
{
@@ -233,65 +202,43 @@ namespace SourceGit.ViewModels
|| c.Index == Models.ChangeState.Renamed)
{
staged.Add(c);
- if (!lastSelectedIsUnstaged && c.Path == viewFile)
- {
- viewChange = c;
- }
+
+ if (lastSelectedStaged.Contains(c.Path))
+ selectedStaged.Add(c);
}
if (c.WorkTree != Models.ChangeState.None)
{
unstaged.Add(c);
hasConflict |= c.IsConflit;
- if (lastSelectedIsUnstaged && c.Path == viewFile)
- {
- viewChange = c;
- }
+
+ if (lastSelectedUnstaged.Contains(c.Path))
+ selectedUnstaged.Add(c);
}
}
_count = changes.Count;
- var unstagedTree = FileTreeNode.Build(unstaged);
- var stagedTree = FileTreeNode.Build(staged);
Dispatcher.UIThread.Invoke(() =>
{
_isLoadingData = true;
Unstaged = unstaged;
Staged = staged;
- UnstagedTree = unstagedTree;
- StagedTree = stagedTree;
_isLoadingData = false;
- // Restore last selection states.
- if (viewChange != null)
- {
- var scrollOffset = Vector.Zero;
- if (_detailContext is DiffContext old)
- scrollOffset = old.SyncScrollOffset;
+ var scrollOffset = Vector.Zero;
+ if (_detailContext is DiffContext old)
+ scrollOffset = old.SyncScrollOffset;
- if (lastSelectedIsUnstaged)
- {
- SelectedUnstagedChange = viewChange;
- SelectedUnstagedTreeNode = FileTreeNode.SelectByPath(_unstagedTree, viewFile);
- }
- else
- {
- SelectedStagedChange = viewChange;
- SelectedStagedTreeNode = FileTreeNode.SelectByPath(_stagedTree, viewFile);
- }
-
- if (_detailContext is DiffContext cur)
- cur.SyncScrollOffset = scrollOffset;
- }
+ if (selectedUnstaged.Count > 0)
+ SelectedUnstaged = selectedUnstaged;
+ else if (selectedStaged.Count > 0)
+ SelectedStaged = selectedStaged;
else
- {
- SelectedUnstagedChange = null;
- SelectedUnstagedTreeNode = null;
- SelectedStagedChange = null;
- SelectedStagedTreeNode = null;
- SetDetail(null, false);
- }
+ SetDetail(null);
+
+ if (_detailContext is DiffContext cur)
+ cur.SyncScrollOffset = scrollOffset;
// Try to load merge message from MERGE_MSG
if (string.IsNullOrEmpty(_commitMessage))
@@ -305,30 +252,24 @@ namespace SourceGit.ViewModels
return hasConflict;
}
- public void SetDetail(Models.Change change, bool isUnstaged)
+ public void OpenAssumeUnchanged()
{
- if (_isLoadingData)
- return;
+ var dialog = new Views.AssumeUnchangedManager()
+ {
+ DataContext = new AssumeUnchangedManager(_repo.FullPath)
+ };
- if (change == null)
- {
- DetailContext = null;
- }
- else if (change.IsConflit && isUnstaged)
- {
- DetailContext = new ConflictContext(_repo.FullPath, change);
- }
- else
- {
- if (_detailContext is DiffContext previous)
- {
- DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), previous);
- }
- else
- {
- DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged));
- }
- }
+ dialog.ShowDialog(App.GetTopLevel() as Window);
+ }
+
+ public void StageSelected()
+ {
+ StageChanges(_selectedUnstaged);
+ }
+
+ public void StageAll()
+ {
+ StageChanges(_unstaged);
}
public async void StageChanges(List changes)
@@ -336,7 +277,7 @@ namespace SourceGit.ViewModels
if (_unstaged.Count == 0 || changes.Count == 0)
return;
- SetDetail(null, true);
+ SetDetail(null);
IsStaging = true;
_repo.SetWatcherEnabled(false);
if (changes.Count == _unstaged.Count)
@@ -357,12 +298,22 @@ namespace SourceGit.ViewModels
IsStaging = false;
}
+ public void UnstageSelected()
+ {
+ UnstageChanges(_selectedStaged);
+ }
+
+ public void UnstageAll()
+ {
+ UnstageChanges(_staged);
+ }
+
public async void UnstageChanges(List changes)
{
if (_staged.Count == 0 || changes.Count == 0)
return;
- SetDetail(null, false);
+ SetDetail(null);
IsUnstaging = true;
_repo.SetWatcherEnabled(false);
if (changes.Count == _staged.Count)
@@ -412,113 +363,25 @@ namespace SourceGit.ViewModels
}
}
- public async void UseTheirs(List changes)
+ public void Commit()
{
- var files = new List();
- foreach (var change in changes)
- {
- if (change.IsConflit)
- files.Add(change.Path);
- }
-
- _repo.SetWatcherEnabled(false);
- var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseTheirs(files));
- if (succ)
- {
- await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec());
- }
- _repo.MarkWorkingCopyDirtyManually();
- _repo.SetWatcherEnabled(true);
+ DoCommit(false);
}
- public async void UseMine(List changes)
+ public void CommitWithPush()
{
- var files = new List();
- foreach (var change in changes)
- {
- if (change.IsConflit)
- files.Add(change.Path);
- }
-
- _repo.SetWatcherEnabled(false);
- var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseMine(files));
- if (succ)
- {
- await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec());
- }
- _repo.MarkWorkingCopyDirtyManually();
- _repo.SetWatcherEnabled(true);
+ DoCommit(true);
}
- public async void UseExternalMergeTool(Models.Change change)
+ public ContextMenu CreateContextMenuForUnstagedChanges()
{
- var type = Preference.Instance.ExternalMergeToolType;
- var exec = Preference.Instance.ExternalMergeToolPath;
-
- var tool = Models.ExternalMerger.Supported.Find(x => x.Type == type);
- if (tool == null)
- {
- App.RaiseException(_repo.FullPath, "Invalid merge tool in preference setting!");
- return;
- }
-
- var args = tool.Type != 0 ? tool.Cmd : Preference.Instance.ExternalMergeToolCmd;
-
- _repo.SetWatcherEnabled(false);
- await Task.Run(() => Commands.MergeTool.OpenForMerge(_repo.FullPath, exec, args, change.Path));
- _repo.SetWatcherEnabled(true);
- }
-
- public async void DoCommit(bool autoPush)
- {
- if (!PopupHost.CanCreatePopup())
- {
- App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!");
- return;
- }
-
- if (_staged.Count == 0)
- {
- App.RaiseException(_repo.FullPath, "No files added to commit!");
- return;
- }
-
- if (string.IsNullOrWhiteSpace(_commitMessage))
- {
- App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!");
- return;
- }
-
- PushCommitMessage();
-
- SetDetail(null, false);
- IsCommitting = true;
- _repo.SetWatcherEnabled(false);
- var succ = await Task.Run(() => new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec());
- if (succ)
- {
- CommitMessage = string.Empty;
- UseAmend = false;
-
- if (autoPush)
- {
- PopupHost.ShowAndStartPopup(new Push(_repo, null));
- }
- }
- _repo.MarkWorkingCopyDirtyManually();
- _repo.SetWatcherEnabled(true);
- IsCommitting = false;
- }
-
- public ContextMenu CreateContextMenuForUnstagedChanges(List changes)
- {
- if (changes.Count == 0)
+ if (_selectedUnstaged.Count == 0)
return null;
var menu = new ContextMenu();
- if (changes.Count == 1)
+ if (_selectedUnstaged.Count == 1)
{
- var change = changes[0];
+ var change = _selectedUnstaged[0];
var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path));
var explore = new MenuItem();
@@ -551,7 +414,7 @@ namespace SourceGit.ViewModels
useTheirs.Header = App.Text("FileCM.UseTheirs");
useTheirs.Click += (_, e) =>
{
- UseTheirs(changes);
+ UseTheirs(_selectedUnstaged);
e.Handled = true;
};
@@ -560,7 +423,7 @@ namespace SourceGit.ViewModels
useMine.Header = App.Text("FileCM.UseMine");
useMine.Click += (_, e) =>
{
- UseMine(changes);
+ UseMine(_selectedUnstaged);
e.Handled = true;
};
@@ -585,7 +448,7 @@ namespace SourceGit.ViewModels
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) =>
{
- StageChanges(changes);
+ StageChanges(_selectedUnstaged);
e.Handled = true;
};
@@ -594,7 +457,7 @@ namespace SourceGit.ViewModels
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) =>
{
- Discard(changes, true);
+ Discard(_selectedUnstaged, true);
e.Handled = true;
};
@@ -605,7 +468,7 @@ namespace SourceGit.ViewModels
{
if (PopupHost.CanCreatePopup())
{
- PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
+ PopupHost.ShowPopup(new StashChanges(_repo, _selectedUnstaged, false));
}
e.Handled = true;
};
@@ -627,7 +490,7 @@ namespace SourceGit.ViewModels
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
- var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath));
+ var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@@ -679,7 +542,7 @@ namespace SourceGit.ViewModels
{
var hasConflicts = false;
var hasNoneConflicts = false;
- foreach (var change in changes)
+ foreach (var change in _selectedUnstaged)
{
if (change.IsConflit)
{
@@ -704,7 +567,7 @@ namespace SourceGit.ViewModels
useTheirs.Header = App.Text("FileCM.UseTheirs");
useTheirs.Click += (_, e) =>
{
- UseTheirs(changes);
+ UseTheirs(_selectedUnstaged);
e.Handled = true;
};
@@ -713,7 +576,7 @@ namespace SourceGit.ViewModels
useMine.Header = App.Text("FileCM.UseMine");
useMine.Click += (_, e) =>
{
- UseMine(changes);
+ UseMine(_selectedUnstaged);
e.Handled = true;
};
@@ -723,31 +586,31 @@ namespace SourceGit.ViewModels
}
var stage = new MenuItem();
- stage.Header = App.Text("FileCM.StageMulti", changes.Count);
+ stage.Header = App.Text("FileCM.StageMulti", _selectedUnstaged.Count);
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) =>
{
- StageChanges(changes);
+ StageChanges(_selectedUnstaged);
e.Handled = true;
};
var discard = new MenuItem();
- discard.Header = App.Text("FileCM.DiscardMulti", changes.Count);
+ discard.Header = App.Text("FileCM.DiscardMulti", _selectedUnstaged.Count);
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) =>
{
- Discard(changes, true);
+ Discard(_selectedUnstaged, true);
e.Handled = true;
};
var stash = new MenuItem();
- stash.Header = App.Text("FileCM.StashMulti", changes.Count);
+ stash.Header = App.Text("FileCM.StashMulti", _selectedUnstaged.Count);
stash.Icon = App.CreateMenuIcon("Icons.Stashes");
stash.Click += (_, e) =>
{
if (PopupHost.CanCreatePopup())
{
- PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
+ PopupHost.ShowPopup(new StashChanges(_repo, _selectedUnstaged, false));
}
e.Handled = true;
};
@@ -769,7 +632,7 @@ namespace SourceGit.ViewModels
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
- var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath));
+ var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@@ -786,15 +649,15 @@ namespace SourceGit.ViewModels
return menu;
}
- public ContextMenu CreateContextMenuForStagedChanges(List changes)
+ public ContextMenu CreateContextMenuForStagedChanges()
{
- if (changes.Count == 0)
+ if (_selectedStaged.Count == 0)
return null;
var menu = new ContextMenu();
- if (changes.Count == 1)
+ if (_selectedStaged.Count == 1)
{
- var change = changes[0];
+ var change = _selectedStaged[0];
var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path));
var explore = new MenuItem();
@@ -822,7 +685,7 @@ namespace SourceGit.ViewModels
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (o, e) =>
{
- UnstageChanges(changes);
+ UnstageChanges(_selectedStaged);
e.Handled = true;
};
@@ -831,7 +694,7 @@ namespace SourceGit.ViewModels
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) =>
{
- Discard(changes, false);
+ Discard(_selectedStaged, false);
e.Handled = true;
};
@@ -842,7 +705,7 @@ namespace SourceGit.ViewModels
{
if (PopupHost.CanCreatePopup())
{
- PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
+ PopupHost.ShowPopup(new StashChanges(_repo, _selectedStaged, false));
}
e.Handled = true;
};
@@ -864,7 +727,7 @@ namespace SourceGit.ViewModels
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
- var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath));
+ var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@@ -894,31 +757,31 @@ namespace SourceGit.ViewModels
else
{
var unstage = new MenuItem();
- unstage.Header = App.Text("FileCM.UnstageMulti", changes.Count);
+ unstage.Header = App.Text("FileCM.UnstageMulti", _selectedStaged.Count);
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (o, e) =>
{
- UnstageChanges(changes);
+ UnstageChanges(_selectedStaged);
e.Handled = true;
};
var discard = new MenuItem();
- discard.Header = App.Text("FileCM.DiscardMulti", changes.Count);
+ discard.Header = App.Text("FileCM.DiscardMulti", _selectedStaged.Count);
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) =>
{
- Discard(changes, false);
+ Discard(_selectedStaged, false);
e.Handled = true;
};
var stash = new MenuItem();
- stash.Header = App.Text("FileCM.StashMulti", changes.Count);
+ stash.Header = App.Text("FileCM.StashMulti", _selectedStaged.Count);
stash.Icon = App.CreateMenuIcon("Icons.Stashes");
stash.Click += (_, e) =>
{
if (PopupHost.CanCreatePopup())
{
- PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
+ PopupHost.ShowPopup(new StashChanges(_repo, _selectedStaged, false));
}
e.Handled = true;
};
@@ -940,7 +803,7 @@ namespace SourceGit.ViewModels
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
- var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath));
+ var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@@ -993,6 +856,139 @@ namespace SourceGit.ViewModels
return menu;
}
+ private void SetDetail(Models.Change change)
+ {
+ if (_isLoadingData)
+ return;
+
+ var isUnstaged = _selectedUnstaged != null && _selectedUnstaged.Count > 0;
+ if (change == null)
+ {
+ DetailContext = null;
+ }
+ else if (change.IsConflit && isUnstaged)
+ {
+ DetailContext = new ConflictContext(_repo.FullPath, change);
+ }
+ else
+ {
+ if (_detailContext is DiffContext previous)
+ {
+ DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), previous);
+ }
+ else
+ {
+ DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged));
+ }
+ }
+ }
+
+ private async void UseTheirs(List changes)
+ {
+ var files = new List();
+ foreach (var change in changes)
+ {
+ if (change.IsConflit)
+ files.Add(change.Path);
+ }
+
+ _repo.SetWatcherEnabled(false);
+ var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseTheirs(files));
+ if (succ)
+ {
+ await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec());
+ }
+ _repo.MarkWorkingCopyDirtyManually();
+ _repo.SetWatcherEnabled(true);
+ }
+
+ private async void UseMine(List changes)
+ {
+ var files = new List();
+ foreach (var change in changes)
+ {
+ if (change.IsConflit)
+ files.Add(change.Path);
+ }
+
+ _repo.SetWatcherEnabled(false);
+ var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseMine(files));
+ if (succ)
+ {
+ await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec());
+ }
+ _repo.MarkWorkingCopyDirtyManually();
+ _repo.SetWatcherEnabled(true);
+ }
+
+ private async void UseExternalMergeTool(Models.Change change)
+ {
+ var type = Preference.Instance.ExternalMergeToolType;
+ var exec = Preference.Instance.ExternalMergeToolPath;
+
+ var tool = Models.ExternalMerger.Supported.Find(x => x.Type == type);
+ if (tool == null)
+ {
+ App.RaiseException(_repo.FullPath, "Invalid merge tool in preference setting!");
+ return;
+ }
+
+ var args = tool.Type != 0 ? tool.Cmd : Preference.Instance.ExternalMergeToolCmd;
+
+ _repo.SetWatcherEnabled(false);
+ await Task.Run(() => Commands.MergeTool.OpenForMerge(_repo.FullPath, exec, args, change.Path));
+ _repo.SetWatcherEnabled(true);
+ }
+
+ private void DoCommit(bool autoPush)
+ {
+ if (!PopupHost.CanCreatePopup())
+ {
+ App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!");
+ return;
+ }
+
+ if (_staged.Count == 0)
+ {
+ App.RaiseException(_repo.FullPath, "No files added to commit!");
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(_commitMessage))
+ {
+ App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!");
+ return;
+ }
+
+ PushCommitMessage();
+
+ SetDetail(null);
+ IsCommitting = true;
+ _repo.SetWatcherEnabled(false);
+
+ Task.Run(() =>
+ {
+ var succ = new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec();
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (succ)
+ {
+ CommitMessage = string.Empty;
+ UseAmend = false;
+
+ if (autoPush)
+ {
+ PopupHost.ShowAndStartPopup(new Push(_repo, null));
+ }
+ }
+ _repo.MarkWorkingCopyDirtyManually();
+ _repo.SetWatcherEnabled(true);
+
+ IsCommitting = false;
+ });
+ });
+ }
+
private void PushCommitMessage()
{
var existIdx = _repo.CommitMessages.IndexOf(CommitMessage);
@@ -1022,13 +1018,9 @@ namespace SourceGit.ViewModels
private bool _useAmend = false;
private List _unstaged = null;
private List _staged = null;
- private Models.Change _selectedUnstagedChange = null;
- private Models.Change _selectedStagedChange = null;
+ private List _selectedUnstaged = null;
+ private List _selectedStaged = null;
private int _count = 0;
- private List _unstagedTree = null;
- private List _stagedTree = null;
- private FileTreeNode _selectedUnstagedTreeNode = null;
- private FileTreeNode _selectedStagedTreeNode = null;
private object _detailContext = null;
private string _commitMessage = string.Empty;
}
diff --git a/src/Views/ChangeCollectionView.axaml b/src/Views/ChangeCollectionView.axaml
new file mode 100644
index 00000000..9b37cb17
--- /dev/null
+++ b/src/Views/ChangeCollectionView.axaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs
new file mode 100644
index 00000000..9b9ad904
--- /dev/null
+++ b/src/Views/ChangeCollectionView.axaml.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Models.TreeDataGrid;
+using Avalonia.Interactivity;
+
+namespace SourceGit.Views
+{
+ public partial class ChangeCollectionView : UserControl
+ {
+ public static readonly StyledProperty IsWorkingCopyChangeProperty =
+ AvaloniaProperty.Register(nameof(IsWorkingCopy), false);
+
+ public bool IsWorkingCopy
+ {
+ get => GetValue(IsWorkingCopyChangeProperty);
+ set => SetValue(IsWorkingCopyChangeProperty, value);
+ }
+
+ public static readonly StyledProperty SingleSelectProperty =
+ AvaloniaProperty.Register(nameof(SingleSelect), true);
+
+ public bool SingleSelect
+ {
+ get => GetValue(SingleSelectProperty);
+ set => SetValue(SingleSelectProperty, value);
+ }
+
+ public static readonly StyledProperty ViewModeProperty =
+ AvaloniaProperty.Register(nameof(ViewMode), Models.ChangeViewMode.Tree);
+
+ public Models.ChangeViewMode ViewMode
+ {
+ get => GetValue(ViewModeProperty);
+ set => SetValue(ViewModeProperty, value);
+ }
+
+ public static readonly StyledProperty> ChangesProperty =
+ AvaloniaProperty.Register>(nameof(Changes), null);
+
+ public List Changes
+ {
+ get => GetValue(ChangesProperty);
+ set => SetValue(ChangesProperty, value);
+ }
+
+ public static readonly StyledProperty> SelectedChangesProperty =
+ AvaloniaProperty.Register>(nameof(SelectedChanges), null);
+
+ public List SelectedChanges
+ {
+ get => GetValue(SelectedChangesProperty);
+ set => SetValue(SelectedChangesProperty, value);
+ }
+
+ public static readonly RoutedEvent ChangeDoubleTappedEvent =
+ RoutedEvent.Register(nameof(ChangeDoubleTapped), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ public event EventHandler ChangeDoubleTapped
+ {
+ add { AddHandler(ChangeDoubleTappedEvent, value); }
+ remove { RemoveHandler(ChangeDoubleTappedEvent, value); }
+ }
+
+ static ChangeCollectionView()
+ {
+ ViewModeProperty.Changed.AddClassHandler((c, e) => c.UpdateSource());
+ ChangesProperty.Changed.AddClassHandler((c, e) => c.UpdateSource());
+ SelectedChangesProperty.Changed.AddClassHandler((c, e) => c.UpdateSelected());
+ }
+
+ public ChangeCollectionView()
+ {
+ InitializeComponent();
+ }
+
+ private void UpdateSource()
+ {
+ if (tree.Source is IDisposable disposable)
+ {
+ disposable.Dispose();
+ tree.Source = null;
+ }
+
+ var changes = Changes;
+ if (changes == null)
+ return;
+
+ var viewMode = ViewMode;
+ if (viewMode == Models.ChangeViewMode.Tree)
+ {
+ var filetree = ViewModels.FileTreeNode.Build(changes, true);
+ var source = new HierarchicalTreeDataGridSource(filetree)
+ {
+ Columns =
+ {
+ new HierarchicalExpanderColumn(
+ new TemplateColumn(null, "TreeModeTemplate", null, GridLength.Auto),
+ x => x.Children,
+ x => x.Children.Count > 0,
+ x => x.IsExpanded),
+ new TextColumn(
+ null,
+ x => string.Empty,
+ GridLength.Star)
+ }
+ };
+
+ var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children);
+ selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
+
+ source.Selection = selection;
+ source.RowSelection.SingleSelect = SingleSelect;
+ source.RowSelection.SelectionChanged += (s, _) =>
+ {
+ if (!_isSelecting && s is Models.TreeDataGridSelectionModel model)
+ {
+ var selection = new List();
+ foreach (var c in model.SelectedItems)
+ CollectChangesInNode(selection, c);
+
+ _isSelecting = true;
+ SetCurrentValue(SelectedChangesProperty, selection);
+ _isSelecting = false;
+ }
+ };
+
+ tree.Source = source;
+ }
+ else if (viewMode == Models.ChangeViewMode.List)
+ {
+ var source = new FlatTreeDataGridSource(changes)
+ {
+ Columns = { new TemplateColumn(null, "ListModeTemplate", null, GridLength.Auto) }
+ };
+
+ var selection = new Models.TreeDataGridSelectionModel(source, null);
+ selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
+
+ source.Selection = selection;
+ source.RowSelection.SingleSelect = SingleSelect;
+ source.RowSelection.SelectionChanged += (s, _) =>
+ {
+ if (!_isSelecting && s is Models.TreeDataGridSelectionModel model)
+ {
+ var selection = new List();
+ foreach (var c in model.SelectedItems)
+ selection.Add(c);
+
+ _isSelecting = true;
+ SetCurrentValue(SelectedChangesProperty, selection);
+ _isSelecting = false;
+ }
+ };
+
+ tree.Source = source;
+ }
+ else
+ {
+ var source = new FlatTreeDataGridSource(changes)
+ {
+ Columns =
+ {
+ new TemplateColumn(null, "GridModeFileTemplate", null, GridLength.Auto),
+ new TemplateColumn(null, "GridModeDirTemplate", null, GridLength.Auto)
+ },
+ };
+
+ var selection = new Models.TreeDataGridSelectionModel(source, null);
+ selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
+
+ source.Selection = selection;
+ source.RowSelection.SingleSelect = SingleSelect;
+ source.RowSelection.SelectionChanged += (s, _) =>
+ {
+ if (!_isSelecting && s is Models.TreeDataGridSelectionModel model)
+ {
+ var selection = new List();
+ foreach (var c in model.SelectedItems)
+ selection.Add(c);
+
+ _isSelecting = true;
+ SetCurrentValue(SelectedChangesProperty, selection);
+ _isSelecting = false;
+ }
+ };
+
+ tree.Source = source;
+ }
+ }
+
+ private void UpdateSelected()
+ {
+ if (_isSelecting || tree.Source == null)
+ return;
+
+ _isSelecting = true;
+ var selected = SelectedChanges;
+ if (tree.Source.Selection is Models.TreeDataGridSelectionModel changeSelection)
+ {
+ if (selected == null || selected.Count == 0)
+ changeSelection.Clear();
+ else
+ changeSelection.Select(selected);
+ }
+ else if (tree.Source.Selection is Models.TreeDataGridSelectionModel treeSelection)
+ {
+ if (selected == null || selected.Count == 0)
+ {
+ treeSelection.Clear();
+ _isSelecting = false;
+ return;
+ }
+
+ var set = new HashSet