4차 C-4: 색 구분 범례를 이동 도구로

개수를 누르면 그 갈래 줄로 가고 다시 누르면 다음 줄로 간다. 차이 이동과 같은 방식이다.
0건은 흐리게만 하고 숨기지 않는다. 자리가 움직이면 다시 찾아야 한다.
This commit is contained in:
김승준 2026-09-10 22:18:06 +09:00
parent 06d018742a
commit bc92676a49
6 changed files with 242 additions and 42 deletions

View File

@ -465,3 +465,42 @@ Gitea 계정 · 토큰 · Fetch · Pull · Push 가 세 줄로 있었다. **원
- **글자가 무슨 일이 나는지 말한다** - `▸ 원격 저장소 · 계정 펴기` / `▾ … 접기`.
무엇이 숨어 있고 누르면 어떻게 되는지가 한 줄에 다 있다.
- **누르는 것으로 읽히게** - 12px 굵게, 강조색, 손 모양 커서, 폭을 190 으로.
---
## C-4. 색 구분 범례를 읽는 것에서 쓰는 것으로 — **완료** (2026-09-10)
**무엇이 문제였나** - 범례 여덟 칸이 늘 한 줄을 차지하는데 <b>읽기만 하는 설명</b>이었다.
0건인 갈래도 있는 갈래와 똑같은 무게로 자리를 잡고 있었다.
**두 가지를 했다.**
**① 0건은 흐리게.** `ZeroToDimConverter` 로 투명도를 0.35 로 낮춘다.
<b>숨기지 않는 까닭</b> - 항목이 나타났다 사라지면 자리가 매번 움직여 같은 것을 다시 찾아야 한다.
자리는 그대로 두고 무게만 뺀다. 있는 것과 없는 것이 한눈에 갈린다.
**② 개수를 누르면 그 갈래로 간다.** `추가 1` 을 누르면 추가된 줄로 가고, 다시 누르면 다음 추가 줄로 간다.
"추가된 줄만 훑고 싶다"는 일이 잦은데 차이 이동(`Alt+↑↓`)은 갈래를 가리지 않는다.
이제 <b>색 이름을 외울 필요도 없다</b> - 색을 보고 그 자리를 누르면 된다.
지금 자리 다음부터 찾고 끝에 닿으면 처음으로 돌아온다. **차이 이동과 같은 방식이라 새로 외울 규칙이 없다.**
한 줄도 없으면 조용히 넘어가지 않고 `삭제 줄이 없습니다` 로 알린다 -
아무 일도 안 일어나면 기능이 없는 줄 안다.
**단추처럼 보이지 않게 했다.** 테두리와 배경을 지우고 손 모양 커서와 마우스 올렸을 때의 옅은 배경만 남겼다.
범례 줄이 단추 여덟 개로 보이면 시끄럽다.
**화면에서 확인한 것** (차이 2줄 · 추가 1줄인 파일)
| 누른 것 | 캐럿 |
|---|---|
| 시작 | 1행 |
| `추가 1` | **7행** (추가된 줄) |
| `BASE와 다름 2` | **12행** |
| 다시 | **5행** |
| 또 다시 | **12행** (한 바퀴 돌아옴) |
| `삭제 0` | 그대로 · "삭제 줄이 없습니다" |
0건 갈래(`머지 반영함` · `직접 수정` · `삭제` · `무시` · `자리 옮김`)가 눈에 띄게 흐리다.
검사 571건 통과 · 빌드 경고 0 · 오류 0.

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
| 2026-09-10 | **4차 C-4** — 범례를 눌러 그 갈래로 이동. 0건은 흐리게 | 아래 |
| 2026-09-10 | **4차 C-3** — Git 탭의 원격 칸을 접을 수 있게. 로컬 경로 한 줄만 남고 세로 128px 가 목록으로 간다 | 아래 |
| 2026-09-10 | **엔터 고침의 되돌이 문제** — TextChanged 안에서 편집기 글을 갈아 끼워 캐럿이 엉켰다. 한 차례 뒤로 물림 | 아래 |
| 2026-09-10 | **엔터 반응 지연** — 줄 수가 바뀌면 0.6초 타이머만 기다리느라 화면이 비어 있었다. 그 자리에서 다시 맞추게 고침 | 아래 |
@ -37,6 +38,7 @@
| 날짜 | 문서 | 내용 |
|---|---|---|
| 2026-09-10 | **4차 C-4** — 범례를 눌러 그 갈래로 이동. 0건은 흐리게 | 아래 |
| 2026-09-10 | **4차 C-3** — Git 탭의 원격 칸을 접을 수 있게. 로컬 경로 한 줄만 남고 세로 128px 가 목록으로 간다 | 아래 |
| 2026-09-10 | **엔터 고침의 되돌이 문제** — TextChanged 안에서 편집기 글을 갈아 끼워 캐럿이 엉켰다. 한 차례 뒤로 물림 | 아래 |
| 2026-09-10 | **엔터 반응 지연** — 줄 수가 바뀌면 0.6초 타이머만 기다리느라 화면이 비어 있었다. 그 자리에서 다시 맞추게 고침 | 아래 |

View File

@ -41,7 +41,7 @@
| **완료** | C-1 같은 경로가 세 군데 나온다 |
| **완료** | C-2 아래쪽 정보가 세 군데로 흩어져 있다 |
| **완료** | C-3 Git 탭의 원격 블록을 접는다 |
| 대기 | C-4 색 구분 범례를 이동 도구로 |
| **완료** | C-4 색 구분 범례를 이동 도구로 |
| 대기 | C-5 ACTIONS 칸을 자주 쓰는 것과 한 번 정하는 것으로 나눈다 |
### D. 변경 이력을 전용 폴더로

View File

@ -2795,6 +2795,48 @@ namespace MergeHub.ViewModels
/// 줄 하나만 반영해도 곧바로 범례 숫자가 바뀌도록, 편집이 끝날 때마다 다시 센다.
/// (UpdateSummaries 가 AfterEdit / 파일 로드 양쪽에서 호출된다)
/// </summary>
/// <summary>
/// 범례에서 고른 갈래의 <b>다음 줄</b>로 옮긴다.
///
/// 범례는 지금까지 읽기만 하는 설명이었다. 그런데 "추가된 줄만 훑고 싶다" 는 일이 잦은데
/// 차이 이동(Alt+↑↓)은 갈래를 가리지 않는다. 개수를 누르면 그 갈래만 딛게 해서
/// <b>범례를 도구로</b> 바꾼다. 색 이름을 외울 필요도 없어진다.
///
/// 지금 자리 다음부터 찾고 끝에 닿으면 처음으로 돌아온다 - 차이 이동과 같은 방식이라
/// 새 규칙을 외울 것이 없다.
/// </summary>
[RelayCommand]
private void GoToKind(LineHighlight kind)
{
if (Lines.Count == 0) return;
for (var step = 1; step <= Lines.Count; step++)
{
var at = (CaretRow + step) % Lines.Count;
if (Lines[at].Highlight != kind) continue;
GoToRow(at);
return;
}
// 한 줄도 없으면 조용히 넘어가는 대신 왜 아무 일도 없는지 알려 준다.
MessageUtil.ShowToast($"{KindName(kind)} 줄이 없습니다.");
}
private static string KindName(LineHighlight kind) => kind switch
{
LineHighlight.Changed => "BASE와 다름",
LineHighlight.Applied => "머지 반영함",
LineHighlight.Edited => "직접 수정",
LineHighlight.Added => "추가",
LineHighlight.Removed => "삭제",
LineHighlight.Ignored => "무시",
LineHighlight.Moved => "자리 옮김",
LineHighlight.RemoteChange => "Theirs only",
LineHighlight.Conflict => "Conflict",
_ => "그"
};
private void UpdateLegendCounts()
{
int changed = 0, applied = 0, edited = 0, added = 0, removed = 0, moved = 0, ignored = 0, same = 0;

View File

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace MergeHub.Views.Controls
{
/// <summary>
/// 개수가 0 이면 흐리게. 범례에서 <b>있는 것과 없는 것</b>을 한눈에 가르려는 것이다.
///
/// 숨기지 않고 흐리게만 하는 까닭 - 항목이 나타났다 사라지면 자리가 매번 움직여
/// 같은 것을 다시 찾아야 한다. 자리는 그대로 두고 무게만 뺀다.
/// </summary>
public sealed class ZeroToDimConverter : IValueConverter
{
/// <summary>0 일 때의 흐리기. 읽히기는 하되 눈이 먼저 가지는 않는 정도.</summary>
private const double Dim = 0.35;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value is int count && count == 0 ? Dim : 1.0;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
Binding.DoNothing;
}
}

View File

@ -6,6 +6,7 @@
xmlns:local="clr-namespace:MergeHub.Views"
xmlns:ctl="clr-namespace:MergeHub.Views.Controls"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:models="clr-namespace:MergeHub.Models"
mc:Ignorable="d"
PreviewKeyDown="View_PreviewKeyDown"
AllowDrop="True" DragOver="View_DragOver" Drop="View_Drop">
@ -44,6 +45,39 @@
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<ctl:ZeroToDimConverter x:Key="ZeroToDim"/>
<!--
범례 한 칸(4차 C-4). 읽기만 하던 설명을 <b>누르면 그 갈래로 가는 도구</b>로 바꿨다.
단추처럼 보이지 않게 테두리와 배경을 지운다 - 줄이 단추 열 개로 보이면 시끄럽다.
대신 손 모양 커서와 마우스를 올렸을 때의 옅은 배경으로 "누르는 것"임을 알린다.
-->
<Style x:Key="LegendButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="3,1"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="MinWidth" Value="0"/>
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Back" Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}" CornerRadius="3">
<ContentPresenter VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Back" Property="Background"
Value="{DynamicResource Ubisam.Brush.Sidebar.Background}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="LegendText" TargetType="TextBlock">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Margin" Value="0,0,3,0"/>
@ -526,60 +560,119 @@
FontSize="11" Margin="0,0,12,0"/>
<!-- 각 항목 뒤의 숫자는 지금 화면에 있는 줄 수. 한 줄만 반영해도 즉시 바뀐다. -->
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Changed, Converter={StaticResource HighlightByName}, ConverterParameter=Changed}"/>
<TextBlock Text="BASE와 다름" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding ChangedCount}" Style="{StaticResource LegendCount}"/>
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.Changed}"
Opacity="{Binding ChangedCount, Converter={StaticResource ZeroToDim}}"
ToolTip="누르면 'BASE와 다름' 줄로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Changed, Converter={StaticResource HighlightByName}, ConverterParameter=Changed}"/>
<TextBlock Text="BASE와 다름" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding ChangedCount}" Style="{StaticResource LegendCount}"/>
</StackPanel>
</Button>
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Applied, Converter={StaticResource HighlightByName}, ConverterParameter=Applied}"/>
<TextBlock Text="머지 반영함" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding AppliedCount}" Style="{StaticResource LegendCount}"
Foreground="{DynamicResource Ubisam.Brush.Success}"/>
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.Applied}"
Opacity="{Binding AppliedCount, Converter={StaticResource ZeroToDim}}"
ToolTip="누르면 '머지 반영함' 줄로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Applied, Converter={StaticResource HighlightByName}, ConverterParameter=Applied}"/>
<TextBlock Text="머지 반영함" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding AppliedCount}" Style="{StaticResource LegendCount}"
Foreground="{DynamicResource Ubisam.Brush.Success}"/>
</StackPanel>
</Button>
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Edited, Converter={StaticResource HighlightByName}, ConverterParameter=Edited}"/>
<TextBlock Text="직접 수정" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding EditedCount}" Style="{StaticResource LegendCount}"/>
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.Edited}"
Opacity="{Binding EditedCount, Converter={StaticResource ZeroToDim}}"
ToolTip="누르면 '직접 수정' 줄로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Edited, Converter={StaticResource HighlightByName}, ConverterParameter=Edited}"/>
<TextBlock Text="직접 수정" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding EditedCount}" Style="{StaticResource LegendCount}"/>
</StackPanel>
</Button>
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Added, Converter={StaticResource HighlightByName}, ConverterParameter=Added}"/>
<TextBlock Text="추가" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding AddedCount}" Style="{StaticResource LegendCount}"/>
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.Added}"
Opacity="{Binding AddedCount, Converter={StaticResource ZeroToDim}}"
ToolTip="누르면 '추가' 줄로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Added, Converter={StaticResource HighlightByName}, ConverterParameter=Added}"/>
<TextBlock Text="추가" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding AddedCount}" Style="{StaticResource LegendCount}"/>
</StackPanel>
</Button>
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Removed, Converter={StaticResource HighlightByName}, ConverterParameter=Removed}"/>
<TextBlock Text="삭제" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding RemovedCount}" Style="{StaticResource LegendCount}"/>
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.Removed}"
Opacity="{Binding RemovedCount, Converter={StaticResource ZeroToDim}}"
ToolTip="누르면 '삭제' 줄로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Removed, Converter={StaticResource HighlightByName}, ConverterParameter=Removed}"/>
<TextBlock Text="삭제" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding RemovedCount}" Style="{StaticResource LegendCount}"/>
</StackPanel>
</Button>
<!-- 무시 규칙에 걸린 줄. 차이로 세지 않는다. -->
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Ignored, Converter={StaticResource HighlightByName}, ConverterParameter=Ignored}"/>
<TextBlock Text="무시" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding IgnoredCount}" Style="{StaticResource LegendCount}"/>
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.Ignored}"
Opacity="{Binding IgnoredCount, Converter={StaticResource ZeroToDim}}"
ToolTip="누르면 '무시' 줄로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Ignored, Converter={StaticResource HighlightByName}, ConverterParameter=Ignored}"/>
<TextBlock Text="무시" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding IgnoredCount}" Style="{StaticResource LegendCount}"/>
</StackPanel>
</Button>
<!-- 자리만 옮긴 줄. 삭제+추가로 두 번 세지 않도록 따로 표시한다. -->
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Moved, Converter={StaticResource HighlightByName}, ConverterParameter=Moved}"/>
<TextBlock Text="자리 옮김" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding MovedCount}" Style="{StaticResource LegendCount}"/>
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.Moved}"
Opacity="{Binding MovedCount, Converter={StaticResource ZeroToDim}}"
ToolTip="누르면 '자리 옮김' 줄로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Moved, Converter={StaticResource HighlightByName}, ConverterParameter=Moved}"/>
<TextBlock Text="자리 옮김" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding MovedCount}" Style="{StaticResource LegendCount}"/>
</StackPanel>
</Button>
<!-- 3-Way 일 때만 뜻이 있는 두 가지. 2-Way 에서는 늘 0 이라 자리만 차지한다. -->
<Border Style="{StaticResource LegendChip}"
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.RemoteChange}"
Opacity="{Binding RemoteChangeCount, Converter={StaticResource ZeroToDim}}"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}"
Background="{Binding Source=RemoteChange, Converter={StaticResource HighlightByName}, ConverterParameter=RemoteChange}"/>
<TextBlock Text="Theirs only" Style="{StaticResource LegendText}"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}"/>
<TextBlock Text="{Binding RemoteChangeCount}" Style="{StaticResource LegendCount}"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}"/>
ToolTip="누르면 '저쪽만 고친 줄'로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=RemoteChange, Converter={StaticResource HighlightByName}, ConverterParameter=RemoteChange}"/>
<TextBlock Text="Theirs only" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding RemoteChangeCount}" Style="{StaticResource LegendCount}"/>
</StackPanel>
</Button>
<Border Style="{StaticResource LegendChip}"
<Button Style="{StaticResource LegendButton}" Command="{Binding GoToKindCommand}"
CommandParameter="{x:Static models:LineHighlight.Conflict}"
Opacity="{Binding ConflictCount, Converter={StaticResource ZeroToDim}}"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}"
Background="{Binding Source=Conflict, Converter={StaticResource HighlightByName}, ConverterParameter=Conflict}"/>
<TextBlock Text="Conflict" Style="{StaticResource LegendText}"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}"/>
<TextBlock Text="{Binding ConflictCount}" Style="{StaticResource LegendCount}"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}"/>
ToolTip="누르면 충돌 줄로 갑니다. 다시 누르면 다음 줄">
<StackPanel Orientation="Horizontal">
<Border Style="{StaticResource LegendChip}"
Background="{Binding Source=Conflict, Converter={StaticResource HighlightByName}, ConverterParameter=Conflict}"/>
<TextBlock Text="Conflict" Style="{StaticResource LegendText}"/>
<TextBlock Text="{Binding ConflictCount}" Style="{StaticResource LegendCount}"/>
</StackPanel>
</Button>
<Border Style="{StaticResource LegendChip}" Background="#66E0A020"/>
<TextBlock Text="줄 안에서 바뀐 부분" Style="{StaticResource LegendText}" Margin="0,0,14,0"/>