From 8793e3c44482f0fd90e9230ed4dadda6a4004b16 Mon Sep 17 00:00:00 2001 From: Ivan Murashka Date: Fri, 24 Apr 2026 19:50:45 +0200 Subject: [PATCH 1/4] enhance: arrow-key navigation in Local Changes tree view --- src/Views/ChangeCollectionView.axaml.cs | 66 +++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index d23bf5bcd..5e507ee46 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -35,19 +35,75 @@ public class ChangeCollectionContainer : ListBox protected override void OnKeyDown(KeyEventArgs e) { - if (SelectedItems is [ViewModels.ChangeTreeNode node]) + if (e.KeyModifiers == KeyModifiers.None && + (e.Key == Key.Left || e.Key == Key.Right) && + SelectedItems is [ViewModels.ChangeTreeNode node] && + this.FindAncestorOfType() is { Content: ViewModels.ChangeCollectionAsTree tree } view) { - if (((e.Key == Key.Left && node.IsExpanded) || (e.Key == Key.Right && !node.IsExpanded)) && - e.KeyModifiers == KeyModifiers.None) + if (e.Key == Key.Left) { - this.FindAncestorOfType()?.ToggleNodeIsExpanded(node); - e.Handled = true; + if (node.IsFolder && node.IsExpanded) + { + view.ToggleNodeIsExpanded(node); + } + else + { + var parent = FindParentRow(tree.Rows, node); + if (parent != null) + MoveSelectionTo(parent); + } } + else + { + if (node.IsFolder && !node.IsExpanded) + { + view.ToggleNodeIsExpanded(node); + } + else if (node.IsFolder) + { + var idx = tree.Rows.IndexOf(node); + if (idx >= 0 && idx + 1 < tree.Rows.Count) + { + var child = tree.Rows[idx + 1]; + if (child.Depth == node.Depth + 1) + MoveSelectionTo(child); + } + } + } + + e.Handled = true; } if (!e.Handled && e.Key != Key.Space && e.Key != Key.Enter) base.OnKeyDown(e); } + + private void MoveSelectionTo(ViewModels.ChangeTreeNode target) + { + SelectedItem = target; + ScrollIntoView(target); + if (ContainerFromItem(target) is Control container) + container.Focus(); + } + + private static ViewModels.ChangeTreeNode FindParentRow(IList rows, ViewModels.ChangeTreeNode node) + { + if (node.Depth == 0) + return null; + + var idx = rows.IndexOf(node); + if (idx <= 0) + return null; + + var parentDepth = node.Depth - 1; + for (int i = idx - 1; i >= 0; i--) + { + if (rows[i].Depth == parentDepth) + return rows[i]; + } + + return null; + } } public partial class ChangeCollectionView : UserControl From 7f0faff4ef77939e0f1dd0c47216657ca82e5a66 Mon Sep 17 00:00:00 2001 From: Ivan Murashka Date: Fri, 24 Apr 2026 20:00:58 +0200 Subject: [PATCH 2/4] enhance: arrow-key navigation in commit Files tree view --- src/Views/RevisionFileTreeView.axaml.cs | 67 ++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs index e9a0e22fe..d549160b5 100644 --- a/src/Views/RevisionFileTreeView.axaml.cs +++ b/src/Views/RevisionFileTreeView.axaml.cs @@ -123,13 +123,41 @@ protected override async void OnKeyDown(KeyEventArgs e) if (SelectedItem is ViewModels.RevisionFileTreeNode node) { - if (node.IsFolder && - e.KeyModifiers == KeyModifiers.None && - (node.IsExpanded && e.Key == Key.Left) || (!node.IsExpanded && e.Key == Key.Right)) + if (e.KeyModifiers == KeyModifiers.None && + (e.Key == Key.Left || e.Key == Key.Right) && + this.FindAncestorOfType() is { } tree) { - var tree = this.FindAncestorOfType(); - if (tree != null) - await tree.ToggleNodeIsExpandedAsync(node); + if (e.Key == Key.Left) + { + if (node.IsFolder && node.IsExpanded) + { + await tree.ToggleNodeIsExpandedAsync(node); + } + else + { + var parent = FindParentRow(tree.Rows, node); + if (parent != null) + MoveSelectionTo(parent); + } + } + else + { + if (node.IsFolder && !node.IsExpanded) + { + await tree.ToggleNodeIsExpandedAsync(node); + } + else if (node.IsFolder) + { + var idx = tree.Rows.IndexOf(node); + if (idx >= 0 && idx + 1 < tree.Rows.Count) + { + var child = tree.Rows[idx + 1]; + if (child.Depth == node.Depth + 1) + MoveSelectionTo(child); + } + } + } + e.Handled = true; } else if (e.Key == Key.C && @@ -182,6 +210,33 @@ protected override async void OnKeyDown(KeyEventArgs e) if (!e.Handled) base.OnKeyDown(e); } + + private void MoveSelectionTo(ViewModels.RevisionFileTreeNode target) + { + SelectedItem = target; + ScrollIntoView(target); + if (ContainerFromItem(target) is Control container) + container.Focus(); + } + + private static ViewModels.RevisionFileTreeNode FindParentRow(IList rows, ViewModels.RevisionFileTreeNode node) + { + if (node.Depth == 0) + return null; + + var idx = rows.IndexOf(node); + if (idx <= 0) + return null; + + var parentDepth = node.Depth - 1; + for (int i = idx - 1; i >= 0; i--) + { + if (rows[i].Depth == parentDepth) + return rows[i]; + } + + return null; + } } public partial class RevisionFileTreeView : UserControl From e9e74bbf420e9633eaf73d71e37fa445a31be2fa Mon Sep 17 00:00:00 2001 From: Ivan Murashka Date: Fri, 24 Apr 2026 20:27:48 +0200 Subject: [PATCH 3/4] enhance: robust focus transfer for arrow-key tree navigation on virtualized lists --- src/Views/ChangeCollectionView.axaml.cs | 3 +++ src/Views/RevisionFileTreeView.axaml.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index 5e507ee46..be2e7ebec 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -8,6 +8,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Threading; using Avalonia.VisualTree; namespace SourceGit.Views @@ -84,6 +85,8 @@ private void MoveSelectionTo(ViewModels.ChangeTreeNode target) ScrollIntoView(target); if (ContainerFromItem(target) is Control container) container.Focus(); + else + Dispatcher.UIThread.Post(() => (ContainerFromItem(target) as Control)?.Focus(), DispatcherPriority.Loaded); } private static ViewModels.ChangeTreeNode FindParentRow(IList rows, ViewModels.ChangeTreeNode node) diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs index d549160b5..c3c1ef6b5 100644 --- a/src/Views/RevisionFileTreeView.axaml.cs +++ b/src/Views/RevisionFileTreeView.axaml.cs @@ -12,6 +12,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform.Storage; +using Avalonia.Threading; using Avalonia.VisualTree; namespace SourceGit.Views @@ -217,6 +218,8 @@ private void MoveSelectionTo(ViewModels.RevisionFileTreeNode target) ScrollIntoView(target); if (ContainerFromItem(target) is Control container) container.Focus(); + else + Dispatcher.UIThread.Post(() => (ContainerFromItem(target) as Control)?.Focus(), DispatcherPriority.Loaded); } private static ViewModels.RevisionFileTreeNode FindParentRow(IList rows, ViewModels.RevisionFileTreeNode node) From ebf8e1c3a9670f484af77c6621677beb6e82cb4d Mon Sep 17 00:00:00 2001 From: Ivan Murashka Date: Fri, 24 Apr 2026 20:28:28 +0200 Subject: [PATCH 4/4] code_style: name outer-ancestor local consistently across tree views --- src/Views/RevisionFileTreeView.axaml.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs index c3c1ef6b5..114b809af 100644 --- a/src/Views/RevisionFileTreeView.axaml.cs +++ b/src/Views/RevisionFileTreeView.axaml.cs @@ -126,17 +126,17 @@ protected override async void OnKeyDown(KeyEventArgs e) { if (e.KeyModifiers == KeyModifiers.None && (e.Key == Key.Left || e.Key == Key.Right) && - this.FindAncestorOfType() is { } tree) + this.FindAncestorOfType() is { } view) { if (e.Key == Key.Left) { if (node.IsFolder && node.IsExpanded) { - await tree.ToggleNodeIsExpandedAsync(node); + await view.ToggleNodeIsExpandedAsync(node); } else { - var parent = FindParentRow(tree.Rows, node); + var parent = FindParentRow(view.Rows, node); if (parent != null) MoveSelectionTo(parent); } @@ -145,14 +145,14 @@ protected override async void OnKeyDown(KeyEventArgs e) { if (node.IsFolder && !node.IsExpanded) { - await tree.ToggleNodeIsExpandedAsync(node); + await view.ToggleNodeIsExpandedAsync(node); } else if (node.IsFolder) { - var idx = tree.Rows.IndexOf(node); - if (idx >= 0 && idx + 1 < tree.Rows.Count) + var idx = view.Rows.IndexOf(node); + if (idx >= 0 && idx + 1 < view.Rows.Count) { - var child = tree.Rows[idx + 1]; + var child = view.Rows[idx + 1]; if (child.Depth == node.Depth + 1) MoveSelectionTo(child); }