diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs
index 0b35f8d618..659fd8bef4 100644
--- a/src/Models/RepositoryUIStates.cs
+++ b/src/Models/RepositoryUIStates.cs
@@ -7,6 +7,18 @@
namespace SourceGit.Models
{
+ public enum RepositoryWorkspaceOrientation
+ {
+ SideBySide,
+ Stacked,
+ }
+
+ public enum RepositoryNavigationPlacement
+ {
+ Sidebar,
+ Top,
+ }
+
public class RepositoryUIStates
{
public HistoryShowFlags HistoryShowFlags
@@ -45,6 +57,36 @@ public double AuthorColumnWidth
set;
} = 120;
+ public int SplitPrimaryViewIndex
+ {
+ get;
+ set;
+ } = -1;
+
+ public int SecondaryViewIndex
+ {
+ get;
+ set;
+ } = -1;
+
+ public RepositoryWorkspaceOrientation WorkspaceOrientation
+ {
+ get;
+ set;
+ } = RepositoryWorkspaceOrientation.SideBySide;
+
+ public double WorkspaceSplitRatio
+ {
+ get;
+ set;
+ } = 0.5;
+
+ public RepositoryNavigationPlacement NavigationPlacement
+ {
+ get;
+ set;
+ } = RepositoryNavigationPlacement.Sidebar;
+
public bool EnableTopoOrderInHistory
{
get;
diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml
index 96399d52ef..82c2b4dd5a 100644
--- a/src/Resources/Locales/en_US.axaml
+++ b/src/Resources/Locales/en_US.axaml
@@ -798,6 +798,7 @@
Run `git gc` command for this repository.
Clear all
Clear
+ Close Secondary View
Configure this repository
CONTINUE
Custom Actions
@@ -822,10 +823,15 @@
LOCAL BRANCHES
More options...
Navigate to HEAD
+ Repository navigation
+ VIEW NAVIGATION
+ Sidebar
+ Top
Create Branch
CLEAR NOTIFICATIONS
Open as Folder
Open in {0}
+ Open in Secondary Pane
Open in External Tools
REMOTES
Add Remote
@@ -844,7 +850,10 @@
Show Submodules as Tree
Show Tags as Tree
SKIP
+ Split Side by Side
+ Split Stacked
Statistics
+ Swap Views
SUBMODULES
Add Submodule
Update Submodule
diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs
index 450bdb41c6..45745a593d 100644
--- a/src/ViewModels/Repository.cs
+++ b/src/ViewModels/Repository.cs
@@ -74,15 +74,89 @@ public int SelectedViewIndex
get => _selectedViewIndex;
set
{
+ if (!IsValidWorkspaceView(value))
+ return;
+
+ if (IsSplitViewEnabled && value == SecondaryViewIndex)
+ {
+ SwapWorkspaceViews();
+ return;
+ }
+
if (SetProperty(ref _selectedViewIndex, value))
{
- OnPropertyChanged(nameof(IsHistoriesVisible));
- OnPropertyChanged(nameof(IsWorkingCopyVisible));
- OnPropertyChanged(nameof(IsStashesVisible));
+ if (IsSplitViewEnabled)
+ _uiStates.SplitPrimaryViewIndex = value;
+ NotifyWorkspaceVisibilityChanged();
+ }
+ }
+ }
+
+ public int SecondaryViewIndex
+ {
+ get => _uiStates.SecondaryViewIndex;
+ private set
+ {
+ if (_uiStates.SecondaryViewIndex != value)
+ {
+ _uiStates.SecondaryViewIndex = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(IsSplitViewEnabled));
+ NotifyWorkspaceVisibilityChanged();
+ }
+ }
+ }
+
+ public bool IsSplitViewEnabled
+ {
+ get => SecondaryViewIndex >= 0;
+ }
+
+ public Models.RepositoryWorkspaceOrientation WorkspaceOrientation
+ {
+ get => _uiStates.WorkspaceOrientation;
+ private set
+ {
+ if (_uiStates.WorkspaceOrientation != value)
+ {
+ _uiStates.WorkspaceOrientation = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public double WorkspaceSplitRatio
+ {
+ get => double.IsFinite(_uiStates.WorkspaceSplitRatio) ? Math.Clamp(_uiStates.WorkspaceSplitRatio, 0.2, 0.8) : 0.5;
+ set
+ {
+ var ratio = double.IsFinite(value) ? Math.Clamp(value, 0.2, 0.8) : 0.5;
+ if (Math.Abs(_uiStates.WorkspaceSplitRatio - ratio) > 0.001)
+ {
+ _uiStates.WorkspaceSplitRatio = ratio;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public Models.RepositoryNavigationPlacement NavigationPlacement
+ {
+ get => _uiStates.NavigationPlacement;
+ private set
+ {
+ if (_uiStates.NavigationPlacement != value)
+ {
+ _uiStates.NavigationPlacement = value;
+ OnPropertyChanged();
}
}
}
+ public void SetNavigationPlacement(Models.RepositoryNavigationPlacement placement)
+ {
+ NavigationPlacement = placement;
+ }
+
public Histories Histories
{
get => _histories;
@@ -100,17 +174,70 @@ public StashesPage StashesPage
public bool IsHistoriesVisible
{
- get => SelectedViewIndex == 0;
+ get => SelectedViewIndex == 0 || SecondaryViewIndex == 0;
}
public bool IsWorkingCopyVisible
{
- get => SelectedViewIndex == 1;
+ get => SelectedViewIndex == 1 || SecondaryViewIndex == 1;
}
public bool IsStashesVisible
{
- get => SelectedViewIndex == 2;
+ get => SelectedViewIndex == 2 || SecondaryViewIndex == 2;
+ }
+
+ public void OpenViewInSecondary(int viewIndex, Models.RepositoryWorkspaceOrientation orientation)
+ {
+ if (!IsValidWorkspaceView(viewIndex) || viewIndex == SelectedViewIndex)
+ return;
+
+ if (!IsSplitViewEnabled)
+ _uiStates.SplitPrimaryViewIndex = SelectedViewIndex;
+
+ WorkspaceOrientation = orientation;
+ SecondaryViewIndex = viewIndex;
+ }
+
+ public void SetWorkspaceOrientation(Models.RepositoryWorkspaceOrientation orientation)
+ {
+ if (IsSplitViewEnabled)
+ WorkspaceOrientation = orientation;
+ }
+
+ public void CloseSecondaryView()
+ {
+ if (!IsSplitViewEnabled)
+ return;
+
+ SecondaryViewIndex = -1;
+ _uiStates.SplitPrimaryViewIndex = -1;
+ }
+
+ public void SwapWorkspaceViews()
+ {
+ if (!IsSplitViewEnabled)
+ return;
+
+ var primary = SelectedViewIndex;
+ _selectedViewIndex = SecondaryViewIndex;
+ _uiStates.SecondaryViewIndex = primary;
+ _uiStates.SplitPrimaryViewIndex = _selectedViewIndex;
+ OnPropertyChanged(nameof(SelectedViewIndex));
+ OnPropertyChanged(nameof(SecondaryViewIndex));
+ NotifyWorkspaceVisibilityChanged();
+ }
+
+ private bool IsValidWorkspaceView(int viewIndex)
+ {
+ return viewIndex == 0 || (!IsBare && viewIndex is 1 or 2);
+ }
+
+ private void NotifyWorkspaceVisibilityChanged()
+ {
+ OnPropertyChanged(nameof(IsHistoriesVisible));
+ OnPropertyChanged(nameof(IsWorkingCopyVisible));
+ OnPropertyChanged(nameof(IsStashesVisible));
}
public bool EnableTopoOrderInHistory
@@ -495,12 +622,41 @@ public void Open()
_workingCopy = new WorkingCopy(this) { CommitMessage = _uiStates.LastCommitMessage };
_stashesPage = new StashesPage(this);
_searchCommitContext = new SearchCommitContext(this);
- _selectedViewIndex = Preferences.Instance.ShowLocalChangesByDefault ? 1 : 0;
+ RestoreWorkspaceState();
_lastFetchTime = DateTime.Now;
_autoFetchTimer = new Timer(AutoFetchByTimer, null, 5000, 5000);
RefreshAll();
}
+ private void RestoreWorkspaceState()
+ {
+ var defaultView = Preferences.Instance.ShowLocalChangesByDefault && !IsBare ? 1 : 0;
+ var primary = _uiStates.SplitPrimaryViewIndex;
+ var secondary = _uiStates.SecondaryViewIndex;
+ if (!IsValidWorkspaceView(primary) ||
+ !IsValidWorkspaceView(secondary) ||
+ primary == secondary)
+ {
+ _selectedViewIndex = defaultView;
+ _uiStates.SplitPrimaryViewIndex = -1;
+ _uiStates.SecondaryViewIndex = -1;
+ }
+ else
+ {
+ _selectedViewIndex = primary;
+ }
+
+ if (!Enum.IsDefined(_uiStates.WorkspaceOrientation))
+ _uiStates.WorkspaceOrientation = Models.RepositoryWorkspaceOrientation.SideBySide;
+
+ if (!Enum.IsDefined(_uiStates.NavigationPlacement))
+ _uiStates.NavigationPlacement = Models.RepositoryNavigationPlacement.Sidebar;
+
+ _uiStates.WorkspaceSplitRatio = double.IsFinite(_uiStates.WorkspaceSplitRatio)
+ ? Math.Clamp(_uiStates.WorkspaceSplitRatio, 0.2, 0.8)
+ : 0.5;
+ }
+
public void Close()
{
var commitMessage = _workingCopy.CommitMessage;
diff --git a/src/Views/DiffView.axaml.cs b/src/Views/DiffView.axaml.cs
index d6d4b6c23d..6a9256b36f 100644
--- a/src/Views/DiffView.axaml.cs
+++ b/src/Views/DiffView.axaml.cs
@@ -43,7 +43,11 @@ protected override void OnLoaded(RoutedEventArgs e)
if (DataContext is ViewModels.DiffContext vm)
vm.CheckSettings();
- ToggleHotkeyBindings(IsEffectivelyVisible);
+ var repository = this.FindAncestorOfType();
+ if (repository != null)
+ repository.UpdateWorkspaceHotkeys();
+ else
+ ToggleHotkeyBindings(IsEffectivelyVisible);
}
private void OnGotoFirstChange(object _, RoutedEventArgs e)
diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml
index 61f349b876..224c0c6ed0 100644
--- a/src/Views/Histories.axaml
+++ b/src/Views/Histories.axaml
@@ -33,6 +33,7 @@
SelectedCommits="{Binding SelectedCommits, Mode=TwoWay}"
ColumnHeaderHeight="24"
RowHeight="26"
+ SizeChanged="OnCommitListSizeChanged"
LayoutUpdated="OnCommitListLayoutUpdated"
ContextRequested="OnCommitListContextRequested"
DoubleTapped="OnCommitListDoubleTapped">
@@ -99,7 +100,7 @@
-
+
@@ -146,9 +147,9 @@
-
+
-
+
@@ -174,9 +175,9 @@
-
+
-
+
@@ -191,9 +192,9 @@
-
+
-
+
@@ -211,9 +212,9 @@
-
+
-
+
@@ -231,7 +232,7 @@
-
+
diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs
index 160e66ca6e..807dff37a3 100644
--- a/src/Views/Histories.axaml.cs
+++ b/src/Views/Histories.axaml.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
@@ -36,19 +38,26 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
{
base.OnPropertyChanged(change);
- if (change.Property == UseHorizontalProperty && IsLoaded)
+ if ((change.Property == UseHorizontalProperty || change.Property == BoundsProperty) && IsLoaded)
RefreshLayout();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
- RefreshLayout();
+ RefreshLayout(true);
}
- private void RefreshLayout()
+ private void RefreshLayout(bool force = false)
{
- if (UseHorizontal)
+ var useHorizontal = UseHorizontal && Bounds.Width >= 720;
+ if (!force && _hasAppliedLayout && useHorizontal == _lastUseHorizontal)
+ return;
+
+ _hasAppliedLayout = true;
+ _lastUseHorizontal = useHorizontal;
+
+ if (useHorizontal)
{
var rowSpan = RowDefinitions.Count;
for (int i = 0; i < Children.Count; i++)
@@ -81,6 +90,8 @@ private void RefreshLayout()
}
private bool _useHorizontal = false;
+ private bool _hasAppliedLayout;
+ private bool _lastUseHorizontal;
}
public class HistoriesCommitList : DataGrid
@@ -295,8 +306,19 @@ private void DecrNoSelectionChangeCount()
private List _selectedCommits = [];
}
+ public class HistoriesDataGridColumn : DataGridTemplateColumn
+ {
+ public string Name
+ {
+ get;
+ set;
+ } = string.Empty;
+ }
+
public partial class Histories : UserControl
{
+ private const double GraphAndSubjectMinWidth = 320;
+
public static readonly DirectProperty CurrentBranchProperty =
AvaloniaProperty.RegisterDirect(
nameof(CurrentBranch),
@@ -372,6 +394,101 @@ public bool IsDetailsPanelExpanded
public Histories()
{
InitializeComponent();
+ GraphAndSubjectColumn = FindColumn(nameof(GraphAndSubjectColumn));
+ AuthorColumn = FindColumn(nameof(AuthorColumn));
+ SHAColumn = FindColumn(nameof(SHAColumn));
+ AuthorTimeColumn = FindColumn(nameof(AuthorTimeColumn));
+ CommitTimeColumn = FindColumn(nameof(CommitTimeColumn));
+ GraphAndSubjectColumn.MinWidth = GraphAndSubjectMinWidth;
+ }
+
+ private HistoriesDataGridColumn FindColumn(string name)
+ {
+ return CommitListContainer.Columns.OfType().Single(x => x.Name == name);
+ }
+
+ protected override void OnLoaded(RoutedEventArgs e)
+ {
+ base.OnLoaded(e);
+
+ if (DataContext is ViewModels.Histories vm)
+ SubscribeToViewModel(vm);
+
+ ApplyResponsiveCommitColumns();
+ }
+
+ protected override void OnUnloaded(RoutedEventArgs e)
+ {
+ UnsubscribeFromViewModel();
+ base.OnUnloaded(e);
+ }
+
+ private void OnCommitListSizeChanged(object _, SizeChangedEventArgs e)
+ {
+ if (e.WidthChanged)
+ ApplyResponsiveCommitColumns();
+ }
+
+ private void ApplyResponsiveCommitColumns()
+ {
+ if (DataContext is not ViewModels.Histories vm || CommitListContainer.Bounds.Width <= 0)
+ return;
+
+ var authorWidth = Math.Max(AuthorColumn.MinWidth, vm.AuthorColumnWidth);
+ AuthorColumn.Width = new DataGridLength(authorWidth, DataGridLengthUnitType.Pixel);
+
+ var remaining = Math.Max(0, CommitListContainer.Bounds.Width - GraphAndSubjectMinWidth);
+ bool AdmitColumn(DataGridColumn column, bool requested, double width)
+ {
+ var visible = requested && remaining >= width;
+ if (visible)
+ remaining -= width;
+
+ column.SetCurrentValue(DataGridColumn.IsVisibleProperty, visible);
+ return visible;
+ }
+
+ AdmitColumn(AuthorColumn, vm.IsAuthorColumnVisible, authorWidth);
+
+ if (vm.IsCommitTimeColumnVisible)
+ AdmitColumn(CommitTimeColumn, true, CommitTimeColumn.MinWidth);
+ else
+ AdmitColumn(AuthorTimeColumn, vm.IsAuthorTimeColumnVisible, AuthorTimeColumn.MinWidth);
+
+ AdmitColumn(SHAColumn, vm.IsSHAColumnVisible, SHAColumn.MinWidth);
+
+ if (vm.IsCommitTimeColumnVisible)
+ AdmitColumn(AuthorTimeColumn, vm.IsAuthorTimeColumnVisible, AuthorTimeColumn.MinWidth);
+ else
+ CommitTimeColumn.SetCurrentValue(DataGridColumn.IsVisibleProperty, false);
+ }
+
+ private void SubscribeToViewModel(ViewModels.Histories vm)
+ {
+ if (_subscribedViewModel == vm)
+ return;
+
+ UnsubscribeFromViewModel();
+ _subscribedViewModel = vm;
+ _subscribedViewModel.PropertyChanged += OnHistoriesViewModelPropertyChanged;
+ }
+
+ private void UnsubscribeFromViewModel()
+ {
+ if (_subscribedViewModel == null)
+ return;
+
+ _subscribedViewModel.PropertyChanged -= OnHistoriesViewModelPropertyChanged;
+ _subscribedViewModel = null;
+ }
+
+ private void OnHistoriesViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName is nameof(ViewModels.Histories.IsAuthorColumnVisible) or
+ nameof(ViewModels.Histories.IsSHAColumnVisible) or
+ nameof(ViewModels.Histories.IsAuthorTimeColumnVisible) or
+ nameof(ViewModels.Histories.IsCommitTimeColumnVisible))
+ ApplyResponsiveCommitColumns();
}
public async Task GotoParent()
@@ -465,7 +582,17 @@ protected override void OnDataContextChanged(EventArgs e)
base.OnDataContextChanged(e);
if (DataContext is ViewModels.Histories vm)
- CommitListContainer.Columns[1].Width = new(vm.AuthorColumnWidth, DataGridLengthUnitType.Pixel);
+ {
+ if (IsLoaded)
+ SubscribeToViewModel(vm);
+ AuthorColumn.Width = new(vm.AuthorColumnWidth, DataGridLengthUnitType.Pixel);
+ }
+ else
+ {
+ UnsubscribeFromViewModel();
+ }
+
+ ApplyResponsiveCommitColumns();
}
private void OnCommitListHeaderPointerMoved(object sender, PointerEventArgs e)
@@ -473,22 +600,26 @@ private void OnCommitListHeaderPointerMoved(object sender, PointerEventArgs e)
if (sender is not Border border)
return;
- if (DataContext is not ViewModels.Histories { IsAuthorColumnVisible: true } vm)
+ if (DataContext is not ViewModels.Histories vm)
+ return;
+
+ if (!AuthorColumn.IsVisible)
return;
var pos = e.GetPosition(border);
if (_resizingAuthorColumn)
{
- var posX = CommitListContainer.Columns[0].ActualWidth;
- var maxW = posX + CommitListContainer.Columns[1].ActualWidth - 100;
+ var posX = GraphAndSubjectColumn.ActualWidth;
+ var otherColumnsWidth = GetVisibleMetadataWidthExceptAuthor();
+ var maxW = Math.Max(AuthorColumn.MinWidth, CommitListContainer.Bounds.Width - GraphAndSubjectMinWidth - otherColumnsWidth);
var delta = posX - pos.X;
- var w = Math.Max(Math.Min(vm.AuthorColumnWidth + delta, maxW), 80);
- CommitListContainer.Columns[1].Width = new(w, DataGridLengthUnitType.Pixel);
+ var w = Math.Clamp(vm.AuthorColumnWidth + delta, AuthorColumn.MinWidth, maxW);
+ AuthorColumn.Width = new(w, DataGridLengthUnitType.Pixel);
vm.AuthorColumnWidth = w;
}
else
{
- var dis = CommitListContainer.Columns[0].ActualWidth - 4 - pos.X;
+ var dis = GraphAndSubjectColumn.ActualWidth - 4 - pos.X;
if (dis < 4 && dis > -4)
{
if (border.Cursor != _resizingCursor)
@@ -506,8 +637,11 @@ private void OnCommitListHeaderPointerPressed(object sender, PointerPressedEvent
if (sender is not Border border)
return;
+ if (!AuthorColumn.IsVisible)
+ return;
+
var pos = e.GetPosition(border);
- var dis = CommitListContainer.Columns[0].ActualWidth - 4 - pos.X;
+ var dis = GraphAndSubjectColumn.ActualWidth - 4 - pos.X;
if (dis > 4 || dis < -4)
return;
@@ -523,6 +657,18 @@ private void OnCommitListHeaderPointerReleased(object sender, PointerReleasedEve
_resizingAuthorColumn = false;
}
+ private double GetVisibleMetadataWidthExceptAuthor()
+ {
+ var width = 0.0;
+ if (SHAColumn.IsVisible)
+ width += SHAColumn.ActualWidth;
+ if (AuthorTimeColumn.IsVisible)
+ width += AuthorTimeColumn.ActualWidth;
+ if (CommitTimeColumn.IsVisible)
+ width += CommitTimeColumn.ActualWidth;
+ return width;
+ }
+
private void OnCommitListHeaderContextRequested(object sender, ContextRequestedEventArgs e)
{
if (DataContext is not ViewModels.Histories vm)
@@ -614,7 +760,7 @@ private void OnCommitListLayoutUpdated(object _1, EventArgs _2)
IsScrollToTopVisible = startY >= rowHeight;
- var clipWidth = dataGrid.Columns[0].ActualWidth - 4;
+ var clipWidth = GraphAndSubjectColumn.ActualWidth - 4;
var lastLayout = CommitGraph.Layout;
if (lastLayout == null ||
Math.Abs(lastLayout.StartY - startY) > 0.01 ||
@@ -1754,5 +1900,11 @@ private async Task InteractiveRebaseWithPrefillActionAsync(ViewModels.Repository
private bool _isDetailsPanelExpanded = true;
private bool _resizingAuthorColumn = false;
private Cursor _resizingCursor = new(StandardCursorType.SizeWestEast);
+ private ViewModels.Histories _subscribedViewModel = null;
+ private HistoriesDataGridColumn GraphAndSubjectColumn { get; }
+ private HistoriesDataGridColumn AuthorColumn { get; }
+ private HistoriesDataGridColumn SHAColumn { get; }
+ private HistoriesDataGridColumn AuthorTimeColumn { get; }
+ private HistoriesDataGridColumn CommitTimeColumn { get; }
}
}
diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml
index 00aa09a228..f9293354c2 100644
--- a/src/Views/Launcher.axaml
+++ b/src/Views/Launcher.axaml
@@ -11,7 +11,7 @@
x:Name="ThisControl"
Icon="/App.ico"
Title="{Binding Title}"
- MinWidth="1024" MinHeight="600">
+ MinWidth="480" MinHeight="360">
diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml
index 8c9557e53d..ba2cc0cd2b 100644
--- a/src/Views/Repository.axaml
+++ b/src/Views/Repository.axaml
@@ -9,167 +9,110 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.Repository"
x:DataType="vm:Repository">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -707,7 +650,7 @@
-
-
-
+
+
+
+
+
+
@@ -821,7 +775,7 @@
Click="OnAbortInProgress"/>
-
-
+
@@ -966,7 +920,7 @@
VerticalAlignment="Center"
Filter="{Binding Mode=OneWay}"
IsFilterValid="{Binding IsValid, Mode=OneWay}"/>
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs
index ecee53c52f..b8d4bcd4f5 100644
--- a/src/Views/Repository.axaml.cs
+++ b/src/Views/Repository.axaml.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using Avalonia;
using Avalonia.Controls;
@@ -10,6 +11,16 @@ namespace SourceGit.Views
{
public partial class Repository : UserControl
{
+ private const double CompactLayoutThreshold = 1100;
+ private const double WorkspacePaneMinWidth = 300;
+ private const double WorkspacePaneMinHeight = 160;
+ private const double WorkspaceSplitterSize = 4;
+ private bool _isCompactLayout;
+ private bool _isCompactSidebarOpen;
+ private GridLength _expandedSidebarWidth = new(260, GridUnitType.Pixel);
+ private ViewModels.Repository _subscribedRepository;
+ private int _activeWorkspaceViewIndex = -1;
+
public Repository()
{
InitializeComponent();
@@ -19,6 +30,496 @@ protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
UpdateLeftSidebarLayout();
+ UpdateResponsiveLayout(Bounds.Width);
+
+ if (DataContext is ViewModels.Repository repo)
+ {
+ SubscribeToRepository(repo);
+ _activeWorkspaceViewIndex = repo.SelectedViewIndex;
+ }
+
+ ApplyWorkspaceLayout();
+ }
+
+ protected override void OnUnloaded(RoutedEventArgs e)
+ {
+ UnsubscribeFromRepository();
+ base.OnUnloaded(e);
+ }
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+ UnsubscribeFromRepository();
+
+ if (IsLoaded && DataContext is ViewModels.Repository repo)
+ {
+ SubscribeToRepository(repo);
+ _activeWorkspaceViewIndex = repo.SelectedViewIndex;
+ }
+
+ ApplyWorkspaceLayout();
+ }
+
+ private void OnRepositorySizeChanged(object _, SizeChangedEventArgs e)
+ {
+ if (e.WidthChanged)
+ UpdateResponsiveLayout(e.NewSize.Width);
+ }
+
+ private void UpdateResponsiveLayout(double width)
+ {
+ if (width <= 0)
+ return;
+
+ var sidebarColumn = RootLayout.ColumnDefinitions[0];
+ var sidebarSplitterColumn = RootLayout.ColumnDefinitions[1];
+ var useCompactLayout = width < CompactLayoutThreshold;
+ if (useCompactLayout == _isCompactLayout)
+ {
+ if (_isCompactSidebarOpen)
+ FullSidebar.Width = Math.Min(340, Math.Max(240, width - 48));
+ return;
+ }
+
+ if (useCompactLayout)
+ {
+ var current = ViewModels.Preferences.Instance.Layout.RepositorySidebarWidth;
+ if (current.IsAbsolute && current.Value >= 200)
+ _expandedSidebarWidth = current;
+
+ _isCompactLayout = true;
+ _isCompactSidebarOpen = false;
+ sidebarColumn.MinWidth = 0;
+ sidebarColumn.MaxWidth = 48;
+ sidebarColumn.SetCurrentValue(ColumnDefinition.WidthProperty, new GridLength(48, GridUnitType.Pixel));
+ sidebarSplitterColumn.Width = new GridLength(0);
+ SidebarSplitter.IsVisible = false;
+ CompactNavigationRail.IsVisible = true;
+ FullSidebar.IsVisible = false;
+ }
+ else
+ {
+ _isCompactLayout = false;
+ _isCompactSidebarOpen = false;
+ CompactSidebarBackdrop.IsVisible = false;
+ CompactNavigationRail.IsVisible = false;
+ CompactSidebarCloseButton.IsVisible = false;
+ FullSidebar.IsVisible = true;
+ FullSidebar.Width = double.NaN;
+ FullSidebar.HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch;
+ Grid.SetColumnSpan(FullSidebar, 1);
+ sidebarColumn.MinWidth = 200;
+ sidebarColumn.MaxWidth = 500;
+ sidebarColumn.SetCurrentValue(ColumnDefinition.WidthProperty, _expandedSidebarWidth);
+ sidebarSplitterColumn.Width = new GridLength(3, GridUnitType.Pixel);
+ SidebarSplitter.IsVisible = true;
+ ViewModels.Preferences.Instance.Layout.RepositorySidebarWidth = _expandedSidebarWidth;
+ }
+ }
+
+ private void OnOpenCompactSidebar(object _, RoutedEventArgs e)
+ {
+ if (_isCompactLayout)
+ {
+ _isCompactSidebarOpen = true;
+ CompactSidebarBackdrop.IsVisible = true;
+ CompactNavigationRail.IsVisible = false;
+ CompactSidebarCloseButton.IsVisible = true;
+ FullSidebar.IsVisible = true;
+ FullSidebar.Width = Math.Min(340, Math.Max(240, Bounds.Width - 48));
+ FullSidebar.HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left;
+ Grid.SetColumnSpan(FullSidebar, 3);
+ }
+
+ e.Handled = true;
+ }
+
+ private void OnCloseCompactSidebar(object _, RoutedEventArgs e)
+ {
+ CloseCompactSidebar();
+ e.Handled = true;
+ }
+
+ private void OnCompactSidebarBackdropPressed(object _, PointerPressedEventArgs e)
+ {
+ CloseCompactSidebar();
+ e.Handled = true;
+ }
+
+ private void OnCompactViewSelected(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button { Tag: string tag } &&
+ int.TryParse(tag, out var selectedView) &&
+ DataContext is ViewModels.Repository repo)
+ repo.SelectedViewIndex = selectedView;
+
+ CloseCompactSidebar();
+ e.Handled = true;
+ }
+
+ internal void CloseCompactSidebar()
+ {
+ if (!_isCompactLayout || !_isCompactSidebarOpen)
+ return;
+
+ _isCompactSidebarOpen = false;
+ CompactSidebarBackdrop.IsVisible = false;
+ CompactSidebarCloseButton.IsVisible = false;
+ FullSidebar.IsVisible = false;
+ FullSidebar.Width = double.NaN;
+ Grid.SetColumnSpan(FullSidebar, 1);
+ CompactNavigationRail.IsVisible = true;
+ }
+
+ private void SubscribeToRepository(ViewModels.Repository repo)
+ {
+ if (_subscribedRepository == repo)
+ return;
+
+ UnsubscribeFromRepository();
+ _subscribedRepository = repo;
+ _subscribedRepository.PropertyChanged += OnRepositoryPropertyChanged;
+ }
+
+ private void UnsubscribeFromRepository()
+ {
+ if (_subscribedRepository == null)
+ return;
+
+ _subscribedRepository.PropertyChanged -= OnRepositoryPropertyChanged;
+ _subscribedRepository = null;
+ }
+
+ private void OnRepositoryPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (DataContext is not ViewModels.Repository repo)
+ return;
+
+ if (e.PropertyName == nameof(ViewModels.Repository.SelectedViewIndex))
+ _activeWorkspaceViewIndex = repo.SelectedViewIndex;
+
+ if (e.PropertyName is nameof(ViewModels.Repository.SelectedViewIndex) or
+ nameof(ViewModels.Repository.SecondaryViewIndex) or
+ nameof(ViewModels.Repository.IsSplitViewEnabled) or
+ nameof(ViewModels.Repository.WorkspaceOrientation) or
+ nameof(ViewModels.Repository.WorkspaceSplitRatio))
+ ApplyWorkspaceLayout();
+ }
+
+ private void OnWorkspaceHostSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (e.WidthChanged)
+ ApplyWorkspaceLayout();
+ }
+
+ private void ApplyWorkspaceLayout()
+ {
+ if (DataContext is not ViewModels.Repository repo || WorkspaceHost.Bounds.Width <= 0)
+ return;
+
+ var pages = new[] { HistoriesPage, WorkingCopyPage, StashesPage };
+ foreach (var page in pages)
+ {
+ page.IsVisible = false;
+ Grid.SetColumn(page, 0);
+ Grid.SetColumnSpan(page, 1);
+ Grid.SetRow(page, 0);
+ Grid.SetRowSpan(page, 1);
+ }
+
+ var columns = WorkspaceHost.ColumnDefinitions;
+ var rows = WorkspaceHost.RowDefinitions;
+ for (var i = 0; i < columns.Count; i++)
+ {
+ columns[i].MinWidth = 0;
+ columns[i].Width = new GridLength(0);
+ }
+ for (var i = 0; i < rows.Count; i++)
+ {
+ rows[i].MinHeight = 0;
+ rows[i].Height = new GridLength(0);
+ }
+
+ var primaryPage = GetWorkspacePage(repo.SelectedViewIndex);
+ primaryPage.IsVisible = true;
+ columns[0].Width = new GridLength(1, GridUnitType.Star);
+ rows[0].Height = new GridLength(1, GridUnitType.Star);
+ WorkspaceSplitter.IsVisible = false;
+
+ if (repo.IsSplitViewEnabled)
+ {
+ var secondaryPage = GetWorkspacePage(repo.SecondaryViewIndex);
+ secondaryPage.IsVisible = true;
+ var ratio = repo.WorkspaceSplitRatio;
+ var useSideBySide = repo.WorkspaceOrientation == Models.RepositoryWorkspaceOrientation.SideBySide &&
+ WorkspaceHost.Bounds.Width >= WorkspacePaneMinWidth * 2 + WorkspaceSplitterSize;
+
+ WorkspaceSplitter.IsVisible = true;
+ if (useSideBySide)
+ {
+ columns[0].MinWidth = WorkspacePaneMinWidth;
+ columns[0].Width = new GridLength(ratio, GridUnitType.Star);
+ columns[1].Width = new GridLength(WorkspaceSplitterSize, GridUnitType.Pixel);
+ columns[2].MinWidth = WorkspacePaneMinWidth;
+ columns[2].Width = new GridLength(1 - ratio, GridUnitType.Star);
+
+ Grid.SetColumn(secondaryPage, 2);
+ Grid.SetColumn(WorkspaceSplitter, 1);
+ Grid.SetRow(WorkspaceSplitter, 0);
+ WorkspaceSplitter.Width = WorkspaceSplitterSize;
+ WorkspaceSplitter.Height = double.NaN;
+ WorkspaceSplitter.ResizeDirection = GridResizeDirection.Columns;
+ WorkspaceSplitter.BorderThickness = new Thickness(1, 0, 0, 0);
+ }
+ else
+ {
+ rows[0].MinHeight = WorkspacePaneMinHeight;
+ rows[0].Height = new GridLength(ratio, GridUnitType.Star);
+ rows[1].Height = new GridLength(WorkspaceSplitterSize, GridUnitType.Pixel);
+ rows[2].MinHeight = WorkspacePaneMinHeight;
+ rows[2].Height = new GridLength(1 - ratio, GridUnitType.Star);
+
+ Grid.SetRow(secondaryPage, 2);
+ Grid.SetColumn(WorkspaceSplitter, 0);
+ Grid.SetRow(WorkspaceSplitter, 1);
+ WorkspaceSplitter.Width = double.NaN;
+ WorkspaceSplitter.Height = WorkspaceSplitterSize;
+ WorkspaceSplitter.ResizeDirection = GridResizeDirection.Rows;
+ WorkspaceSplitter.BorderThickness = new Thickness(0, 1, 0, 0);
+ }
+ }
+
+ if (_activeWorkspaceViewIndex != repo.SelectedViewIndex &&
+ _activeWorkspaceViewIndex != repo.SecondaryViewIndex)
+ _activeWorkspaceViewIndex = repo.SelectedViewIndex;
+
+ UpdateWorkspaceHotkeys();
+ }
+
+ private Border GetWorkspacePage(int viewIndex)
+ {
+ return viewIndex switch
+ {
+ 1 => WorkingCopyPage,
+ 2 => StashesPage,
+ _ => HistoriesPage,
+ };
+ }
+
+ private int GetWorkspaceViewIndex(object page)
+ {
+ if (page == WorkingCopyPage)
+ return 1;
+ if (page == StashesPage)
+ return 2;
+ return 0;
+ }
+
+ private void OnWorkspacePagePointerEntered(object sender, PointerEventArgs e)
+ {
+ ActivateWorkspacePage(sender);
+ }
+
+ private void OnWorkspacePageGotFocus(object sender, GotFocusEventArgs e)
+ {
+ ActivateWorkspacePage(sender);
+ }
+
+ private void ActivateWorkspacePage(object page)
+ {
+ if (page is not Border { IsVisible: true } border)
+ return;
+
+ _activeWorkspaceViewIndex = GetWorkspaceViewIndex(border);
+ UpdateWorkspaceHotkeys();
+ }
+
+ internal void UpdateWorkspaceHotkeys()
+ {
+ var pages = new[] { HistoriesPage, WorkingCopyPage, StashesPage };
+ for (var i = 0; i < pages.Length; i++)
+ {
+ var diffViewer = pages[i].FindDescendantOfType();
+ diffViewer?.ToggleHotkeyBindings(pages[i].IsVisible && i == _activeWorkspaceViewIndex);
+ }
+ }
+
+ private void OnWorkspaceSplitterDragCompleted(object sender, VectorEventArgs e)
+ {
+ if (DataContext is not ViewModels.Repository { IsSplitViewEnabled: true } repo)
+ return;
+
+ var columns = WorkspaceHost.ColumnDefinitions;
+ var rows = WorkspaceHost.RowDefinitions;
+ double first;
+ double second;
+ if (WorkspaceSplitter.ResizeDirection == GridResizeDirection.Columns)
+ {
+ first = columns[0].ActualWidth;
+ second = columns[2].ActualWidth;
+ }
+ else
+ {
+ first = rows[0].ActualHeight;
+ second = rows[2].ActualHeight;
+ }
+
+ if (first + second > 0)
+ repo.WorkspaceSplitRatio = first / (first + second);
+ }
+
+ private void OnRepositoryViewContextRequested(object sender, ContextRequestedEventArgs e)
+ {
+ OpenRepositoryViewContextMenu(sender, e, false);
+ }
+
+ internal void OpenRepositoryViewContextMenu(object sender, ContextRequestedEventArgs e, bool includeViewAction)
+ {
+ if (DataContext is not ViewModels.Repository repo ||
+ sender is not Control control ||
+ !int.TryParse(control.Tag?.ToString(), out var viewIndex))
+ return;
+
+ var menu = new ContextMenu();
+ if (!repo.IsBare)
+ {
+ var openSecondary = new MenuItem
+ {
+ Header = App.Text("Repository.OpenInSecondary"),
+ Icon = this.CreateMenuIcon("Icons.Layout"),
+ IsEnabled = viewIndex != repo.SelectedViewIndex && viewIndex != repo.SecondaryViewIndex,
+ };
+ openSecondary.Click += (_, ev) =>
+ {
+ repo.OpenViewInSecondary(viewIndex, repo.WorkspaceOrientation);
+ ev.Handled = true;
+ };
+ menu.Items.Add(openSecondary);
+ menu.Items.Add(new MenuItem { Header = "-" });
+
+ var canOpenSplit = repo.IsSplitViewEnabled || viewIndex != repo.SelectedViewIndex;
+ var sideBySide = new MenuItem
+ {
+ Header = App.Text("Repository.SplitSideBySide"),
+ Icon = repo.IsSplitViewEnabled && repo.WorkspaceOrientation == Models.RepositoryWorkspaceOrientation.SideBySide
+ ? this.CreateMenuIcon("Icons.Check")
+ : null,
+ IsEnabled = canOpenSplit,
+ };
+ sideBySide.Click += (_, ev) =>
+ {
+ if (repo.IsSplitViewEnabled)
+ repo.SetWorkspaceOrientation(Models.RepositoryWorkspaceOrientation.SideBySide);
+ else
+ repo.OpenViewInSecondary(viewIndex, Models.RepositoryWorkspaceOrientation.SideBySide);
+ ev.Handled = true;
+ };
+ menu.Items.Add(sideBySide);
+
+ var stacked = new MenuItem
+ {
+ Header = App.Text("Repository.SplitStacked"),
+ Icon = repo.IsSplitViewEnabled && repo.WorkspaceOrientation == Models.RepositoryWorkspaceOrientation.Stacked
+ ? this.CreateMenuIcon("Icons.Check")
+ : null,
+ IsEnabled = canOpenSplit,
+ };
+ stacked.Click += (_, ev) =>
+ {
+ if (repo.IsSplitViewEnabled)
+ repo.SetWorkspaceOrientation(Models.RepositoryWorkspaceOrientation.Stacked);
+ else
+ repo.OpenViewInSecondary(viewIndex, Models.RepositoryWorkspaceOrientation.Stacked);
+ ev.Handled = true;
+ };
+ menu.Items.Add(stacked);
+ menu.Items.Add(new MenuItem { Header = "-" });
+
+ var swap = new MenuItem
+ {
+ Header = App.Text("Repository.SwapViews"),
+ Icon = this.CreateMenuIcon("Icons.Layout"),
+ IsEnabled = repo.IsSplitViewEnabled,
+ };
+ swap.Click += (_, ev) =>
+ {
+ repo.SwapWorkspaceViews();
+ ev.Handled = true;
+ };
+ menu.Items.Add(swap);
+
+ var close = new MenuItem
+ {
+ Header = App.Text("Repository.CloseSecondaryView"),
+ Icon = this.CreateMenuIcon("Icons.Close"),
+ IsEnabled = repo.IsSplitViewEnabled,
+ };
+ close.Click += (_, ev) =>
+ {
+ repo.CloseSecondaryView();
+ ev.Handled = true;
+ };
+ menu.Items.Add(close);
+
+ if (includeViewAction && viewIndex is 1 or 2)
+ {
+ menu.Items.Add(new MenuItem { Header = "-" });
+ var action = new MenuItem
+ {
+ Header = App.Text(viewIndex == 1 ? "Repository.DiscardAll" : "Repository.ClearStashes"),
+ Icon = this.CreateMenuIcon(viewIndex == 1 ? "Icons.Undo" : "Icons.RemoveAll"),
+ };
+ action.Click += (_, ev) =>
+ {
+ if (viewIndex == 1)
+ repo.DiscardAllChanges();
+ else
+ repo.ClearStashes();
+ ev.Handled = true;
+ };
+ menu.Items.Add(action);
+ }
+
+ menu.Items.Add(new MenuItem { Header = "-" });
+ }
+
+ var navigation = new MenuItem
+ {
+ Header = App.Text("Repository.NavigationPlacement"),
+ IsEnabled = false,
+ };
+ menu.Items.Add(navigation);
+
+ var sidebarNavigation = new MenuItem
+ {
+ Header = App.Text("Repository.NavigationPlacement.Sidebar"),
+ Icon = repo.NavigationPlacement == Models.RepositoryNavigationPlacement.Sidebar
+ ? this.CreateMenuIcon("Icons.Check")
+ : null,
+ };
+ sidebarNavigation.Click += (_, ev) =>
+ {
+ repo.SetNavigationPlacement(Models.RepositoryNavigationPlacement.Sidebar);
+ ev.Handled = true;
+ };
+ menu.Items.Add(sidebarNavigation);
+
+ var topNavigation = new MenuItem
+ {
+ Header = App.Text("Repository.NavigationPlacement.Top"),
+ Icon = repo.NavigationPlacement == Models.RepositoryNavigationPlacement.Top
+ ? this.CreateMenuIcon("Icons.Check")
+ : null,
+ };
+ topNavigation.Click += (_, ev) =>
+ {
+ repo.SetNavigationPlacement(Models.RepositoryNavigationPlacement.Top);
+ ev.Handled = true;
+ };
+ menu.Items.Add(topNavigation);
+ menu.Open(control);
+ e.Handled = true;
}
private void OnToggleFilter(object _, RoutedEventArgs e)
@@ -359,7 +860,7 @@ private void OnSearchSuggestionTapped(object sender, TappedEventArgs e)
e.Handled = true;
}
- private void OnOpenAdvancedHistoriesOption(object sender, RoutedEventArgs e)
+ internal void OpenAdvancedHistoriesOption(object sender, RoutedEventArgs e)
{
if (sender is Button button && DataContext is ViewModels.Repository { Histories: { } histories } repo)
{
@@ -695,13 +1196,5 @@ private async void OnBisectCommand(object sender, RoutedEventArgs e)
e.Handled = true;
}
- private void OnRightPagePropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
- {
- if (e.Property == Border.IsVisibleProperty && sender is Border page)
- {
- var diffViewer = page.FindDescendantOfType();
- diffViewer?.ToggleHotkeyBindings(page.IsVisible);
- }
- }
}
}
diff --git a/src/Views/RepositoryToolbar.axaml b/src/Views/RepositoryToolbar.axaml
index 87450c0245..ab0ddb5e9f 100644
--- a/src/Views/RepositoryToolbar.axaml
+++ b/src/Views/RepositoryToolbar.axaml
@@ -7,130 +7,171 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.RepositoryToolbar"
x:DataType="vm:Repository">
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/RepositoryToolbar.axaml.cs b/src/Views/RepositoryToolbar.axaml.cs
index 0d03acb89b..21d1af473d 100644
--- a/src/Views/RepositoryToolbar.axaml.cs
+++ b/src/Views/RepositoryToolbar.axaml.cs
@@ -18,6 +18,72 @@ public RepositoryToolbar()
InitializeComponent();
}
+ private void OnToolbarSizeChanged(object _, SizeChangedEventArgs e)
+ {
+ if (!e.WidthChanged)
+ return;
+
+ var compact = e.NewSize.Width < 760;
+ FullToolbar.IsVisible = !compact;
+ CompactToolbar.IsVisible = compact;
+ }
+
+ private void OpenCompactOperations(object sender, RoutedEventArgs ev)
+ {
+ if (sender is not Button button || DataContext is not ViewModels.Repository repo)
+ return;
+
+ var logs = new MenuItem { Header = App.Text("Repository.ViewLogs"), Icon = this.CreateMenuIcon("Icons.Logs") };
+ logs.Click += (_, e) => OpenGitLogs(button, e);
+ var statistics = new MenuItem { Header = App.Text("Repository.Statistics"), Icon = this.CreateMenuIcon("Icons.Statistics") };
+ statistics.Click += (_, e) => OpenStatistics(button, e);
+ var configure = new MenuItem { Header = App.Text("Repository.Configure"), Icon = this.CreateMenuIcon("Icons.Settings") };
+ configure.Click += (_, e) => OpenConfigure(button, e);
+
+ var stash = new MenuItem { Header = App.Text("Stash"), Icon = this.CreateMenuIcon("Icons.Stashes.Add") };
+ stash.Click += async (_, e) =>
+ {
+ await repo.StashAllAsync(false);
+ e.Handled = true;
+ };
+ var applyPatch = new MenuItem { Header = App.Text("Apply.Title"), Icon = this.CreateMenuIcon("Icons.ApplyPatch") };
+ applyPatch.Click += (_, e) =>
+ {
+ repo.ApplyPatch();
+ e.Handled = true;
+ };
+
+ var gitFlow = new MenuItem { Header = App.Text("GitFlow"), Icon = this.CreateMenuIcon("Icons.GitFlow") };
+ gitFlow.Click += (_, e) => OpenGitFlowMenu(button, e);
+ var gitLfs = new MenuItem { Header = App.Text("GitLFS"), Icon = this.CreateMenuIcon("Icons.LFS") };
+ gitLfs.Click += (_, e) => OpenGitLFSMenu(button, e);
+ var bisect = new MenuItem { Header = App.Text("Bisect"), Icon = this.CreateMenuIcon("Icons.Bisect") };
+ bisect.Click += (_, e) => StartBisect(button, e);
+ var customActions = new MenuItem { Header = App.Text("Repository.CustomActions"), Icon = this.CreateMenuIcon("Icons.Action") };
+ customActions.Click += (_, e) => OpenCustomActionMenu(button, e);
+ var cleanup = new MenuItem { Header = App.Text("Repository.Clean"), Icon = this.CreateMenuIcon("Icons.Clean") };
+ cleanup.Click += (_, e) => Cleanup(button, e);
+
+ var menu = new ContextMenu { Placement = PlacementMode.BottomEdgeAlignedLeft };
+ menu.Items.Add(logs);
+ menu.Items.Add(statistics);
+ menu.Items.Add(configure);
+ menu.Items.Add(new MenuItem { Header = "-" });
+ if (!repo.IsBare)
+ {
+ menu.Items.Add(stash);
+ menu.Items.Add(applyPatch);
+ menu.Items.Add(new MenuItem { Header = "-" });
+ menu.Items.Add(gitFlow);
+ menu.Items.Add(gitLfs);
+ menu.Items.Add(bisect);
+ }
+ menu.Items.Add(customActions);
+ menu.Items.Add(cleanup);
+ menu.Open(button);
+ ev.Handled = true;
+ }
+
private void OpenWithExternalTools(object sender, RoutedEventArgs ev)
{
if (sender is Button button && DataContext is ViewModels.Repository repo)
diff --git a/src/Views/RepositoryViewSwitcher.axaml b/src/Views/RepositoryViewSwitcher.axaml
new file mode 100644
index 0000000000..0de085db12
--- /dev/null
+++ b/src/Views/RepositoryViewSwitcher.axaml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/RepositoryViewSwitcher.axaml.cs b/src/Views/RepositoryViewSwitcher.axaml.cs
new file mode 100644
index 0000000000..61946f2da6
--- /dev/null
+++ b/src/Views/RepositoryViewSwitcher.axaml.cs
@@ -0,0 +1,67 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.VisualTree;
+
+namespace SourceGit.Views
+{
+ public partial class RepositoryViewSwitcher : UserControl
+ {
+ public static readonly StyledProperty OrientationProperty =
+ AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical);
+
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ public static readonly StyledProperty ShowInlineActionsProperty =
+ AvaloniaProperty.Register(nameof(ShowInlineActions), true);
+
+ public bool ShowInlineActions
+ {
+ get => GetValue(ShowInlineActionsProperty);
+ set => SetValue(ShowInlineActionsProperty, value);
+ }
+
+ public static readonly StyledProperty ItemMinWidthProperty =
+ AvaloniaProperty.Register(nameof(ItemMinWidth));
+
+ public double ItemMinWidth
+ {
+ get => GetValue(ItemMinWidthProperty);
+ set => SetValue(ItemMinWidthProperty, value);
+ }
+
+ public RepositoryViewSwitcher()
+ {
+ InitializeComponent();
+ ViewSelector.AddHandler(PointerPressedEvent, OnViewSelectorPointerPressed, RoutingStrategies.Tunnel);
+ }
+
+ private void OnViewSelectorPointerPressed(object sender, PointerPressedEventArgs e)
+ {
+ if (e.GetCurrentPoint(ViewSelector).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonPressed)
+ e.Handled = true;
+ }
+
+ private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ this.FindAncestorOfType()?.CloseCompactSidebar();
+ e.Handled = true;
+ }
+
+ private void OnRepositoryViewContextRequested(object sender, ContextRequestedEventArgs e)
+ {
+ this.FindAncestorOfType()?.OpenRepositoryViewContextMenu(sender, e, !ShowInlineActions);
+ }
+
+ private void OnOpenAdvancedHistoriesOption(object sender, RoutedEventArgs e)
+ {
+ this.FindAncestorOfType()?.OpenAdvancedHistoriesOption(sender, e);
+ }
+ }
+}
diff --git a/src/Views/StashesPage.axaml b/src/Views/StashesPage.axaml
index 0ea6897f14..5f0bcb9ead 100644
--- a/src/Views/StashesPage.axaml
+++ b/src/Views/StashesPage.axaml
@@ -9,15 +9,20 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.StashesPage"
x:DataType="vm:StashesPage">
-
+
-
+
-
+
+
+
+
+
+
-
+
@@ -138,7 +143,7 @@
-
-
+
diff --git a/src/Views/StashesPage.axaml.cs b/src/Views/StashesPage.axaml.cs
index 678337c457..f11e1e4901 100644
--- a/src/Views/StashesPage.axaml.cs
+++ b/src/Views/StashesPage.axaml.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Text;
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform.Storage;
@@ -10,6 +11,10 @@ namespace SourceGit.Views
{
public partial class StashesPage : UserControl
{
+ private const double SingleColumnThreshold = 720;
+ private bool _isSingleColumn;
+ private GridLength _expandedLeftWidth = new(300, GridUnitType.Pixel);
+
public StashesPage()
{
InitializeComponent();
@@ -22,12 +27,75 @@ private void OnMainLayoutSizeChanged(object sender, SizeChangedEventArgs e)
var layout = ViewModels.Preferences.Instance.Layout;
var width = grid.Bounds.Width;
- var leftWidth = Math.Max(width - 304, 300);
+ if (width <= 0)
+ return;
+
+ var useSingleColumn = width < SingleColumnThreshold;
+ if (useSingleColumn != _isSingleColumn)
+ SetSingleColumnLayout(useSingleColumn);
+
+ if (useSingleColumn)
+ return;
+
+ var leftWidth = Math.Max(220, width - 264);
if (layout.StashesLeftWidth.Value - leftWidth > 1.0)
layout.StashesLeftWidth = new GridLength(leftWidth, GridUnitType.Pixel);
}
+ private void SetSingleColumnLayout(bool enabled)
+ {
+ var columns = MainLayout.ColumnDefinitions;
+ var rows = MainLayout.RowDefinitions;
+ var layout = ViewModels.Preferences.Instance.Layout;
+
+ _isSingleColumn = enabled;
+ if (enabled)
+ {
+ if (layout.StashesLeftWidth.IsAbsolute && layout.StashesLeftWidth.Value >= 220)
+ _expandedLeftWidth = layout.StashesLeftWidth;
+
+ columns[0].MinWidth = 0;
+ columns[0].SetCurrentValue(ColumnDefinition.WidthProperty, new GridLength(1, GridUnitType.Star));
+ columns[1].Width = new GridLength(0);
+ columns[2].MinWidth = 0;
+ columns[2].Width = new GridLength(0);
+ rows[0].Height = new GridLength(2, GridUnitType.Star);
+ rows[1].Height = new GridLength(4, GridUnitType.Pixel);
+ rows[2].Height = new GridLength(3, GridUnitType.Star);
+
+ Grid.SetColumn(StashListPanel, 0);
+ Grid.SetRow(StashListPanel, 0);
+ Grid.SetColumn(LayoutSplitter, 0);
+ Grid.SetRow(LayoutSplitter, 1);
+ Grid.SetColumn(DetailsPanel, 0);
+ Grid.SetRow(DetailsPanel, 2);
+ LayoutSplitter.BorderThickness = new Thickness(0, 1, 0, 0);
+ DetailsPanel.Margin = new Thickness(4, 0, 4, 4);
+ }
+ else
+ {
+ columns[0].MinWidth = 220;
+ columns[0].SetCurrentValue(ColumnDefinition.WidthProperty, _expandedLeftWidth);
+ columns[1].Width = new GridLength(4, GridUnitType.Pixel);
+ columns[2].MinWidth = 260;
+ columns[2].Width = new GridLength(1, GridUnitType.Star);
+ rows[0].Height = new GridLength(1, GridUnitType.Star);
+ rows[1].Height = new GridLength(0);
+ rows[2].Height = new GridLength(0);
+
+ Grid.SetColumn(StashListPanel, 0);
+ Grid.SetRow(StashListPanel, 0);
+ Grid.SetColumn(LayoutSplitter, 1);
+ Grid.SetRow(LayoutSplitter, 0);
+ Grid.SetColumn(DetailsPanel, 2);
+ Grid.SetRow(DetailsPanel, 0);
+ LayoutSplitter.BorderThickness = new Thickness(1, 0, 0, 0);
+ DetailsPanel.Margin = new Thickness(0, 4, 4, 4);
+ layout.StashesLeftWidth = _expandedLeftWidth;
+ }
+ }
+
private async void OnStashListKeyDown(object sender, KeyEventArgs e)
{
if (DataContext is ViewModels.StashesPage { SelectedStash: { } stash } vm)
diff --git a/src/Views/Welcome.axaml b/src/Views/Welcome.axaml
index 18bf85870c..340b4b261d 100644
--- a/src/Views/Welcome.axaml
+++ b/src/Views/Welcome.axaml
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml
index 12d1afa8a9..3486f6b167 100644
--- a/src/Views/WorkingCopy.axaml
+++ b/src/Views/WorkingCopy.axaml
@@ -8,15 +8,20 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="SourceGit.Views.WorkingCopy"
x:DataType="vm:WorkingCopy">
-
+
-
+
-
+
+
+
+
+
+
-
+
@@ -194,7 +199,7 @@
-
-
+
-
+
-
-
+
+
@@ -247,7 +252,7 @@
-
+
-
+
diff --git a/src/Views/WorkingCopy.axaml.cs b/src/Views/WorkingCopy.axaml.cs
index 72e6fbb4b2..7428413075 100644
--- a/src/Views/WorkingCopy.axaml.cs
+++ b/src/Views/WorkingCopy.axaml.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Text;
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@@ -13,6 +14,10 @@ namespace SourceGit.Views
{
public partial class WorkingCopy : UserControl
{
+ private const double SingleColumnThreshold = 720;
+ private bool _isSingleColumn;
+ private GridLength _expandedLeftWidth = new(300, GridUnitType.Pixel);
+
public WorkingCopy()
{
InitializeComponent();
@@ -25,12 +30,75 @@ private void OnMainLayoutSizeChanged(object sender, SizeChangedEventArgs e)
var layout = ViewModels.Preferences.Instance.Layout;
var width = grid.Bounds.Width;
- var leftWidth = Math.Max(width - 304, 300);
+ if (width <= 0)
+ return;
+
+ var useSingleColumn = width < SingleColumnThreshold;
+ if (useSingleColumn != _isSingleColumn)
+ SetSingleColumnLayout(useSingleColumn);
+
+ if (useSingleColumn)
+ return;
+
+ var leftWidth = Math.Max(220, width - 264);
if (layout.WorkingCopyLeftWidth.Value - leftWidth > 1.0)
layout.WorkingCopyLeftWidth = new GridLength(leftWidth, GridUnitType.Pixel);
}
+ private void SetSingleColumnLayout(bool enabled)
+ {
+ var columns = MainLayout.ColumnDefinitions;
+ var rows = MainLayout.RowDefinitions;
+ var layout = ViewModels.Preferences.Instance.Layout;
+
+ _isSingleColumn = enabled;
+ if (enabled)
+ {
+ if (layout.WorkingCopyLeftWidth.IsAbsolute && layout.WorkingCopyLeftWidth.Value >= 220)
+ _expandedLeftWidth = layout.WorkingCopyLeftWidth;
+
+ columns[0].MinWidth = 0;
+ columns[0].SetCurrentValue(ColumnDefinition.WidthProperty, new GridLength(1, GridUnitType.Star));
+ columns[1].Width = new GridLength(0);
+ columns[2].MinWidth = 0;
+ columns[2].Width = new GridLength(0);
+ rows[0].Height = new GridLength(2, GridUnitType.Star);
+ rows[1].Height = new GridLength(4, GridUnitType.Pixel);
+ rows[2].Height = new GridLength(3, GridUnitType.Star);
+
+ Grid.SetColumn(ChangesPanel, 0);
+ Grid.SetRow(ChangesPanel, 0);
+ Grid.SetColumn(LayoutSplitter, 0);
+ Grid.SetRow(LayoutSplitter, 1);
+ Grid.SetColumn(DetailsPanel, 0);
+ Grid.SetRow(DetailsPanel, 2);
+ LayoutSplitter.BorderThickness = new Thickness(0, 1, 0, 0);
+ DetailsPanel.Margin = new Thickness(4, 0, 4, 4);
+ }
+ else
+ {
+ columns[0].MinWidth = 220;
+ columns[0].SetCurrentValue(ColumnDefinition.WidthProperty, _expandedLeftWidth);
+ columns[1].Width = new GridLength(4, GridUnitType.Pixel);
+ columns[2].MinWidth = 260;
+ columns[2].Width = new GridLength(1, GridUnitType.Star);
+ rows[0].Height = new GridLength(1, GridUnitType.Star);
+ rows[1].Height = new GridLength(0);
+ rows[2].Height = new GridLength(0);
+
+ Grid.SetColumn(ChangesPanel, 0);
+ Grid.SetRow(ChangesPanel, 0);
+ Grid.SetColumn(LayoutSplitter, 1);
+ Grid.SetRow(LayoutSplitter, 0);
+ Grid.SetColumn(DetailsPanel, 2);
+ Grid.SetRow(DetailsPanel, 0);
+ LayoutSplitter.BorderThickness = new Thickness(1, 0, 0, 0);
+ DetailsPanel.Margin = new Thickness(0, 4, 4, 4);
+ layout.WorkingCopyLeftWidth = _expandedLeftWidth;
+ }
+ }
+
private async void OnOpenAssumeUnchanged(object sender, RoutedEventArgs e)
{
var repoView = this.FindAncestorOfType();