diff --git a/src/Models/RevisionFile.cs b/src/Models/RevisionFile.cs index 59868fcc..f1f5265f 100644 --- a/src/Models/RevisionFile.cs +++ b/src/Models/RevisionFile.cs @@ -10,6 +10,9 @@ namespace SourceGit.Models public class RevisionImageFile { public Bitmap Image { get; set; } = null; + public long FileSize { get; set; } = 0; + public string ImageType { get; set; } = string.Empty; + public string ImageSize => Image != null ? $"{Image.PixelSize.Width} x {Image.PixelSize.Height}" : "0 x 0"; } public class RevisionTextFile diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 95a2f5e1..c89baacf 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -176,19 +177,17 @@ namespace SourceGit.ViewModels if (IMG_EXTS.Contains(ext)) { var stream = Commands.QueryFileContent.Run(_repo.FullPath, _commit.SHA, file.Path); - var bitmap = stream.Length > 0 ? new Bitmap(stream) : null; - Dispatcher.UIThread.Invoke(() => - { - ViewRevisionFileContent = new Models.RevisionImageFile() { Image = bitmap }; - }); + var fileSize = stream.Length; + var bitmap = fileSize > 0 ? new Bitmap(stream) : null; + var imageType = Path.GetExtension(file.Path).TrimStart('.').ToUpper(CultureInfo.CurrentCulture); + var image = new Models.RevisionImageFile() { Image = bitmap, FileSize = fileSize, ImageType = imageType }; + Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = image); } else { var size = new Commands.QueryFileSize(_repo.FullPath, file.Path, _commit.SHA).Result(); - Dispatcher.UIThread.Invoke(() => - { - ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size }; - }); + var binary = new Models.RevisionBinaryFile() { Size = size }; + Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = binary); } return; @@ -202,7 +201,6 @@ namespace SourceGit.ViewModels var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() }; obj.Object.Oid = matchLFS.Groups[1].Value; obj.Object.Size = long.Parse(matchLFS.Groups[2].Value); - Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = obj); } else @@ -220,14 +218,8 @@ namespace SourceGit.ViewModels if (commit != null) { var body = new Commands.QueryCommitFullMessage(submoduleRoot, file.SHA).Result(); - Dispatcher.UIThread.Invoke(() => - { - ViewRevisionFileContent = new Models.RevisionSubmodule() - { - Commit = commit, - FullMessage = body, - }; - }); + var submodule = new Models.RevisionSubmodule() { Commit = commit, FullMessage = body }; + Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = submodule); } else { diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 398293a3..9563896b 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -120,8 +121,10 @@ namespace SourceGit.ViewModels if (IMG_EXTS.Contains(ext)) { var stream = Commands.QueryFileContent.Run(_repo.FullPath, _selectedCommit.SHA, _file); - var bitmap = stream.Length > 0 ? new Bitmap(stream) : null; - var image = new Models.RevisionImageFile() { Image = bitmap }; + var fileSize = stream.Length; + var bitmap = fileSize > 0 ? new Bitmap(stream) : null; + var imageType = Path.GetExtension(_file).TrimStart('.').ToUpper(CultureInfo.CurrentCulture); + var image = new Models.RevisionImageFile() { Image = bitmap, FileSize = fileSize, ImageType = imageType }; Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, image)); } else diff --git a/src/Views/ImageContainer.cs b/src/Views/ImageContainer.cs new file mode 100644 index 00000000..aecea0b2 --- /dev/null +++ b/src/Views/ImageContainer.cs @@ -0,0 +1,344 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Styling; + +namespace SourceGit.Views +{ + public class ImageContainer : Control + { + public override void Render(DrawingContext context) + { + if (_bgBrush == null) + { + var maskBrush = new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? 0xFF404040 : 0xFFBBBBBB); + var bg = new DrawingGroup() + { + Children = + { + new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(0, 0, 12, 12)) }, + new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(12, 12, 12, 12)) }, + } + }; + + _bgBrush = new DrawingBrush(bg) + { + AlignmentX = AlignmentX.Left, + AlignmentY = AlignmentY.Top, + DestinationRect = new RelativeRect(new Size(24, 24), RelativeUnit.Absolute), + Stretch = Stretch.None, + TileMode = TileMode.Tile, + }; + } + + context.FillRectangle(_bgBrush, new Rect(Bounds.Size)); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property.Name == "ActualThemeVariant") + { + _bgBrush = null; + InvalidateVisual(); + } + } + + private DrawingBrush _bgBrush = null; + } + + public class ImageView : ImageContainer + { + public static readonly StyledProperty ImageProperty = + AvaloniaProperty.Register(nameof(Image)); + + public Bitmap Image + { + get => GetValue(ImageProperty); + set => SetValue(ImageProperty, value); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (Image is { } image) + context.DrawImage(image, new Rect(0, 0, Bounds.Width, Bounds.Height)); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ImageProperty) + InvalidateMeasure(); + } + + protected override Size MeasureOverride(Size availableSize) + { + if (Image is { } image) + { + var imageSize = image.Size; + var scaleW = availableSize.Width / imageSize.Width; + var scaleH = availableSize.Height / imageSize.Height; + var scale = Math.Min(scaleW, scaleH); + return new Size(scale * imageSize.Width, scale * imageSize.Height); + } + + return availableSize; + } + } + + public class ImageSwipeControl : ImageContainer + { + public static readonly StyledProperty AlphaProperty = + AvaloniaProperty.Register(nameof(Alpha), 0.5); + + public double Alpha + { + get => GetValue(AlphaProperty); + set => SetValue(AlphaProperty, value); + } + + public static readonly StyledProperty OldImageProperty = + AvaloniaProperty.Register(nameof(OldImage)); + + public Bitmap OldImage + { + get => GetValue(OldImageProperty); + set => SetValue(OldImageProperty, value); + } + + public static readonly StyledProperty NewImageProperty = + AvaloniaProperty.Register(nameof(NewImage)); + + public Bitmap NewImage + { + get => GetValue(NewImageProperty); + set => SetValue(NewImageProperty, value); + } + + static ImageSwipeControl() + { + AffectsMeasure(OldImageProperty, NewImageProperty); + AffectsRender(AlphaProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var alpha = Alpha; + var w = Bounds.Width; + var h = Bounds.Height; + var x = w * alpha; + var left = OldImage; + if (left != null && alpha > 0) + { + var src = new Rect(0, 0, left.Size.Width * alpha, left.Size.Height); + var dst = new Rect(0, 0, x, h); + context.DrawImage(left, src, dst); + } + + var right = NewImage; + if (right != null && alpha < 1) + { + var src = new Rect(right.Size.Width * alpha, 0, right.Size.Width * (1 - alpha), right.Size.Height); + var dst = new Rect(x, 0, w - x, h); + context.DrawImage(right, src, dst); + } + + context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x, 0), new Point(x, Bounds.Height)); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + var p = e.GetPosition(this); + var hitbox = new Rect(Math.Max(Bounds.Width * Alpha - 2, 0), 0, 4, Bounds.Height); + var pointer = e.GetCurrentPoint(this); + if (pointer.Properties.IsLeftButtonPressed && hitbox.Contains(p)) + { + _pressedOnSlider = true; + Cursor = new Cursor(StandardCursorType.SizeWestEast); + e.Pointer.Capture(this); + e.Handled = true; + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + _pressedOnSlider = false; + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + var w = Bounds.Width; + var p = e.GetPosition(this); + + if (_pressedOnSlider) + { + SetCurrentValue(AlphaProperty, Math.Clamp(p.X, 0, w) / w); + } + else + { + var hitbox = new Rect(Math.Max(w * Alpha - 2, 0), 0, 4, Bounds.Height); + if (hitbox.Contains(p)) + { + if (!_lastInSlider) + { + _lastInSlider = true; + Cursor = new Cursor(StandardCursorType.SizeWestEast); + } + } + else + { + if (_lastInSlider) + { + _lastInSlider = false; + Cursor = null; + } + } + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var left = OldImage; + var right = NewImage; + + if (left == null) + return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); + + if (right == null) + return GetDesiredSize(left.Size, availableSize); + + var ls = GetDesiredSize(left.Size, availableSize); + var rs = GetDesiredSize(right.Size, availableSize); + return ls.Width > rs.Width ? ls : rs; + } + + private Size GetDesiredSize(Size img, Size available) + { + var sw = available.Width / img.Width; + var sh = available.Height / img.Height; + var scale = Math.Min(sw, sh); + return new Size(scale * img.Width, scale * img.Height); + } + + private bool _pressedOnSlider = false; + private bool _lastInSlider = false; + } + + public class ImageBlendControl : ImageContainer + { + public static readonly StyledProperty AlphaProperty = + AvaloniaProperty.Register(nameof(Alpha), 1.0); + + public double Alpha + { + get => GetValue(AlphaProperty); + set => SetValue(AlphaProperty, value); + } + + public static readonly StyledProperty OldImageProperty = + AvaloniaProperty.Register(nameof(OldImage)); + + public Bitmap OldImage + { + get => GetValue(OldImageProperty); + set => SetValue(OldImageProperty, value); + } + + public static readonly StyledProperty NewImageProperty = + AvaloniaProperty.Register(nameof(NewImage)); + + public Bitmap NewImage + { + get => GetValue(NewImageProperty); + set => SetValue(NewImageProperty, value); + } + + static ImageBlendControl() + { + AffectsMeasure(OldImageProperty, NewImageProperty); + AffectsRender(AlphaProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); + var alpha = Alpha; + var left = OldImage; + var right = NewImage; + var drawLeft = left != null && alpha < 1.0; + var drawRight = right != null && alpha > 0; + + if (drawLeft && drawRight) + { + using (var rt = new RenderTargetBitmap(right.PixelSize, right.Dpi)) + { + var rtRect = new Rect(rt.Size); + using (var dc = rt.CreateDrawingContext()) + { + using (dc.PushRenderOptions(RO_SRC)) + using (dc.PushOpacity(1 - alpha)) + dc.DrawImage(left, rtRect); + + using (dc.PushRenderOptions(RO_DST)) + using (dc.PushOpacity(alpha)) + dc.DrawImage(right, rtRect); + } + + context.DrawImage(rt, rtRect, rect); + } + } + else if (drawLeft) + { + using (context.PushOpacity(1 - alpha)) + context.DrawImage(left, rect); + } + else if (drawRight) + { + using (context.PushOpacity(alpha)) + context.DrawImage(right, rect); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var left = OldImage; + var right = NewImage; + + if (left == null) + return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); + + if (right == null) + return GetDesiredSize(left.Size, availableSize); + + var ls = GetDesiredSize(left.Size, availableSize); + var rs = GetDesiredSize(right.Size, availableSize); + return ls.Width > rs.Width ? ls : rs; + } + + private Size GetDesiredSize(Size img, Size available) + { + var sw = available.Width / img.Width; + var sh = available.Height / img.Height; + var scale = Math.Min(sw, sh); + return new Size(scale * img.Width, scale * img.Height); + } + + private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; + private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Plus, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; + } +} diff --git a/src/Views/ImageDiffView.axaml b/src/Views/ImageDiffView.axaml index 43cc3d6a..f388e976 100644 --- a/src/Views/ImageDiffView.axaml +++ b/src/Views/ImageDiffView.axaml @@ -28,17 +28,14 @@ - + - - - - + @@ -51,17 +48,14 @@ - + - - - - + @@ -96,9 +90,9 @@ - + diff --git a/src/Views/ImageDiffView.axaml.cs b/src/Views/ImageDiffView.axaml.cs index bf13e7ba..bc516b5a 100644 --- a/src/Views/ImageDiffView.axaml.cs +++ b/src/Views/ImageDiffView.axaml.cs @@ -5,300 +5,12 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Styling; namespace SourceGit.Views { - public class ImageContainer : Control - { - public override void Render(DrawingContext context) - { - if (_bgBrush == null) - { - var maskBrush = new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? 0xFF404040 : 0xFFBBBBBB); - var bg = new DrawingGroup() - { - Children = - { - new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(0, 0, 12, 12)) }, - new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(12, 12, 12, 12)) }, - } - }; + - _bgBrush = new DrawingBrush(bg) - { - AlignmentX = AlignmentX.Left, - AlignmentY = AlignmentY.Top, - DestinationRect = new RelativeRect(new Size(24, 24), RelativeUnit.Absolute), - Stretch = Stretch.None, - TileMode = TileMode.Tile, - }; - } - - context.FillRectangle(_bgBrush, new Rect(Bounds.Size)); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property.Name == "ActualThemeVariant") - { - _bgBrush = null; - InvalidateVisual(); - } - } - - private DrawingBrush _bgBrush = null; - } - - public class ImagesSwipeControl : ImageContainer - { - public static readonly StyledProperty AlphaProperty = - AvaloniaProperty.Register(nameof(Alpha), 0.5); - - public double Alpha - { - get => GetValue(AlphaProperty); - set => SetValue(AlphaProperty, value); - } - - public static readonly StyledProperty OldImageProperty = - AvaloniaProperty.Register(nameof(OldImage)); - - public Bitmap OldImage - { - get => GetValue(OldImageProperty); - set => SetValue(OldImageProperty, value); - } - - public static readonly StyledProperty NewImageProperty = - AvaloniaProperty.Register(nameof(NewImage)); - - public Bitmap NewImage - { - get => GetValue(NewImageProperty); - set => SetValue(NewImageProperty, value); - } - - static ImagesSwipeControl() - { - AffectsMeasure(OldImageProperty, NewImageProperty); - AffectsRender(AlphaProperty); - } - - public override void Render(DrawingContext context) - { - base.Render(context); - - var alpha = Alpha; - var w = Bounds.Width; - var h = Bounds.Height; - var x = w * alpha; - var left = OldImage; - if (left != null && alpha > 0) - { - var src = new Rect(0, 0, left.Size.Width * alpha, left.Size.Height); - var dst = new Rect(0, 0, x, h); - context.DrawImage(left, src, dst); - } - - var right = NewImage; - if (right != null && alpha < 1) - { - var src = new Rect(right.Size.Width * alpha, 0, right.Size.Width * (1 - alpha), right.Size.Height); - var dst = new Rect(x, 0, w - x, h); - context.DrawImage(right, src, dst); - } - - context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x, 0), new Point(x, Bounds.Height)); - } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - base.OnPointerPressed(e); - - var p = e.GetPosition(this); - var hitbox = new Rect(Math.Max(Bounds.Width * Alpha - 2, 0), 0, 4, Bounds.Height); - var pointer = e.GetCurrentPoint(this); - if (pointer.Properties.IsLeftButtonPressed && hitbox.Contains(p)) - { - _pressedOnSlider = true; - Cursor = new Cursor(StandardCursorType.SizeWestEast); - e.Pointer.Capture(this); - e.Handled = true; - } - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - base.OnPointerReleased(e); - _pressedOnSlider = false; - } - - protected override void OnPointerMoved(PointerEventArgs e) - { - var w = Bounds.Width; - var p = e.GetPosition(this); - - if (_pressedOnSlider) - { - SetCurrentValue(AlphaProperty, Math.Clamp(p.X, 0, w) / w); - } - else - { - var hitbox = new Rect(Math.Max(w * Alpha - 2, 0), 0, 4, Bounds.Height); - if (hitbox.Contains(p)) - { - if (!_lastInSlider) - { - _lastInSlider = true; - Cursor = new Cursor(StandardCursorType.SizeWestEast); - } - } - else - { - if (_lastInSlider) - { - _lastInSlider = false; - Cursor = null; - } - } - } - } - - protected override Size MeasureOverride(Size availableSize) - { - var left = OldImage; - var right = NewImage; - - if (left == null) - return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); - - if (right == null) - return GetDesiredSize(left.Size, availableSize); - - var ls = GetDesiredSize(left.Size, availableSize); - var rs = GetDesiredSize(right.Size, availableSize); - return ls.Width > rs.Width ? ls : rs; - } - - private Size GetDesiredSize(Size img, Size available) - { - var sw = available.Width / img.Width; - var sh = available.Height / img.Height; - var scale = Math.Min(sw, sh); - return new Size(scale * img.Width, scale * img.Height); - } - - private bool _pressedOnSlider = false; - private bool _lastInSlider = false; - } - - public class ImageBlendControl : ImageContainer - { - public static readonly StyledProperty AlphaProperty = - AvaloniaProperty.Register(nameof(Alpha), 1.0); - - public double Alpha - { - get => GetValue(AlphaProperty); - set => SetValue(AlphaProperty, value); - } - - public static readonly StyledProperty OldImageProperty = - AvaloniaProperty.Register(nameof(OldImage)); - - public Bitmap OldImage - { - get => GetValue(OldImageProperty); - set => SetValue(OldImageProperty, value); - } - - public static readonly StyledProperty NewImageProperty = - AvaloniaProperty.Register(nameof(NewImage)); - - public Bitmap NewImage - { - get => GetValue(NewImageProperty); - set => SetValue(NewImageProperty, value); - } - - static ImageBlendControl() - { - AffectsMeasure(OldImageProperty, NewImageProperty); - AffectsRender(AlphaProperty); - } - - public override void Render(DrawingContext context) - { - base.Render(context); - - var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); - var alpha = Alpha; - var left = OldImage; - var right = NewImage; - var drawLeft = left != null && alpha < 1.0; - var drawRight = right != null && alpha > 0; - - if (drawLeft && drawRight) - { - using (var rt = new RenderTargetBitmap(right.PixelSize, right.Dpi)) - { - var rtRect = new Rect(rt.Size); - using (var dc = rt.CreateDrawingContext()) - { - using (dc.PushRenderOptions(RO_SRC)) - using (dc.PushOpacity(1 - alpha)) - dc.DrawImage(left, rtRect); - - using (dc.PushRenderOptions(RO_DST)) - using (dc.PushOpacity(alpha)) - dc.DrawImage(right, rtRect); - } - - context.DrawImage(rt, rtRect, rect); - } - } - else if (drawLeft) - { - using (context.PushOpacity(1 - alpha)) - context.DrawImage(left, rect); - } - else if (drawRight) - { - using (context.PushOpacity(alpha)) - context.DrawImage(right, rect); - } - } - - protected override Size MeasureOverride(Size availableSize) - { - var left = OldImage; - var right = NewImage; - - if (left == null) - return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); - - if (right == null) - return GetDesiredSize(left.Size, availableSize); - - var ls = GetDesiredSize(left.Size, availableSize); - var rs = GetDesiredSize(right.Size, availableSize); - return ls.Width > rs.Width ? ls : rs; - } - - private Size GetDesiredSize(Size img, Size available) - { - var sw = available.Width / img.Width; - var sh = available.Height / img.Height; - var scale = Math.Min(sw, sh); - return new Size(scale * img.Width, scale * img.Height); - } - - private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; - private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Plus, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; - } + public partial class ImageDiffView : UserControl { diff --git a/src/Views/RevisionFileContentViewer.axaml b/src/Views/RevisionFileContentViewer.axaml index 67ba5913..2084a8b2 100644 --- a/src/Views/RevisionFileContentViewer.axaml +++ b/src/Views/RevisionFileContentViewer.axaml @@ -23,16 +23,25 @@ - - - - - - - + + + + + + - + + + + + + + + + + +