UbisamBase 플랫폼 위에서 도는 플러그인. 지금까지 만든 것을 그대로 담는다. - 폴더 비교 / 파일 비교(부분 수용·워드랩·찾기) / 3-Way 병합 / Git 조작 - 비교 엔진: histogram 정렬 · 이동 블록 감지 · 무시 규칙 · 13개 언어 토크나이저 - doc/ 에 작업 지시서와 진행 기록 빌드 산출물(bin/obj)은 추적하지 않는다 - 플랫폼 런타임이 빌드 때마다 D:\UbisamPlatform\bin 에서 복사되어 들어오므로 저장소에 담을 이유가 없다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
170 lines
6.8 KiB
C#
170 lines
6.8 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 파일 전체의 변경 분포를 세로 막대로 축소해 보여준다(목업 ③ 코드 미니맵).
|
|
/// 클릭하면 그 위치의 줄을 요청한다 - 실제 스크롤은 화면 쪽에서 처리한다.
|
|
/// </summary>
|
|
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));
|
|
|
|
/// <summary>
|
|
/// 범례·코드 패널과 같은 색을 쓰되, 미니맵은 얇아서 옅은 알파로는 안 보인다.
|
|
/// 그래서 같은 색상값을 불투명하게 올려 쓴다.
|
|
/// </summary>
|
|
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 = "변경 분포 - 클릭하면 해당 줄로 이동";
|
|
}
|
|
|
|
/// <summary>클릭한 지점에 해당하는 줄이 선택되었을 때 발생.</summary>
|
|
public event EventHandler<MergeLine>? 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 줄 내용이 바뀌어도 컬렉션 자체는 그대로라 다시 그릴 계기가 없다.
|
|
/// ViewModel 이 편집할 때마다 이 값을 올려서 갱신을 알린다.
|
|
/// </summary>
|
|
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<MergeLine> Snapshot()
|
|
{
|
|
if (Source is null) return new List<MergeLine>();
|
|
return Source.OfType<MergeLine>().ToList();
|
|
}
|
|
|
|
private static T Freeze<T>(T freezable) where T : Freezable
|
|
{
|
|
freezable.Freeze();
|
|
return freezable;
|
|
}
|
|
}
|
|
}
|