From eff6cc0770c4f95e9c7c93972e3a7d491f228bc0 Mon Sep 17 00:00:00 2001 From: Philippe Miossec Date: Fri, 29 Mar 2019 15:18:48 +0100 Subject: [PATCH] Blame: Display avatars --- GitUI/Avatars/AvatarService.cs | 2 +- GitUI/Editor/BlameAuthorMargin.cs | 78 +++++++++++++++++++ GitUI/Editor/FileViewer.cs | 8 ++ GitUI/Editor/FileViewerInternal.cs | 15 ++++ GitUI/GitUI.csproj | 1 + GitUI/UserControls/BlameControl.cs | 34 +++++++- .../UserControls/BlameControlTests.cs | 4 +- 7 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 GitUI/Editor/BlameAuthorMargin.cs diff --git a/GitUI/Avatars/AvatarService.cs b/GitUI/Avatars/AvatarService.cs index 31c02fcd834..895208c22a6 100644 --- a/GitUI/Avatars/AvatarService.cs +++ b/GitUI/Avatars/AvatarService.cs @@ -4,7 +4,7 @@ namespace GitUI.Avatars { public static class AvatarService { - private static InitialsAvatarGenerator AvatarGenerator = new InitialsAvatarGenerator(); + private static readonly InitialsAvatarGenerator AvatarGenerator = new InitialsAvatarGenerator(); public static IAvatarProvider Default { get; } = new BackupAvatarProvider(new AvatarMemoryCache(new AvatarPersistentCache(new AvatarDownloader(AvatarGenerator), AvatarGenerator)), Images.User80); diff --git a/GitUI/Editor/BlameAuthorMargin.cs b/GitUI/Editor/BlameAuthorMargin.cs new file mode 100644 index 00000000000..b77c269e778 --- /dev/null +++ b/GitUI/Editor/BlameAuthorMargin.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using GitExtUtils.GitUI; +using ICSharpCode.TextEditor; +using Microsoft.VisualStudio.Threading; + +namespace GitUI.Editor +{ + /// + /// This class display avatars in the gutter in a blame. + /// + public class BlameAuthorMargin : AbstractMargin + { + private List _avatars; + private readonly int _lineHeight; + private readonly Color _backgroundColor; + + public BlameAuthorMargin(TextArea textArea) : base(textArea) + { + _lineHeight = textArea.Font.Height + 1; + _backgroundColor = ((SolidBrush)SystemBrushes.Window).Color; + } + + public void SetAvatars(List> avatars) + { + _avatars = avatars.Select(a => ThreadHelper.JoinableTaskFactory.Run(async () => await a)).ToList(); + + // Update the resolution otherwise the image is not drawn at the good size :( + foreach (var avatar in _avatars) + { + if (avatar is Bitmap bitmapAvatar) + { + bitmapAvatar.SetResolution(DpiUtil.DpiX, DpiUtil.DpiY); + } + } + } + + public override int Width => _lineHeight; + + public override bool IsVisible => true; + + public override void Paint(Graphics g, Rectangle rect) + { + if (rect.Width <= 0 || rect.Height <= 0) + { + return; + } + + g.Clear(_backgroundColor); + + if (_avatars == null || _avatars.Count == 0) + { + return; + } + + var verticalOffset = textArea.VirtualTop.Y; + var lineStart = verticalOffset / _lineHeight; + var negativeOffset = (lineStart * _lineHeight) - verticalOffset; + var lineCount = (int)rect.Height / _lineHeight; + + for (int i = 0; i < lineCount; i++) + { + if (lineStart + i >= _avatars.Count) + { + break; + } + + if (_avatars[lineStart + i] != null) + { + g.DrawImage(_avatars[lineStart + i], new Point(0, negativeOffset + (i * _lineHeight))); + } + } + + base.Paint(g, rect); + } + } +} diff --git a/GitUI/Editor/FileViewer.cs b/GitUI/Editor/FileViewer.cs index 68a80187250..f07cf1ad7b9 100644 --- a/GitUI/Editor/FileViewer.cs +++ b/GitUI/Editor/FileViewer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.IO; @@ -17,7 +18,9 @@ using GitUI.Hotkey; using GitUI.Properties; using GitUIPluginInterfaces; +using ICSharpCode.TextEditor; using JetBrains.Annotations; +using Microsoft.VisualStudio.Threading; using ResourceManager; namespace GitUI.Editor @@ -1517,5 +1520,10 @@ public IgnoreWhitespaceKind IgnoreWhitespace public ToolStripButton IgnoreAllWhitespacesButton => _fileViewer.ignoreAllWhitespaces; public ToolStripMenuItem IgnoreAllWhitespacesMenuItem => _fileViewer.ignoreAllWhitespaceChangesToolStripMenuItem; } + + public void SetGutterAvatars(List> avatars) + { + internalFileViewer.SetGutterAvatars(avatars); + } } } diff --git a/GitUI/Editor/FileViewerInternal.cs b/GitUI/Editor/FileViewerInternal.cs index bc1bc140174..4c361139f3b 100644 --- a/GitUI/Editor/FileViewerInternal.cs +++ b/GitUI/Editor/FileViewerInternal.cs @@ -1,12 +1,15 @@ using System; +using System.Collections.Generic; using System.Drawing; using System.Threading.Tasks; using System.Windows.Forms; using GitCommands; using GitExtUtils.GitUI; using GitUI.Editor.Diff; +using GitUI.Properties; using ICSharpCode.TextEditor; using ICSharpCode.TextEditor.Document; +using Microsoft.VisualStudio.Threading; namespace GitUI.Editor { @@ -28,6 +31,7 @@ public partial class FileViewerInternal : GitModuleControl, IFileViewer private readonly CurrentViewPositionCache _currentViewPositionCache; private DiffViewerLineNumberControl _lineNumbersControl; private DiffHighlightService _diffHighlightService = DiffHighlightService.Instance; + private BlameAuthorMargin _authorsAvatarMargin; public FileViewerInternal() { @@ -547,5 +551,16 @@ public TestAccessor(FileViewerInternal control) public TextEditorControl TextEditor => _control.TextEditor; } + + public void SetGutterAvatars(List> avatars) + { + if (_authorsAvatarMargin == null) + { + _authorsAvatarMargin = new BlameAuthorMargin(TextEditor.ActiveTextAreaControl.TextArea); + TextEditor.ActiveTextAreaControl.TextArea.InsertLeftMargin(0, _authorsAvatarMargin); + } + + _authorsAvatarMargin.SetAvatars(avatars); + } } } \ No newline at end of file diff --git a/GitUI/GitUI.csproj b/GitUI/GitUI.csproj index 8200be4be2a..d8ead9a07be 100644 --- a/GitUI/GitUI.csproj +++ b/GitUI/GitUI.csproj @@ -332,6 +332,7 @@ + diff --git a/GitUI/UserControls/BlameControl.cs b/GitUI/UserControls/BlameControl.cs index 20a59545af4..8d9cf49e768 100644 --- a/GitUI/UserControls/BlameControl.cs +++ b/GitUI/UserControls/BlameControl.cs @@ -1,18 +1,23 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.Drawing.Imaging; using System.Globalization; using System.Linq; using System.Text; +using System.Threading.Tasks; using System.Windows.Forms; using GitCommands; using GitExtUtils; +using GitUI.Avatars; using GitUI.BranchTreePanel; using GitUI.CommitInfo; using GitUI.Editor; using GitUI.HelperDialogs; using GitUIPluginInterfaces; +using ICSharpCode.TextEditor; using JetBrains.Annotations; +using Microsoft.VisualStudio.Threading; namespace GitUI.Blame { @@ -268,7 +273,10 @@ private void BlameFile_VScrollPositionChanged(object sender, EventArgs e) private void ProcessBlame(string filename, GitRevision revision, IReadOnlyList children, Control controlToMask, int lineNumber, int scrollpos) { - var (gutter, body) = BuildBlameContents(filename); + var avatarSize = BlameAuthor.Font.Height + 1; + var (gutter, body, avatars) = BuildBlameContents(filename, avatarSize); + + BlameAuthor.SetGutterAvatars(avatars); ThreadHelper.JoinableTaskFactory.RunAsync( () => BlameAuthor.ViewTextAsync("committer.txt", gutter)); @@ -293,7 +301,8 @@ private void ProcessBlame(string filename, GitRevision revision, IReadOnlyList> avatars) BuildBlameContents(string filename, + int avatarSize) { var body = new StringBuilder(capacity: 4096); @@ -320,14 +329,31 @@ private void ProcessBlame(string filename, GitRevision revision, IReadOnlyList> avatars = new List>(); + Dictionary> cacheAvatars = new Dictionary>(); + var noAvatar = ThreadHelper.JoinableTaskFactory.RunAsync(() => Task.FromResult(null)); foreach (var line in _blame.Lines) { if (line.Commit == lastCommit) { + avatars.Add(noAvatar); gutter.AppendLine(emptyLine); } else { + var authorEmail = line.Commit.AuthorMail.Trim('<', '>'); + if (cacheAvatars.ContainsKey(authorEmail)) + { + avatars.Add(cacheAvatars[authorEmail]); + } + else + { + var avatar = ThreadHelper.JoinableTaskFactory.RunAsync(async () => await AvatarService.Default.GetAvatarAsync(authorEmail, line.Commit.Author, + avatarSize)); + cacheAvatars.Add(authorEmail, avatar); + avatars.Add(avatar); + } + BuildAuthorLine(line, lineBuilder, dateTimeFormat, filename, AppSettings.BlameShowAuthor, AppSettings.BlameShowAuthorDate, AppSettings.BlameShowOriginalFilePath, AppSettings.BlameDisplayAuthorFirst); gutter.Append(lineBuilder); @@ -340,7 +366,7 @@ private void ProcessBlame(string filename, GitRevision revision, IReadOnlyList _control.BuildAuthorLine(line, lineBuilder, dateTimeFormat, filename, showAuthor, showAuthorDate, showOriginalFilePath, displayAuthorFirst); - public (string gutter, string body) BuildBlameContents(string filename) => _control.BuildBlameContents(filename); + public (string gutter, string body, List> avatars) BuildBlameContents(string filename) => _control.BuildBlameContents(filename, 10); } } } diff --git a/UnitTests/GitUITests/UserControls/BlameControlTests.cs b/UnitTests/GitUITests/UserControls/BlameControlTests.cs index 06e314e3e46..5ca21683ca9 100644 --- a/UnitTests/GitUITests/UserControls/BlameControlTests.cs +++ b/UnitTests/GitUITests/UserControls/BlameControlTests.cs @@ -94,7 +94,7 @@ public void BuildBlameContents_WithDateAndTime() { AppSettings.BlameShowAuthorTime = true; - var (gutter, content) = _sut.BuildBlameContents("fileName.txt"); + var (gutter, content, _) = _sut.BuildBlameContents("fileName.txt"); content.Should().Be($"line1{Environment.NewLine}line2{Environment.NewLine}line3{Environment.NewLine}line4{Environment.NewLine}"); var gutterLines = gutter.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); @@ -123,7 +123,7 @@ public void BuildBlameContents_WithDateButNotTime() AppSettings.BlameShowAuthorTime = false; // When - var (gutter, content) = _sut.BuildBlameContents("fileName.txt"); + var (gutter, content, _) = _sut.BuildBlameContents("fileName.txt"); // Then content.Should().Be($"line1{Environment.NewLine}line2{Environment.NewLine}line3{Environment.NewLine}line4{Environment.NewLine}");