색상 행에 인라인 팔레트 — 스와치를 누르면 그 자리에서 고르고, '자세히…' 로 피커

색을 바꾸려면 매번 대화상자를 열어야 했다. 실측해 보니 그럴 이유가 없다 —
--db-colors 진단을 신설해 운영 디자인 1,271건의 BackColor·ForeColor 60,319건을 집계한 결과
상위 두 값이 전체의 84%다: White 29,010건(48%), "224, 224, 224" 21,497건(36%).
뒤는 꼬리가 길고 얇다(Transparent 792 · Silver 557 · Window 440 · Gainsboro 415 …).
ForeColor 는 Black 4,920건(85%) · ControlText 345건이 사실상 전부.

스와치를 토글로 바꾸고 팝업에 실사용 상위 값을 배경 12종 / 글자·강조 12종으로 깔았다.
표준 색상표를 늘어놓지 않은 것은 의도다 — 목적은 자주 쓰는 값을 한 번에 고르게 하는 것이지
색을 발명하게 하는 것이 아니다. 임의 색은 팝업 아래 '자세히…' 로 기존 피커
(채도·명도 사각형 + 색상 슬라이더 + HEX·RGB + 프리셋·최근색)를 연다.

핵심은 견본이 레거시 원문 값을 그대로 넣는다는 점이다. 명명색을 RGB 로 풀면
원문과 달라져 왕복 diff 가 생기고, 시스템색이 갖는 의미(Window/Control/ControlText)도 잃는다.
'Window' 를 고르면 "Window" 가 저장된다. 툴팁에는 실사용 건수를 함께 띄운다.

'비우기'는 기본값을 저장하는 것이 아니라 키를 지운다 — 레거시 PropertyGrid 의 재설정과
같고, 원래 BackColor 가 없던 페이지(12.7%)를 원상태로 되돌릴 수 있어야 한다.

함께 고친 것: 페이지 속성 모드에서 '컨트롤을 선택하면 속성이 표시됩니다' 안내가 행과 같이
뜨던 것(HasSelection 이 컨트롤 선택만 보고 있었다). 탭 스트립은 페이지 모드에서 숨긴다.

편집 스모크 4건 추가(원문 값 적용 / 명명색 보존 / 적용 후 닫힘 / 비우기는 키 제거).
회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-13 10:56:47 +09:00
co-authored by Claude Opus 5
parent ac36f93a03
commit e84337705f
7 changed files with 337 additions and 21 deletions
@@ -39,7 +39,11 @@ public sealed class InspectorViewModel : ViewModelBase
public ObservableCollection<PropertyRowViewModel> Rows { get; } = new();
/// <summary>선택된 컨트롤이 있는지 — 인스펙터 빈 상태 판정</summary>
public bool HasSelection => designer.Selection.Items.Count > 0;
/// <summary>
/// 보여 줄 대상이 있는가 — 컨트롤 선택 또는 페이지 속성 모드.
/// 페이지도 레거시에선 하나의 Object 라 '선택 없음' 안내를 띄우면 안 된다.
/// </summary>
public bool HasSelection => designer.Selection.Items.Count > 0 || designer.InspectedPage is not null;
/// <summary>
/// 선택된 탭. 컨트롤을 바꿔도 유지된다 — 데이터 탭에서 여러 컨트롤의 태그를 잇달아 손보는
@@ -104,8 +108,9 @@ public sealed class InspectorViewModel : ViewModelBase
/// <summary>동작 탭을 그릴지 — 이 컨트롤에 동작 속성이 있을 때만</summary>
public bool ShowBehaviorTab => BehaviorCount > 0;
/// <summary>탭이 하나뿐이면 알약 줄 자체가 잡음이다 — 둘 이상일 때만 띄운다</summary>
public bool ShowTabStrip => HasSelection && (ShowDataTab || ShowBehaviorTab);
/// <summary>탭이 하나뿐이면 알약 줄 자체가 잡음이다 — 둘 이상일 때만 띄운다.
/// 페이지 속성 모드에는 데이터·동작 탭이 없으므로 컨트롤 선택일 때만 본다.</summary>
public bool ShowTabStrip => designer.Selection.Items.Count > 0 && (ShowDataTab || ShowBehaviorTab);
/// <summary>선택은 있는데 이 탭에 보여줄 게 없는 상태 — 빈 화면 대신 안내를 띄운다</summary>
public bool IsTabEmpty => HasSelection && Rows.Count == 0;
@@ -319,7 +324,17 @@ public sealed class InspectorViewModel : ViewModelBase
}
AddPageRow(page, backRow,
() => root.Props.GetText("BackColor") ?? DefaultPageBackColor,
value => root.Props.SetText("BackColor", value.Length == 0 ? DefaultPageBackColor : value),
value =>
{
// 비우면 기본값을 '저장'하는 게 아니라 키를 지운다 — 레거시 PropertyGrid 의 재설정과 같고,
// 원래 이 키가 없던 페이지(12.7%)를 원상태로 되돌릴 수 있어야 한다
if (value.Length == 0)
{
root.Props.Remove("BackColor");
return;
}
root.Props.SetText("BackColor", value);
},
page.NotifyPaperChanged);
// 페이지 글꼴은 자식이 상속한다 — 여기를 바꾸면 Font 를 명시하지 않은 컨트롤이 전부 따라온다
@@ -512,6 +512,8 @@ public sealed class QueryRowViewModel : PropertyRowViewModel
/// <summary>색 행 — 레거시 invariant 문자열("R, G, B"/명명색) + 미리보기 스와치</summary>
public sealed class ColorRowViewModel : PropertyRowViewModel
{
private bool isPaletteOpen;
/// <summary>미리보기 브러시</summary>
public Brush Preview
{
@@ -531,9 +533,47 @@ public sealed class ColorRowViewModel : PropertyRowViewModel
/// <summary>색상 피커 열기 — 확정 시 레거시 invariant 형식("R, G, B")으로 반영</summary>
public M.Framework.WPF.ICustomCommand? PickCommand { get; set; }
/// <summary>인라인 팔레트 열림 — 스와치를 누르면 토글된다</summary>
public bool IsPaletteOpen
{
get => isPaletteOpen;
set => SetProperty(ref isPaletteOpen, value);
}
/// <summary>자주 쓰는 배경색 견본</summary>
public IReadOnlyList<ColorSwatchViewModel> BackgroundSwatches { get; } =
Core.Catalog.LegacyColorCatalog.Backgrounds.Select(e => new ColorSwatchViewModel(e.Value, e.Usage)).ToList();
/// <summary>자주 쓰는 글자색·강조 견본</summary>
public IReadOnlyList<ColorSwatchViewModel> ForegroundSwatches { get; } =
Core.Catalog.LegacyColorCatalog.Foregrounds.Select(e => new ColorSwatchViewModel(e.Value, e.Usage)).ToList();
/// <summary>견본 선택 — 레거시 원문 값을 그대로 넣고 팔레트를 닫는다</summary>
public M.Framework.WPF.ICustomCommand? ApplySwatchCommand { get; set; }
/// <summary>비우기 — 속성값을 지운다(상속/기본값으로 되돌림)</summary>
public M.Framework.WPF.ICustomCommand? ClearCommand { get; set; }
public ColorRowViewModel(string label) : base(label)
{
PickCommand = new M.Framework.WPF.Command((sender, e) => OnPick());
PickCommand = new M.Framework.WPF.Command((sender, e) =>
{
IsPaletteOpen = false;
OnPick();
});
ApplySwatchCommand = new M.Framework.WPF.Command((object? parameter) =>
{
if (parameter is ColorSwatchViewModel swatch)
{
ValueText = swatch.Value;
}
IsPaletteOpen = false;
});
ClearCommand = new M.Framework.WPF.Command((sender, e) =>
{
ValueText = string.Empty;
IsPaletteOpen = false;
});
}
protected override void OnValueApplied() => OnPropertyChanged(nameof(Preview));
@@ -556,6 +596,44 @@ public sealed class ColorRowViewModel : PropertyRowViewModel
}
}
/// <summary>
/// 색 견본 하나 — 인라인 팔레트 항목.
/// <see cref="Value"/> 는 화면 표시용 hex 가 아니라 <b>레거시 원문 그대로</b>(명명색 또는 "R, G, B")다.
/// 명명색을 RGB 로 풀어 저장하면 원문과 달라져 왕복 diff 가 생기고, 시스템색이 갖는 의미(Window/Control)도 잃는다.
/// </summary>
public sealed class ColorSwatchViewModel
{
/// <summary>저장될 레거시 값</summary>
public string Value { get; }
/// <summary>견본 채움</summary>
public Brush Preview { get; }
/// <summary>견본 윤곽 — 배경이 아니라 견본 자신의 명도로 정한다</summary>
public Brush Edge { get; }
/// <summary>툴팁 — 값 + 실사용 건수</summary>
public string ToolTip { get; }
public ColorSwatchViewModel(string value, string usage)
{
Value = value;
var (a, r, g, b) = Core.Serialization.LegacyFormat.ParseColor(value);
var fill = new SolidColorBrush(Color.FromArgb(a, r, g, b));
fill.Freeze();
Preview = fill;
var luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255.0;
var edge = new SolidColorBrush(luminance > 0.6
? Color.FromRgb(0x8A, 0x8A, 0x8A)
: Color.FromRgb(0xD0, 0xD0, 0xD0));
edge.Freeze();
Edge = edge;
ToolTip = usage.Length > 0 ? $"{value} ({usage})" : value;
}
}
/// <summary>읽기 전용 행 — 중첩/바이너리/참조 등 raw 편집 불가 값 표시</summary>
public sealed class ReadOnlyRowViewModel : PropertyRowViewModel
{