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;
}
}
}