using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Windows; using System.Windows.Input; using System.Windows.Media; using MergeHub.Models; namespace MergeHub.Views.Controls { /// /// 파일 전체의 변경 분포를 세로 막대로 축소해 보여준다(목업 ③ 코드 미니맵). /// 클릭하면 그 위치의 줄을 요청한다 - 실제 스크롤은 화면 쪽에서 처리한다. /// public sealed class ChangeMiniMap : FrameworkElement { private const double MinTickHeight = 2.0; private static readonly Brush TrackBrush = Freeze(new SolidColorBrush(Color.FromArgb(0x22, 0x80, 0x80, 0x80))); private static readonly Pen SelectionPen = Freeze(new Pen(new SolidColorBrush(Color.FromRgb(0x2E, 0x86, 0xDE)), 1.5)); /// /// 범례·코드 패널과 같은 색을 쓰되, 미니맵은 얇아서 옅은 알파로는 안 보인다. /// 그래서 같은 색상값을 불투명하게 올려 쓴다. /// private static readonly Brush ChangedBrush = Freeze(new SolidColorBrush(Color.FromRgb(0xE0, 0xA0, 0x20))); private static readonly Brush AppliedBrush = Freeze(new SolidColorBrush(Color.FromRgb(0x2E, 0xA0, 0x43))); private static readonly Brush EditedBrush = Freeze(new SolidColorBrush(Color.FromRgb(0x9B, 0x59, 0xB6))); private static readonly Brush AddedBrush = Freeze(new SolidColorBrush(Color.FromRgb(0x2E, 0x86, 0xDE))); private static readonly Brush RemovedBrush = Freeze(new SolidColorBrush(Color.FromRgb(0xD1, 0x4B, 0x4B))); private static readonly Brush MovedBrush = Freeze(new SolidColorBrush(Color.FromRgb(0x00, 0xA0, 0xA8))); private static Brush BrushFor(LineHighlight highlight) => highlight switch { LineHighlight.Applied => AppliedBrush, LineHighlight.Edited => EditedBrush, LineHighlight.Added => AddedBrush, LineHighlight.Removed => RemovedBrush, LineHighlight.Moved => MovedBrush, _ => ChangedBrush }; private INotifyCollectionChanged? _observed; public ChangeMiniMap() { Cursor = Cursors.Hand; Width = 14; ToolTip = "변경 분포 - 클릭하면 해당 줄로 이동"; } /// 클릭한 지점에 해당하는 줄이 선택되었을 때 발생. public event EventHandler? LineRequested; public static readonly DependencyProperty SourceProperty = DependencyProperty.Register( nameof(Source), typeof(IEnumerable), typeof(ChangeMiniMap), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnSourceChanged)); public IEnumerable? Source { get => (IEnumerable?)GetValue(SourceProperty); set => SetValue(SourceProperty, value); } /// /// 줄 내용이 바뀌어도 컬렉션 자체는 그대로라 다시 그릴 계기가 없다. /// ViewModel 이 편집할 때마다 이 값을 올려서 갱신을 알린다. /// public static readonly DependencyProperty VersionProperty = DependencyProperty.Register( nameof(Version), typeof(int), typeof(ChangeMiniMap), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsRender)); public int Version { get => (int)GetValue(VersionProperty); set => SetValue(VersionProperty, value); } public static readonly DependencyProperty SelectedLineProperty = DependencyProperty.Register( nameof(SelectedLine), typeof(MergeLine), typeof(ChangeMiniMap), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)); public MergeLine? SelectedLine { get => (MergeLine?)GetValue(SelectedLineProperty); set => SetValue(SelectedLineProperty, value); } private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not ChangeMiniMap map) return; if (map._observed is not null) map._observed.CollectionChanged -= map.OnCollectionChanged; map._observed = e.NewValue as INotifyCollectionChanged; if (map._observed is not null) map._observed.CollectionChanged += map.OnCollectionChanged; map.InvalidateVisual(); } private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => InvalidateVisual(); protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); var lines = Snapshot(); if (lines.Count == 0 || ActualHeight <= 0) return; var ratio = Math.Min(1, Math.Max(0, e.GetPosition(this).Y / ActualHeight)); var index = Math.Min(lines.Count - 1, (int)(ratio * lines.Count)); LineRequested?.Invoke(this, lines[index]); e.Handled = true; } protected override void OnRender(DrawingContext dc) { var w = ActualWidth; var h = ActualHeight; if (w <= 0 || h <= 0) return; dc.DrawRectangle(TrackBrush, null, new Rect(0, 0, w, h)); var lines = Snapshot(); if (lines.Count == 0) return; var step = h / lines.Count; var tick = Math.Max(MinTickHeight, step); for (var i = 0; i < lines.Count; i++) { var highlight = lines[i].Highlight; if (highlight == LineHighlight.None || highlight == LineHighlight.Ignored) continue; // 코드 패널의 줄 배경과 같은 기준을 쓴다. // 반영한 줄은 그 즉시 초록으로 바뀌어야 하므로 IsDifferent 가 아니라 Highlight 를 본다. dc.DrawRectangle(BrushFor(highlight), null, new Rect(2, i * step, w - 4, tick)); } // 지금 보고 있는 줄 위치 var selected = SelectedLine; if (selected is null) return; var selectedIndex = lines.IndexOf(selected); if (selectedIndex < 0) return; var y = selectedIndex * step + tick / 2; dc.DrawLine(SelectionPen, new Point(0, y), new Point(w, y)); } private List Snapshot() { if (Source is null) return new List(); return Source.OfType().ToList(); } private static T Freeze(T freezable) where T : Freezable { freezable.Freeze(); return freezable; } } }