diff --git a/src/Converters/FilterModeConverters.cs b/src/Converters/FilterModeConverters.cs index 016613e83..fe268ef79 100644 --- a/src/Converters/FilterModeConverters.cs +++ b/src/Converters/FilterModeConverters.cs @@ -15,5 +15,11 @@ public static class FilterModeConverters _ => Brushes.Transparent, }; }); + + public static readonly FuncValueConverter HighlightModeToIsAllBright = + new FuncValueConverter(v => + { + return v == Models.CommitGraphHighlighting.All; + }); } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 7f55e31f8..29ca5a6d4 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace SourceGit.Models @@ -13,6 +13,14 @@ public enum CommitSearchMethod ByContent, } + public enum CommitLineageSearchMethod + { + None = 0, + ParentsOnly = 1, + ChildsOnly = 2, + FullLineage = 3, + } + public class Commit { public string SHA { get; set; } = string.Empty; @@ -27,6 +35,8 @@ public class Commit public bool IsMerged { get; set; } = false; public int Color { get; set; } = 0; public double LeftMargin { get; set; } = 0; + public int Index { get; set; } = -1; + public int PathIndex { get; set; } = -1; public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 82505c351..0a314c961 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -6,6 +6,14 @@ namespace SourceGit.Models { + public enum CommitGraphHighlighting + { + All, + CurrentBranchOnly, + SelectedLineageOnly, + CurrentBranchAndSelectedLineage, + } + public record CommitGraphLayout(double StartY, double ClipWidth, double RowHeight); public class CommitGraph @@ -32,6 +40,9 @@ public class Path(int color, bool isMerged) public List Points { get; } = []; public int Color { get; } = color; public bool IsMerged { get; } = isMerged; + public bool IsHoveredRelated { get; set; } = false; + public int StartCommitIndex { get; set; } = -1; + public int EndCommitIndex { get; set; } = -1; } public class Link @@ -41,6 +52,8 @@ public class Link public Point End; public int Color; public bool IsMerged; + public int StartCommitIndex; + public int EndCommitIndex; } public enum DotType @@ -75,6 +88,13 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable var offsetY = -halfHeight; var colorPicker = new ColorPicker(); + var commitMap = new Dictionary(); + for (int i = 0; i < commits.Count; i++) + { + commits[i].Index = i; + commitMap[commits[i].SHA] = i; + } + foreach (var commit in commits) { PathHelper major = null; @@ -103,12 +123,14 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable else { major.End(offsetX, offsetY, halfHeight); + major.Path.EndCommitIndex = commit.Index; ended.Add(l); } } else { l.End(major.LastX, offsetY, halfHeight); + l.Path.EndCommitIndex = commit.Index; ended.Add(l); } @@ -137,16 +159,27 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable if (commit.Parents.Count > 0) { - major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY)); + major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY), commit.Index); unsolved.Add(major); temp.Paths.Add(major.Path); } } else if (isMerged && !major.IsMerged && commit.Parents.Count > 0) { - major.ReplaceMerged(); + major.Path.EndCommitIndex = commit.Index; + major.ReplaceMerged(commit.Index); temp.Paths.Add(major.Path); } + else if (major != null && commit.Parents.Count > 0) + { + // Break at every commit to ensure path-aware highlight is precise. + major.Path.EndCommitIndex = commit.Index; + major.Replace(major.Path.Color, major.Path.IsMerged, commit.Index); + temp.Paths.Add(major.Path); + } + + if (major != null) + commit.PathIndex = temp.Paths.IndexOf(major.Path); // Calculate link position of this commit. var position = new Point(major?.LastX ?? offsetX, offsetY); @@ -172,7 +205,8 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable if (isMerged && !parent.IsMerged) { parent.Goto(parent.LastX, offsetY + halfHeight, halfHeight); - parent.ReplaceMerged(); + parent.Path.EndCommitIndex = commit.Index; + parent.ReplaceMerged(commit.Index); temp.Paths.Add(parent.Path); } @@ -183,6 +217,8 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable Control = new Point(parent.LastX, position.Y), Color = parent.Path.Color, IsMerged = isMerged, + StartCommitIndex = commit.Index, + EndCommitIndex = commitMap.GetValueOrDefault(parentHash, -1), }); } else @@ -190,7 +226,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable offsetX += unitWidth; // Create new curve for parent commit that not includes before - var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight)); + var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight), commit.Index); unsolved.Add(l); temp.Paths.Add(l.Path); } @@ -213,6 +249,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable continue; path.End((i + 0.5) * unitWidth + 4, endY + halfHeight, halfHeight); + path.Path.EndCommitIndex = commits.Count - 1; } unsolved.Clear(); @@ -249,7 +286,7 @@ private class PathHelper public bool IsMerged => Path.IsMerged; - public PathHelper(string next, bool isMerged, int color, Point start) + public PathHelper(string next, bool isMerged, int color, Point start, int startCommitIndex) { Next = next; LastX = start.X; @@ -257,9 +294,10 @@ public PathHelper(string next, bool isMerged, int color, Point start) Path = new Path(color, isMerged); Path.Points.Add(start); + Path.StartCommitIndex = startCommitIndex; } - public PathHelper(string next, bool isMerged, int color, Point start, Point to) + public PathHelper(string next, bool isMerged, int color, Point start, Point to, int startCommitIndex) { Next = next; LastX = to.X; @@ -268,6 +306,7 @@ public PathHelper(string next, bool isMerged, int color, Point start, Point to) Path = new Path(color, isMerged); Path.Points.Add(start); Path.Points.Add(to); + Path.StartCommitIndex = startCommitIndex; } /// @@ -348,16 +387,21 @@ public void End(double x, double y, double halfHeight) /// /// End the current path and create a new from the end. /// - public void ReplaceMerged() + public void Replace(int newColor, bool isMerged, int startCommitIndex) { - var color = Path.Color; Add(LastX, _lastY); - Path = new Path(color, true); + Path = new Path(newColor, isMerged); Path.Points.Add(new Point(LastX, _lastY)); + Path.StartCommitIndex = startCommitIndex; _endY = 0; } + public void ReplaceMerged(int startCommitIndex) + { + Replace(Path.Color, true, startCommitIndex); + } + private void Add(double x, double y) { if (_endY < y) diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs index 53fbd801b..c83b26201 100644 --- a/src/Models/RepositoryUIStates.cs +++ b/src/Models/RepositoryUIStates.cs @@ -39,11 +39,17 @@ public bool EnableTopoOrderInHistory set; } = false; - public bool OnlyHighlightCurrentBranchInHistory + public CommitGraphHighlighting CommitGraphHighlighting { get; set; - } = false; + } = CommitGraphHighlighting.All; + + public CommitLineageSearchMethod LineageSearchMethod + { + get; + set; + } = CommitLineageSearchMethod.FullLineage; public BranchSortMode LocalBranchSortMode { diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 490fd5e44..424ff4fe5 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -206,8 +206,8 @@ Tips: You can cherry-pick selected commits if the other side is HEAD (using context menu). Repository Configure COMMIT TEMPLATE - Built-in parameters: - + Built-in parameters: + ${branch_name} Current local branch name. ${files_num} Number of changed files ${files} Paths of changed files @@ -218,8 +218,8 @@ Template Name: CUSTOM ACTION Arguments: - Built-in parameters: - + Built-in parameters: + ${REPO} Repository's path ${REMOTE} Selected remote or selected branch's remote ${BRANCH} Selected branch, without ${REMOTE} part for remote branches @@ -491,6 +491,15 @@ COMMIT TIME DATE TIME GRAPH & SUBJECT + Highlighting Mode + Show All + Current Branch Only + Selected Lineage Only + Current Branch & Selected Lineage + Lineage Search Method + Ancestors Only + Descendants Only + Full Lineage SHA SELECTED {0} COMMITS SHOW COLUMNS @@ -641,6 +650,7 @@ Theme Theme Overrides Use auto-hide scrollbars + Enable hover view tracking Use fixed tab width in titlebar Use native window frame DIFF/MERGE TOOL @@ -782,6 +792,7 @@ Create Branch CLEAR NOTIFICATIONS Only highlight current branch + Highlight selected commit lineage Open as Folder Open in {0} Open in External Tools diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 71759c1af..d2a34a18d 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -211,7 +211,7 @@ 仓库配置 提交信息模板 内置变量: - + ${branch_name} 当前分支名 ${files_num} 变更文件数量 ${files} 变更文件路径列表 @@ -223,7 +223,7 @@ 自定义操作 命令行参数 : 内置变量: - + ${REPO} 仓库路径 ${REMOTE} 选中的远程仓库或选中分支所属的远程仓库 ${BRANCH} 选中的分支,对于远程分支不包含远程名 @@ -495,6 +495,15 @@ 提交时间 日期时间 路线图与主题 + 高亮模式 + 全部显示 + 仅当前分支 + 仅选中谱系 + 当前分支与选中谱系 + 谱系搜索方式 + 仅祖先 + 仅后代 + 完整谱系 提交指纹 已选中 {0} 项提交 显示列 @@ -645,6 +654,7 @@ 主题 主题自定义 允许滚动条自动隐藏 + 开启悬停视图跟踪 主标签使用固定宽度 使用系统默认窗体样式 对比/合并工具 @@ -786,6 +796,7 @@ 新建分支 清空通知列表 仅高亮显示当前分支 + 高亮显示所选提交的血缘关系 本仓库(文件夹) 在 {0} 中打开 使用外部工具打开 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index b7f884068..5136d773a 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -495,6 +495,15 @@ 提交時間 日期時間 路線圖與訊息標題 + 高亮模式 + 全部顯示 + 僅目前分支 + 僅選取譜系 + 目前分支與選取譜系 + 譜系搜尋方式 + 僅祖先 + 僅後代 + 完整譜系 提交編號 已選取 {0} 項提交 顯示欄位 @@ -645,6 +654,7 @@ 佈景主題 自訂主題 允許自動隱藏捲軸 + 開啟懸停視圖跟蹤 使用固定寬度的分頁標籤 使用系統原生預設視窗樣式 對比/合併工具 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 185fbda56..b4226ff39 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -73,6 +73,49 @@ public Models.CommitGraph Graph set => SetProperty(ref _graph, value); } + public long HoveredCommitIndex + { + get => _hoveredCommitIndex; + set + { + if (SetProperty(ref _hoveredCommitIndex, value)) + { + if (Preferences.Instance.EnableHoverViewTracking && value >= 0 && value < _commits.Count) + { + var hoveredIndex = (int)value; + var depth = 1000u; + var topLimit = -1; + var bottomLimit = -1; + + if (_visibleTopIndex >= 0 && _visibleBottomIndex >= _visibleTopIndex) + { + topLimit = _visibleTopIndex; + bottomLimit = _visibleBottomIndex; + + // Add a small guard band to keep context around viewport edges. + var dist = Math.Max(Math.Abs(hoveredIndex - topLimit), Math.Abs(bottomLimit - hoveredIndex)); + depth = (uint)Math.Max(500, dist + 32); + } + + HoveredLineageCommits = GetCommitLineage( + _commits[hoveredIndex], + Models.CommitLineageSearchMethod.FullLineage, + depth, + topLimit, + bottomLimit); + } + else + HoveredLineageCommits = null; + } + } + } + + public bool[] HoveredLineageCommits + { + get => _hoveredLineageCommits; + set => SetProperty(ref _hoveredLineageCommits, value); + } + public List SelectedCommits { get => _selectedCommits; @@ -83,6 +126,18 @@ public List SelectedCommits } } + public HashSet SelectedLineagePaths + { + get => _selectedLineagePaths; + set => SetProperty(ref _selectedLineagePaths, value); + } + + public bool[] SelectedLineageCommits + { + get => _selectedLineageCommits; + set => SetProperty(ref _selectedLineageCommits, value); + } + public object DetailContext { get => _detailContext; @@ -104,15 +159,45 @@ public Models.Branch CurrentBranch get => _repo.CurrentBranch; } - public bool HighlightCurrentBranchOnly + public Models.CommitGraphHighlighting CommitGraphHighlighting { - get => _repo.UIStates.OnlyHighlightCurrentBranchInHistory; + get => _repo.UIStates.CommitGraphHighlighting; set { - if (_repo.UIStates.OnlyHighlightCurrentBranchInHistory != value) + if (_repo.UIStates.CommitGraphHighlighting != value) { - _repo.UIStates.OnlyHighlightCurrentBranchInHistory = value; + _repo.UIStates.CommitGraphHighlighting = value; OnPropertyChanged(); + + var highlightSelected = value == Models.CommitGraphHighlighting.SelectedLineageOnly || + value == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + if (highlightSelected && _selectedCommits.Count == 1) + CalculateTargetLineage(_selectedCommits[0]); + else if (!highlightSelected) + { + SelectedLineageCommits = null; + SelectedLineagePaths = null; + } + } + } + } + + public Models.CommitLineageSearchMethod LineageSearchMethod + { + get => _repo.UIStates.LineageSearchMethod; + set + { + if (_repo.UIStates.LineageSearchMethod != value) + { + _repo.UIStates.LineageSearchMethod = value; + OnPropertyChanged(); + + if (_selectedCommits.Count == 1 && + (CommitGraphHighlighting == Models.CommitGraphHighlighting.SelectedLineageOnly || + CommitGraphHighlighting == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage)) + { + CalculateTargetLineage(_selectedCommits[0]); + } } } } @@ -253,6 +338,41 @@ public void NavigateTo(string commitSHA) .ConfigureAwait(false); } + public void SetVisibleCommitRange(int topIndex, int bottomIndex) + { + // 扩展 viewport 上下各 100 行,保证高亮连续性 + const int VIEWPORT_PADDING = 100; + int paddedTop = Math.Max(0, topIndex - VIEWPORT_PADDING); + int paddedBottom = Math.Min(_commits.Count - 1, bottomIndex + VIEWPORT_PADDING); + + // 只有当滚动超过阈值时才触发 lineage 重新计算 + const int SCROLL_THRESHOLD = 40; // 可根据实际体验调整 + bool needUpdate = false; + if (_visibleTopIndex < 0 || _visibleBottomIndex < 0) + { + needUpdate = true; + } + else if (Math.Abs(paddedTop - _visibleTopIndex) > SCROLL_THRESHOLD || + Math.Abs(paddedBottom - _visibleBottomIndex) > SCROLL_THRESHOLD) + { + needUpdate = true; + } + + if (needUpdate) + { + _visibleTopIndex = paddedTop; + _visibleBottomIndex = paddedBottom; + // 触发 hover lineage 重新计算(如果有 hover) + if (_hoveredCommitIndex >= 0 && _hoveredCommitIndex < _commits.Count) + { + // 重新赋值以触发 setter + var tmp = _hoveredCommitIndex; + _hoveredCommitIndex = -1; + HoveredCommitIndex = tmp; + } + } + } + public async Task CheckoutBranchByDecoratorAsync(Models.Decorator decorator) { if (decorator == null) @@ -401,6 +521,14 @@ public void CompareWithWorktree(Models.Commit commit) private void PostCommitsChanged() { + _commitMap.Clear(); + for (int i = 0; i < _commits.Count; i++) + { + var c = _commits[i]; + c.Index = i; + _commitMap[c.SHA] = c; + } + if (_selectedCommits.Count == 0) return; @@ -429,6 +557,40 @@ private void PostCommitsChanged() SelectedCommits = selected; } + private void CalculateTargetLineage(Models.Commit commit) + { + Task.Run(() => + { + if (commit == null) + { + Dispatcher.UIThread.Post(() => + { + SelectedLineageCommits = null; + SelectedLineagePaths = null; + }); + return; + } + + var paths = new HashSet(); + var lineage = GetCommitLineage(commit, LineageSearchMethod, 20000); + for (int i = 0; i < lineage.Length; i++) + { + if (lineage[i]) + { + var c = _commits[i]; + if (c.PathIndex >= 0) + paths.Add(c.PathIndex); + } + } + + Dispatcher.UIThread.Post(() => + { + SelectedLineageCommits = lineage; + SelectedLineagePaths = paths; + }); + }); + } + private void PostSelectedCommitsChanged() { if (_ignoreSelectionChange) @@ -438,6 +600,8 @@ private void PostSelectedCommitsChanged() { _repo.SearchCommitContext.Selected = null; DetailContext = new Models.Null(); + SelectedLineageCommits = null; + SelectedLineagePaths = null; } else if (_selectedCommits.Count == 1) { @@ -449,6 +613,11 @@ private void PostSelectedCommitsChanged() detail.Commit = c; else DetailContext = new CommitDetail(_repo, _commitDetailSharedData) { Commit = c }; + + var highlightSelected = _repo.UIStates.CommitGraphHighlighting == Models.CommitGraphHighlighting.SelectedLineageOnly || + _repo.UIStates.CommitGraphHighlighting == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + if (highlightSelected) + CalculateTargetLineage(c); } else if (_selectedCommits.Count == 2) { @@ -458,19 +627,87 @@ private void PostSelectedCommitsChanged() compare.SetTargets(_selectedCommits[1], _selectedCommits[0]); else DetailContext = new RevisionCompare(_repo, _selectedCommits[1], _selectedCommits[0]); + + SelectedLineageCommits = null; + SelectedLineagePaths = null; } else { _repo.SearchCommitContext.Selected = null; DetailContext = new Models.Count(_selectedCommits.Count); + SelectedLineageCommits = null; + SelectedLineagePaths = null; } } + public bool[] GetCommitLineage( + Models.Commit commit, + Models.CommitLineageSearchMethod method, + uint depth = 100, + int viewportTopIndex = -1, + int viewportBottomIndex = -1) + { + var active = new bool[_commits.Count]; + if (commit == null || method == Models.CommitLineageSearchMethod.None) + return active; + + active[commit.Index] = true; + + // GUI boundaries: only search within a limited range of commits to improve performance + int topLimit = Math.Max(0, commit.Index - (int)depth); + int bottomLimit = Math.Min(_commits.Count - 1, commit.Index + (int)depth); + + // Viewport boundaries: tighten the traversal window to visible rows when available. + if (viewportTopIndex >= 0 && viewportBottomIndex >= viewportTopIndex) + { + topLimit = Math.Max(topLimit, viewportTopIndex); + bottomLimit = Math.Min(bottomLimit, viewportBottomIndex); + } + + // DOWN (Descendants) - Moving to lower indices + if (method == Models.CommitLineageSearchMethod.ChildsOnly || method == Models.CommitLineageSearchMethod.FullLineage) + { + for (int i = commit.Index - 1; i >= topLimit; i--) + { + foreach (var pSha in _commits[i].Parents) + { + if (_commitMap.TryGetValue(pSha, out var parent) && parent.Index < _commits.Count && active[parent.Index]) + { + active[i] = true; + break; + } + } + } + } + + // UP (Ancestors) - Moving to higher indices + if (method == Models.CommitLineageSearchMethod.ParentsOnly || method == Models.CommitLineageSearchMethod.FullLineage) + { + for (int i = commit.Index; i <= bottomLimit; i++) + { + if (active[i]) + { + foreach (var pSha in _commits[i].Parents) + { + if (_commitMap.TryGetValue(pSha, out var parent) && parent.Index <= bottomLimit) + { + active[parent.Index] = true; + } + } + } + } + } + + return active; + } + private Repository _repo = null; private CommitDetailSharedData _commitDetailSharedData = null; private bool _isLoading = true; private List _commits = new List(); private Models.CommitGraph _graph = null; + private long _hoveredCommitIndex = -1; + private bool[] _hoveredLineageCommits = null; private List _selectedCommits = []; private Models.Bisect _bisect = null; private object _detailContext = new Models.Null(); @@ -481,5 +718,10 @@ private void PostSelectedCommitsChanged() private GridLength _topArea = new GridLength(1, GridUnitType.Star); private GridLength _bottomArea = new GridLength(1, GridUnitType.Star); private bool _isCollapseDetails = false; + private HashSet _selectedLineagePaths = null; + private bool[] _selectedLineageCommits = null; + private int _visibleTopIndex = -1; + private int _visibleBottomIndex = -1; + private Dictionary _commitMap = new Dictionary(); } } diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index da7fe3e5d..1078f349c 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -181,6 +181,12 @@ public bool UseAutoHideScrollBars set => SetProperty(ref _useAutoHideScrollBars, value); } + public bool EnableHoverViewTracking + { + get => _enableHoverViewTracking; + set => SetProperty(ref _enableHoverViewTracking, value); + } + public bool UseGitHubStyleAvatar { get => _useGitHubStyleAvatar; @@ -824,6 +830,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private int _subjectGuideLength = 50; private bool _useFixedTabWidth = true; private bool _useAutoHideScrollBars = true; + private bool _enableHoverViewTracking = true; private bool _useGitHubStyleAvatar = true; private bool _showAuthorTimeInGraph = false; private bool _showChildren = false; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index c0f8c432e..c636e504f 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -126,10 +126,10 @@ private set } } - public bool HighlightCurrentBranchOnlyInHistory + public Models.CommitGraphHighlighting CommitGraphHighlighting { - get => _histories.HighlightCurrentBranchOnly; - set => _histories.HighlightCurrentBranchOnly = value; + get => _histories.CommitGraphHighlighting; + set => _histories.CommitGraphHighlighting = value; } public string Filter diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs index 67ea8f706..ab8fda51b 100644 --- a/src/Views/CommitGraph.cs +++ b/src/Views/CommitGraph.cs @@ -24,13 +24,49 @@ public IBrush DotBrush set => SetValue(DotBrushProperty, value); } - public static readonly StyledProperty OnlyHighlightCurrentBranchProperty = - AvaloniaProperty.Register(nameof(OnlyHighlightCurrentBranch), true); + public static readonly StyledProperty HighlightModeProperty = + AvaloniaProperty.Register(nameof(HighlightMode), Models.CommitGraphHighlighting.All); - public bool OnlyHighlightCurrentBranch + public Models.CommitGraphHighlighting HighlightMode { - get => GetValue(OnlyHighlightCurrentBranchProperty); - set => SetValue(OnlyHighlightCurrentBranchProperty, value); + get => GetValue(HighlightModeProperty); + set => SetValue(HighlightModeProperty, value); + } + + public static readonly StyledProperty HoveredLineageCommitsProperty = + AvaloniaProperty.Register(nameof(HoveredLineageCommits)); + + public bool[] HoveredLineageCommits + { + get => GetValue(HoveredLineageCommitsProperty); + set => SetValue(HoveredLineageCommitsProperty, value); + } + + public static readonly StyledProperty HoveredCommitIndexProperty = + AvaloniaProperty.Register(nameof(HoveredCommitIndex), -1); + + public long HoveredCommitIndex + { + get => GetValue(HoveredCommitIndexProperty); + set => SetValue(HoveredCommitIndexProperty, value); + } + + public static readonly StyledProperty SelectedLineageCommitsProperty = + AvaloniaProperty.Register(nameof(SelectedLineageCommits)); + + public bool[] SelectedLineageCommits + { + get => GetValue(SelectedLineageCommitsProperty); + set => SetValue(SelectedLineageCommitsProperty, value); + } + + public static readonly StyledProperty> SelectedLineagePathsProperty = + AvaloniaProperty.Register>(nameof(SelectedLineagePaths)); + + public System.Collections.Generic.HashSet SelectedLineagePaths + { + get => GetValue(SelectedLineagePathsProperty); + set => SetValue(SelectedLineagePathsProperty, value); } public static readonly StyledProperty LayoutProperty = @@ -47,10 +83,53 @@ static CommitGraph() AffectsRender( GraphProperty, DotBrushProperty, - OnlyHighlightCurrentBranchProperty, + HighlightModeProperty, + HoveredLineageCommitsProperty, + HoveredCommitIndexProperty, + SelectedLineageCommitsProperty, + SelectedLineagePathsProperty, LayoutProperty); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == GraphProperty || + change.Property == HoveredCommitIndexProperty || + change.Property == HoveredLineageCommitsProperty || + change.Property == SelectedLineageCommitsProperty || + change.Property == SelectedLineagePathsProperty || + change.Property == HighlightModeProperty) + { + UpdateHoveredRelated(); + } + } + + private void UpdateHoveredRelated() + { + var graph = Graph; + if (graph == null) + return; + + foreach (var line in graph.Paths) + line.IsHoveredRelated = false; + + var hoveredLineage = HoveredLineageCommits; + if (hoveredLineage != null) + { + foreach (var line in graph.Paths) + { + if (line.StartCommitIndex >= 0 && line.EndCommitIndex >= 0 && + line.StartCommitIndex < hoveredLineage.Length && line.EndCommitIndex < hoveredLineage.Length) + { + line.IsHoveredRelated = hoveredLineage[line.StartCommitIndex] && + hoveredLineage[line.EndCommitIndex]; + } + } + } + } + public override void Render(DrawingContext context) { base.Render(context); @@ -74,8 +153,15 @@ public override void Render(DrawingContext context) private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom, double rowHeight) { + var hoverBold = 2.0; var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness); - var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; + var highlightMode = HighlightMode; + var onlyHighlightCurrentBranch = highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly || + highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + var highlightSelectedLineage = highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly || + highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + var selectedLineageCommits = SelectedLineageCommits; + var hoveredLineage = HoveredLineageCommits; foreach (var link in graph.Links) { @@ -87,10 +173,35 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double if (startY > bottom) break; - var pen = Models.CommitGraph.Pens[link.Color]; - if (onlyHighlightCurrentBranch && !link.IsMerged) + var isLinkInSelectedLineage = + highlightSelectedLineage && + selectedLineageCommits != null && + link.StartCommitIndex >= 0 && link.EndCommitIndex >= 0 && + link.StartCommitIndex < selectedLineageCommits.Length && link.EndCommitIndex < selectedLineageCommits.Length && + selectedLineageCommits[link.StartCommitIndex] && + selectedLineageCommits[link.EndCommitIndex]; + + var isLinkInHoveredLineage = hoveredLineage != null && + link.StartCommitIndex >= 0 && link.EndCommitIndex >= 0 && + link.StartCommitIndex < hoveredLineage.Length && link.EndCommitIndex < hoveredLineage.Length && + hoveredLineage[link.StartCommitIndex] && + hoveredLineage[link.EndCommitIndex]; + + var pen = link.Color < 0 ? grayedPen : Models.CommitGraph.Pens[link.Color]; + bool shouldDim = false; + if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly) + shouldDim = !link.IsMerged; + else if (highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly) + shouldDim = !isLinkInSelectedLineage; + else if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage) + shouldDim = !link.IsMerged && !isLinkInSelectedLineage; + + if (shouldDim) pen = grayedPen; + if (isLinkInHoveredLineage) + pen = new Pen(pen.Brush, pen.Thickness + hoverBold); + var geo = new StreamGeometry(); using (var ctx = geo.Open()) { @@ -112,8 +223,27 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double if (last.Y > bottom) break; + var isLineInSelectedLineage = highlightSelectedLineage && selectedLineageCommits != null && + line.StartCommitIndex >= 0 && line.EndCommitIndex >= 0 && + line.StartCommitIndex < selectedLineageCommits.Length && line.EndCommitIndex < selectedLineageCommits.Length && + selectedLineageCommits[line.StartCommitIndex] && + selectedLineageCommits[line.EndCommitIndex]; + var geo = new StreamGeometry(); - var pen = Models.CommitGraph.Pens[line.Color]; + var pen = line.Color < 0 ? grayedPen : Models.CommitGraph.Pens[line.Color]; + bool shouldDim = false; + if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly) + shouldDim = !line.IsMerged; + else if (highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly) + shouldDim = !isLineInSelectedLineage; + else if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage) + shouldDim = !line.IsMerged && !isLineInSelectedLineage; + + if (shouldDim) + pen = grayedPen; + + if (line.IsHoveredRelated) + pen = new Pen(pen.Brush, pen.Thickness + hoverBold); using (var ctx = geo.Open()) { @@ -167,10 +297,7 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double } } - if (!line.IsMerged && onlyHighlightCurrentBranch) - context.DrawGeometry(null, grayedPen, geo); - else - context.DrawGeometry(null, pen, geo); + context.DrawGeometry(null, pen, geo); } } @@ -179,10 +306,19 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl var dotFill = DotBrush; var dotFillPen = new Pen(dotFill, 2); var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness); - var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; + var highlightMode = HighlightMode; + var onlyHighlightCurrentBranch = highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly || + highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + var highlightSelectedLineage = highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly || + highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + var selectedLineageCommits = SelectedLineageCommits; + + if (DataContext is not ViewModels.Histories vm) + return; - foreach (var dot in graph.Dots) + for (int i = 0; i < graph.Dots.Count; i++) { + var dot = graph.Dots[i]; var center = new Point(dot.Center.X, dot.Center.Y * rowHeight); if (center.Y < top) @@ -190,8 +326,18 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl if (center.Y > bottom) break; + bool isDotInSelectedLineage = highlightSelectedLineage && selectedLineageCommits != null && i >= 0 && i < selectedLineageCommits.Length && selectedLineageCommits[i]; + var pen = Models.CommitGraph.Pens[dot.Color]; - if (!dot.IsMerged && onlyHighlightCurrentBranch) + bool shouldDim = false; + if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly) + shouldDim = !dot.IsMerged; + else if (highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly) + shouldDim = !isDotInSelectedLineage; + else if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage) + shouldDim = !dot.IsMerged && !isDotInSelectedLineage; + + if (shouldDim) pen = grayedPen; switch (dot.Type) @@ -213,3 +359,4 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl } } } + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index f4ff5e19a..9ec40ac6f 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -23,7 +23,7 @@ - + - + - + - + + CommitGraphHighlighting="{Binding CommitGraphHighlighting, Mode=OneWay}"> @@ -971,3 +963,4 @@ +