diff --git a/src/Models/HistoryLayout.cs b/src/Models/HistoryLayout.cs new file mode 100644 index 000000000..e2308c10f --- /dev/null +++ b/src/Models/HistoryLayout.cs @@ -0,0 +1,9 @@ +namespace SourceGit.Models +{ + public enum HistoryLayout + { + Stacked, + SideBySide, + Focused, + } +} diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs index 8657b93b6..bb68010da 100644 --- a/src/Models/RepositoryUIStates.cs +++ b/src/Models/RepositoryUIStates.cs @@ -45,6 +45,12 @@ public double AuthorColumnWidth set; } = 120; + public double FocusedHistoryDetailsWidth + { + get; + set; + } = 340; + public bool EnableTopoOrderInHistory { get; diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index d3ffb203a..c90a11326 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -515,6 +515,8 @@ Track all *{0} files Select Commit Advanced Options + ← Graph + Copy full commit SHA AUTHOR AUTHOR TIME COMMIT TIME @@ -527,6 +529,7 @@ Selected Commits Only Selected Commits (First-parent Only) SELECTED {0} COMMITS + DETAILS SHOW COLUMNS Hold 'Ctrl' or 'Shift' to select multiple commits. Hold ⌘ or ⇧ to select multiple commits. @@ -822,6 +825,7 @@ Filter in commit graph filter(s) enabled LAYOUT + Focused Horizontal Vertical COMMITS ORDER diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 636aad665..fb2c96a95 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -539,9 +539,9 @@ private void Refresh() }, token); } - private void UpdateDetails() + public void UpdateDetails() { - if (ActiveTabIndex == 1 && _changeSelection is { Count: 1, HasFolder: false }) + if ((ActiveTabIndex == 1 || Preferences.Instance.IsFocusedHistoryLayout) && _changeSelection is { Count: 1, HasFolder: false }) DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, _changeSelection.Changes[0]), _diffContext); else DiffContext = null; diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 128b53ff9..18064b5dc 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Threading.Tasks; @@ -149,10 +150,92 @@ public object DetailContext set { if (SetProperty(ref _detailContext, value)) + { + ShowGraph(); + ObserveDetailContext(value); OnPropertyChanged(nameof(IsOpenAsStandaloneVisible)); + } + } + } + + public DiffContext FocusedDiff + { + get => _focusedDiff; + private set + { + if (SetProperty(ref _focusedDiff, value)) + OnPropertyChanged(nameof(IsFocusedDiffVisible)); } } + public string FocusedDiffPath + { + get => _focusedDiffPath; + private set => SetProperty(ref _focusedDiffPath, value); + } + + public bool IsFocusedDiffVisible => Preferences.Instance.IsFocusedHistoryLayout && FocusedDiff != null; + + public void OpenFocusedDiff() + { + if (!Preferences.Instance.IsFocusedHistoryLayout) + return; + + ChangeSelection selection; + DiffContext diff; + if (DetailContext is CommitDetail detail) + { + selection = detail.ChangeSelection; + if (detail.DiffContext == null) + detail.UpdateDetails(); + diff = detail.DiffContext; + } + else if (DetailContext is RevisionCompare compare) + { + selection = compare.ChangeSelection; + diff = compare.DiffContext; + } + else + { + return; + } + + if (selection is { Count: 1, HasFolder: false } && diff != null) + { + FocusedDiffPath = selection.Changes[0].Path; + FocusedDiff = diff; + } + } + + public void ShowGraph() + { + FocusedDiff = null; + FocusedDiffPath = string.Empty; + } + + public void UpdateLayout() + { + ShowGraph(); + OnPropertyChanged(nameof(RightArea)); + OnPropertyChanged(nameof(BottomArea)); + } + + private void ObserveDetailContext(object context) + { + if (_observedDetailContext != null) + _observedDetailContext.PropertyChanged -= OnDetailContextPropertyChanged; + _observedDetailContext = context as INotifyPropertyChanged; + if (_observedDetailContext != null) + _observedDetailContext.PropertyChanged += OnDetailContextPropertyChanged; + } + + private void OnDetailContextPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(CommitDetail.Commit) or nameof(RevisionCompare.IsLoading) or + nameof(RevisionCompare.StartPoint) or nameof(RevisionCompare.EndPoint)) + ShowGraph(); + } + public Models.Bisect Bisect { get => _bisect; @@ -184,8 +267,20 @@ public GridLength LeftArea public GridLength RightArea { - get => _rightArea; - set => SetProperty(ref _rightArea, value); + get => Preferences.Instance.IsFocusedHistoryLayout ? new GridLength(_repo.UIStates.FocusedHistoryDetailsWidth) : _rightArea; + set + { + if (Preferences.Instance.IsFocusedHistoryLayout) + { + if (value.IsAbsolute && value.Value >= 260) + { + _repo.UIStates.FocusedHistoryDetailsWidth = value.Value; + OnPropertyChanged(); + } + } + else + SetProperty(ref _rightArea, value); + } } public GridLength TopArea @@ -199,7 +294,7 @@ public GridLength BottomArea get => _isCollapseDetails ? new GridLength(28, GridUnitType.Pixel) : _bottomArea; set { - if (!Preferences.Instance.UseTwoColumnsLayoutInHistories && !_isCollapseDetails) + if (!Preferences.Instance.IsHorizontalHistoryLayout && !_isCollapseDetails) SetProperty(ref _bottomArea, value); } } @@ -220,7 +315,7 @@ public bool IsCollapseDetails get => _isCollapseDetails; set { - if (!Preferences.Instance.UseTwoColumnsLayoutInHistories && SetProperty(ref _isCollapseDetails, value)) + if (!Preferences.Instance.IsHorizontalHistoryLayout && SetProperty(ref _isCollapseDetails, value)) { OnPropertyChanged(nameof(TopArea)); OnPropertyChanged(nameof(BottomArea)); @@ -588,6 +683,9 @@ private void GenerateGraph(List commits) private Models.Bisect _bisect = null; private object _detailContext = Models.Null.Instance; private bool _ignoreSelectionChange = false; + private INotifyPropertyChanged _observedDetailContext = null; + private DiffContext _focusedDiff = null; + private string _focusedDiffPath = string.Empty; private bool _isSearchingCommits = false; private SearchCommitContext _searchCommitContext = null; diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 4803ec429..c24d43bd9 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -213,8 +213,40 @@ public bool UseCompactBranchNamesInGraph public bool UseTwoColumnsLayoutInHistories { - get => _useTwoColumnsLayoutInHistories; - set => SetProperty(ref _useTwoColumnsLayoutInHistories, value); + get => IsHorizontalHistoryLayout; + set + { + // Keep reading older preference files. The explicit layout takes precedence, + // regardless of the order of properties in the JSON document. + if (SetProperty(ref _useTwoColumnsLayoutInHistories, value) && !_historyLayout.HasValue) + NotifyHistoryLayoutChanged(); + } + } + + public Models.HistoryLayout HistoryLayout + { + get => _historyLayout ?? (_useTwoColumnsLayoutInHistories ? Models.HistoryLayout.SideBySide : Models.HistoryLayout.Stacked); + set + { + if (Enum.IsDefined(value) && _historyLayout != value) + { + _historyLayout = value; + NotifyHistoryLayoutChanged(); + } + } + } + + [JsonIgnore] + public bool IsHorizontalHistoryLayout => HistoryLayout != Models.HistoryLayout.Stacked; + + [JsonIgnore] + public bool IsFocusedHistoryLayout => HistoryLayout == Models.HistoryLayout.Focused; + + private void NotifyHistoryLayoutChanged() + { + OnPropertyChanged(nameof(HistoryLayout)); + OnPropertyChanged(nameof(IsHorizontalHistoryLayout)); + OnPropertyChanged(nameof(IsFocusedHistoryLayout)); } public bool DisplayTimeAsPeriodInHistories @@ -831,6 +863,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private bool _showTagsInGraph = true; private bool _useTwoColumnsLayoutInHistories = false; + private Models.HistoryLayout? _historyLayout = null; private bool _displayTimeAsPeriodInHistories = false; private bool _useSideBySideDiff = false; private bool _ignoreWhitespaceChangesInDiff = false; diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 5f0e2fc1f..14b7fbeb4 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -121,8 +121,8 @@ _endPoint is Models.Commit e && hashes.Contains(e.SHA)) return; - _startPoint = l; - _endPoint = r; + StartPoint = l; + EndPoint = r; Refresh(); } @@ -343,6 +343,9 @@ private void RefreshVisible() private void Refresh() { + OnPropertyChanged(nameof(LeftSideDesc)); + OnPropertyChanged(nameof(RightSideDesc)); + Task.Run(async () => { _changes = await new Commands.CompareRevisions(_repo.FullPath, GetSHA(_startPoint), GetSHA(_endPoint)) diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index 81b7030d6..8dc36d23e 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -166,6 +166,49 @@ public event EventHandler ChangeDoubleTapped public ChangeCollectionView() { InitializeComponent(); + AddHandler(PointerReleasedEvent, OnChangePointerReleased, RoutingStrategies.Bubble, true); + AddHandler(KeyDownEvent, OnChangeKeyDown, RoutingStrategies.Bubble, true); + } + + public static readonly RoutedEvent ChangeActivatedEvent = + RoutedEvent.Register(nameof(ChangeActivated), RoutingStrategies.Bubble); + + public event EventHandler ChangeActivated + { + add { AddHandler(ChangeActivatedEvent, value); } + remove { RemoveHandler(ChangeActivatedEvent, value); } + } + + private void OnChangePointerReleased(object sender, PointerReleasedEventArgs e) + { + if (e.InitialPressMouseButton != MouseButton.Left || e.KeyModifiers != KeyModifiers.None || + Selection is not { Count: 1, HasFolder: false }) + return; + + for (var source = e.Source as Control; source != null && source != this; source = source.GetVisualParent() as Control) + { + if (source is not ListBoxItem item) + continue; + + var change = item.DataContext switch + { + Models.Change c => c, + ViewModels.ChangeTreeNode { IsFolder: false } node => node.Change, + _ => null, + }; + if (change == Selection.Changes[0]) + RaiseEvent(new RoutedEventArgs(ChangeActivatedEvent)); + return; + } + } + + private void OnChangeKeyDown(object sender, KeyEventArgs e) + { + if (e.KeyModifiers != KeyModifiers.None || Selection is not { Count: 1, HasFolder: false }) + return; + + if (e.Key is Key.Enter or Key.Space or Key.Up or Key.Down or Key.Home or Key.End or Key.PageUp or Key.PageDown) + RaiseEvent(new RoutedEventArgs(ChangeActivatedEvent)); } public void ToggleNodeIsExpanded(ViewModels.ChangeTreeNode node) diff --git a/src/Views/ChangeListLayout.cs b/src/Views/ChangeListLayout.cs new file mode 100644 index 000000000..813f84bff --- /dev/null +++ b/src/Views/ChangeListLayout.cs @@ -0,0 +1,59 @@ +using Avalonia; +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public class ChangeListLayout : Grid + { + public static readonly StyledProperty IsFileListOnlyProperty = + AvaloniaProperty.Register(nameof(IsFileListOnly)); + + public static readonly StyledProperty ListWidthProperty = + AvaloniaProperty.Register(nameof(ListWidth), new GridLength(256)); + + public bool IsFileListOnly + { + get => GetValue(IsFileListOnlyProperty); + set => SetValue(IsFileListOnlyProperty, value); + } + + public GridLength ListWidth + { + get => GetValue(ListWidthProperty); + set => SetValue(ListWidthProperty, value); + } + + protected override System.Type StyleKeyOverride => typeof(Grid); + + public ChangeListLayout() + { + ColumnDefinitions = new ColumnDefinitions("256,4,*"); + ColumnDefinitions[0].PropertyChanged += (_, e) => + { + if (!_updating && !IsFileListOnly && e.Property == ColumnDefinition.WidthProperty) + SetCurrentValue(ListWidthProperty, ColumnDefinitions[0].Width); + }; + RefreshLayout(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == IsFileListOnlyProperty || change.Property == ListWidthProperty) + RefreshLayout(); + } + + private void RefreshLayout() + { + _updating = true; + ColumnDefinitions[0].MinWidth = IsFileListOnly ? 0 : 200; + ColumnDefinitions[0].Width = IsFileListOnly ? GridLength.Star : ListWidth; + ColumnDefinitions[1].Width = new GridLength(IsFileListOnly ? 0 : 4); + ColumnDefinitions[2].MinWidth = IsFileListOnly ? 0 : 100; + ColumnDefinitions[2].Width = IsFileListOnly ? new GridLength(0) : GridLength.Star; + _updating = false; + } + + private bool _updating = false; + } +} diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml index d6c0dba6b..8a5a92f4b 100644 --- a/src/Views/CommitBaseInfo.axaml +++ b/src/Views/CommitBaseInfo.axaml @@ -8,7 +8,115 @@ xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CommitBaseInfo" - x:Name="ThisControl"> + x:Name="ThisControl" + Classes.compact="{Binding #ThisControl.IsCompact}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitBaseInfo.axaml.cs b/src/Views/CommitBaseInfo.axaml.cs index 50809468d..ce8c67b5b 100644 --- a/src/Views/CommitBaseInfo.axaml.cs +++ b/src/Views/CommitBaseInfo.axaml.cs @@ -10,6 +10,15 @@ namespace SourceGit.Views { public partial class CommitBaseInfo : UserControl { + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact)); + + public bool IsCompact + { + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + public static readonly DirectProperty FullMessageProperty = AvaloniaProperty.RegisterDirect( nameof(FullMessage), diff --git a/src/Views/CommitChanges.axaml b/src/Views/CommitChanges.axaml index 6fcdad281..75543f6e1 100644 --- a/src/Views/CommitChanges.axaml +++ b/src/Views/CommitChanges.axaml @@ -6,13 +6,10 @@ xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CommitChanges" - x:DataType="vm:CommitDetail"> - - - - - - + x:DataType="vm:CommitDetail" + x:Name="ThisControl"> + @@ -58,7 +55,8 @@ Changes="{Binding VisibleChanges, Mode=OneWay}" Selection="{Binding ChangeSelection, Mode=TwoWay}" ContextRequested="OnChangeContextRequested" - KeyDown="OnChangeCollectionViewKeyDown"/> + KeyDown="OnChangeCollectionViewKeyDown" + ChangeActivated="OnChangeActivated"/> @@ -75,12 +73,13 @@ - + @@ -100,5 +99,5 @@ - + diff --git a/src/Views/CommitChanges.axaml.cs b/src/Views/CommitChanges.axaml.cs index 5ff13d593..0c6385f21 100644 --- a/src/Views/CommitChanges.axaml.cs +++ b/src/Views/CommitChanges.axaml.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Text; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Platform.Storage; using Avalonia.VisualTree; @@ -11,11 +13,26 @@ namespace SourceGit.Views { public partial class CommitChanges : UserControl { + public static readonly StyledProperty IsFileListOnlyProperty = + AvaloniaProperty.Register(nameof(IsFileListOnly)); + + public bool IsFileListOnly + { + get => GetValue(IsFileListOnlyProperty); + set => SetValue(IsFileListOnlyProperty, value); + } + public CommitChanges() { InitializeComponent(); } + private void OnChangeActivated(object sender, RoutedEventArgs e) + { + if (IsFileListOnly && this.FindAncestorOfType() is { } histories) + histories.OpenFocusedDiff(); + } + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { e.Handled = true; diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index dd175eadd..64a8463a7 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -299,7 +299,7 @@ public ContextMenu CreateChangeContextMenu(Models.Change change) private void OnTabHeaderPointerPressed(object sender, PointerPressedEventArgs e) { - if (ViewModels.Preferences.Instance.UseTwoColumnsLayoutInHistories) + if (ViewModels.Preferences.Instance.IsHorizontalHistoryLayout) return; var historiesView = this.FindAncestorOfType(); diff --git a/src/Views/FocusedCommitDetail.axaml b/src/Views/FocusedCommitDetail.axaml new file mode 100644 index 000000000..496ac0053 --- /dev/null +++ b/src/Views/FocusedCommitDetail.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Views/FocusedCommitDetail.axaml.cs b/src/Views/FocusedCommitDetail.axaml.cs new file mode 100644 index 000000000..f8a969760 --- /dev/null +++ b/src/Views/FocusedCommitDetail.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class FocusedCommitDetail : UserControl + { + public FocusedCommitDetail() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index a3d24529f..58d3d0126 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -10,7 +10,8 @@ x:Class="SourceGit.Views.Histories" x:DataType="vm:Histories" x:Name="ThisControl"> - + @@ -299,6 +300,20 @@ + + + + + + + + + + + + + + - - - - - - + @@ -113,7 +123,8 @@ Changes="{Binding VisibleChanges, Mode=OneWay}" Selection="{Binding ChangeSelection, Mode=TwoWay}" ContextRequested="OnChangeContextRequested" - KeyDown="OnChangeCollectionViewKeyDown"/> + KeyDown="OnChangeCollectionViewKeyDown" + ChangeActivated="OnChangeActivated"/> @@ -135,12 +146,13 @@ - + @@ -160,6 +172,6 @@ - + diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index 1b2a1c32f..45fd764d2 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -2,20 +2,37 @@ using System.IO; using System.Text; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Platform.Storage; +using Avalonia.VisualTree; namespace SourceGit.Views { public partial class RevisionCompare : UserControl { + public static readonly StyledProperty IsFileListOnlyProperty = + AvaloniaProperty.Register(nameof(IsFileListOnly)); + + public bool IsFileListOnly + { + get => GetValue(IsFileListOnlyProperty); + set => SetValue(IsFileListOnlyProperty, value); + } + public RevisionCompare() { InitializeComponent(); } + private void OnChangeActivated(object sender, RoutedEventArgs e) + { + if (IsFileListOnly && this.FindAncestorOfType() is { } histories) + histories.OpenFocusedDiff(); + } + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { if (DataContext is ViewModels.RevisionCompare { ChangeSelection: { Count: > 0 } selection } vm)