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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
@@ -351,7 +380,11 @@
-
+
+
+
+
@@ -369,7 +402,7 @@
-
+
@@ -422,7 +455,7 @@
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
@@ -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)