[200] 의 패널은 코드로 Border 를 쌓는 비가상화 렌더고 SheetMe 는 가상화 ListBox 다. 골격은 SheetMe 것을 지키고 겉모습과 조작 표면만 옮겼다 — 통째로 베끼면 1,067 컨트롤 서식에서 검색어 한 글자마다 수천 비주얼을 다시 만들게 되고, [200] 에 대응물이 없는 컨테이너 자식 재귀 표시가 사라진다. 구조 - 페이지를 별도 섹션이 아니라 아웃라인의 최상위 노드로. 접기·복제·삭제·컨트롤 수·현재 강조 - 맨 아래 '+ 페이지 추가' 행 - 목록 순서를 뒤집었다 — 이제 맨 위가 z-order 최상위(캔버스에서 위에 겹쳐 보이는 것이 목록 위) - 들여쓰기 12 / 32 / +16 - 레이어 패널을 LayerPanelView 로 분리(MainView.xaml 이 1,000줄을 넘겼고, 진단 렌더러가 이 패널만 따로 찍어 라이트·다크·선택 상태를 검사할 수 있어야 한다) 선택 표시 - IsSelected 가 아니라 Highlight 3상태(None/Blue/Navy)가 배경을 그린다. 그룹 전원 선택 시 폴더는 파랑, 멤버는 서브트리 배경이 되고 맞닿는 모서리를 각지게 해 하나의 블록으로 이어진다 - 밴드 위에서는 아이콘·보조 텍스트 색도 함께 뒤집는다. 안 그러면 눈·자물쇠가 1.6:1 로 묻혀 선택한 행에서만 잠금 상태가 안 보이는 상태가 된다 - 신설 토큰 3종(B.LayerSel / B.LayerSubtree / B.OnSelMuted) — [200] 원본값은 각각 흰 글자 3.00:1, 다크 밴드 1.03:1, 라이트 밴드 1.14:1 로 이 저장소가 이미 기각한 값이다 기능 - 드래그로 z-order 재정렬(삽입선 어도너). 임의 index 이동 연산 MoveWithinParent 신설 - 페이지 복제 — 컨트롤 이름을 전부 새로 발급하고 그룹도 복제본끼리 다시 묶는다 ([200] 은 GroupId 를 그대로 둬서 복제 그룹이 원본과 gid 를 공유한다) - 페이지 헤더 클릭 → 캔버스가 그 페이지로 이동(ScrollToPage) - 데이터 배선 배지 — 값이 조회·데이터소스에서 오는 컨트롤 표시 - 검색칩에 테두리(라이트에서 B.Input=B.Panel 이라 경계 대비가 1.00:1 이었다) 이식하지 않은 것: 컨트롤 행 인라인 개명. [200] 이 고치는 값은 표시 전용 별칭이지만 SheetMe 의 컨트롤 이름은 외부 참조 키라 목록에서 무심코 고치면 배선이 끊긴다. 더블클릭은 요청대로 캔버스 이동을 유지하고, 그룹 행에서만 이름 편집으로 간다. 동반 수정: 눈·자물쇠 토글에 되돌리기 지점 추가(400ms 코얼레스), 그룹 이름 비우기 경로가 스냅숏 없이 지워지던 것, 숨김 흐리기를 행 전체가 아니라 이름 줄에만. 편집 스모크의 레이어 검사 19 → 42건. 회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
165 lines
6.8 KiB
C#
165 lines
6.8 KiB
C#
using System.Globalization;
|
|
using System.Windows.Data;
|
|
using System.Windows.Media;
|
|
|
|
namespace SheetMe.Designer.Controls;
|
|
|
|
/// <summary>
|
|
/// 팔레트 타입 → (Lucide 아이콘, 카테고리) 매핑 — [200]SheetMe 팔레트 타일 룩 이식.
|
|
/// Core(ControlRegistry)는 UI 무관하게 유지하고 표시 계층인 여기서만 매핑한다.
|
|
/// </summary>
|
|
public static class PaletteIconCatalog
|
|
{
|
|
#region Member Fields
|
|
private static readonly Dictionary<string, (string Icon, string Category)> Map = new(StringComparer.Ordinal)
|
|
{
|
|
["Label"] = ("type", "표시"),
|
|
["PictureBox"] = ("image", "표시"),
|
|
["Line"] = ("minus", "표시"),
|
|
// MaskedTextBox 는 TextBox 와 아이콘이 겹쳐 구분이 안 됐다 — [200] 어휘로 분리
|
|
["TextBox"] = ("text-cursor-input", "입력"),
|
|
["MaskedTextBox"] = ("type", "입력"),
|
|
["ComboBox"] = ("list", "입력"),
|
|
["ListBox"] = ("list-checks", "입력"),
|
|
["CheckList"] = ("list-checks", "입력"),
|
|
["DateTimePicker"] = ("calendar", "입력"),
|
|
["CalcBox"] = ("sigma", "입력"),
|
|
["CheckBox"] = ("square-check", "선택"),
|
|
["RadioButton"] = ("circle-dot", "선택"),
|
|
["Panel"] = ("package", "컨테이너"),
|
|
["GroupBox"] = ("box", "컨테이너"),
|
|
["Button"] = ("mouse-pointer-click", "동작/데이터"),
|
|
["DataTable"] = ("table", "동작/데이터"),
|
|
};
|
|
#endregion
|
|
|
|
#region Methods
|
|
/// <summary>타입의 Lucide 아이콘명(미지정 타입은 square)</summary>
|
|
public static string IconOf(string type) => Map.TryGetValue(type, out var entry) ? entry.Icon : "square";
|
|
|
|
/// <summary>타입의 팔레트 카테고리(그룹 헤더)</summary>
|
|
public static string CategoryOf(string type) => Map.TryGetValue(type, out var entry) ? entry.Category : "기타";
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// 아이콘 컨버터 공용 파라미터 해석 — <c>"크기"</c> 또는 <c>"크기|브러시키"</c>.
|
|
///
|
|
/// 브러시 키를 생략하면 <c>B.Muted</c> 를 쓴다(하위호환). 색을 인자로 받을 수 있어야
|
|
/// 활성 문서 탭 아이콘을 강조색으로, 레이어 타입 아이콘을 본문색으로 칠할 수 있다.
|
|
///
|
|
/// <b>Brush 인스턴스가 아니라 키를 그대로 돌려준다</b> — 여기서 TryFindResource 로 브러시를 꺼내
|
|
/// 대입하면 그 시점 값으로 굳어 테마 전환을 따라가지 못한다.
|
|
/// </summary>
|
|
internal static class IconParam
|
|
{
|
|
public static (double Size, string BrushKey) Parse(object? parameter, double defaultSize)
|
|
{
|
|
var size = defaultSize;
|
|
var key = "B.Muted";
|
|
|
|
if (parameter is string text && text.Length > 0)
|
|
{
|
|
var bar = text.IndexOf('|');
|
|
var sizePart = bar >= 0 ? text[..bar] : text;
|
|
if (bar >= 0 && bar + 1 < text.Length)
|
|
{
|
|
key = text[(bar + 1)..].Trim();
|
|
}
|
|
if (double.TryParse(sizePart, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
|
|
{
|
|
size = parsed;
|
|
}
|
|
}
|
|
|
|
return (size, key);
|
|
}
|
|
}
|
|
|
|
/// <summary>팔레트 타입명 → Lucide 아이콘 비주얼(FrameworkElement) 컨버터. parameter=<c>"크기"</c> 또는 <c>"크기|브러시키"</c>(기본 18, B.Muted)</summary>
|
|
public sealed class TypeToIconConverter : IValueConverter
|
|
{
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
{
|
|
if (value is not string type)
|
|
{
|
|
return null;
|
|
}
|
|
var (size, brushKey) = IconParam.Parse(parameter, 18);
|
|
return LucideIcons.Icon(PaletteIconCatalog.IconOf(type), size, brushKey);
|
|
}
|
|
|
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
=> throw new NotSupportedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>"아이콘명|크기|브러시키"</c> 한 문자열 → Lucide 아이콘 비주얼.
|
|
///
|
|
/// 색이 <b>행 상태에 따라 달라져야</b> 할 때 쓴다. 보통은 색을 ConverterParameter 에 적지만
|
|
/// ConverterParameter 는 바인딩할 수 없어서, 색이 변하는 순간 아이콘이 이전 색에 굳어 버린다
|
|
/// (선택 밴드 위에서 파란 아이콘이 사라지는 식). 그래서 사양 전체를 값 쪽으로 옮겼다.
|
|
/// </summary>
|
|
public sealed class IconSpecConverter : IValueConverter
|
|
{
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
{
|
|
if (value is not string spec)
|
|
{
|
|
return null;
|
|
}
|
|
var first = spec.IndexOf('|');
|
|
if (first < 0)
|
|
{
|
|
return LucideIcons.Icon(spec, 14, "B.Muted");
|
|
}
|
|
var name = spec[..first];
|
|
var (size, brushKey) = IconParam.Parse(spec[(first + 1)..], 14);
|
|
return LucideIcons.Icon(name, size, brushKey);
|
|
}
|
|
|
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
=> throw new NotSupportedException();
|
|
}
|
|
|
|
/// <summary>팔레트 타입명 → 카테고리명 컨버터(CollectionViewSource 그룹핑용)</summary>
|
|
public sealed class TypeToCategoryConverter : IValueConverter
|
|
{
|
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
=> value is string type ? PaletteIconCatalog.CategoryOf(type) : "기타";
|
|
|
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
=> throw new NotSupportedException();
|
|
}
|
|
|
|
/// <summary>불리언 반전 컨버터(선택 도구 토글 = !IsHandTool 표시용)</summary>
|
|
public sealed class InverseBoolConverter : IValueConverter
|
|
{
|
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
=> value is not true;
|
|
|
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
=> value is not true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lucide 아이콘명 → 비주얼 컨버터(문서 탭 file-text, 플로팅 바 layout-grid 등 고정 아이콘용).
|
|
/// Binding Source 에 아이콘명 문자열을 넣어 사용 — 항목마다 새 비주얼이 생성되어 공유 부모 충돌이 없다.
|
|
/// parameter=<c>"크기"</c> 또는 <c>"크기|브러시키"</c>(기본 14, B.Muted).
|
|
/// </summary>
|
|
public sealed class IconNameToVisualConverter : IValueConverter
|
|
{
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
{
|
|
if (value is not string name)
|
|
{
|
|
return null;
|
|
}
|
|
var (size, brushKey) = IconParam.Parse(parameter, 14);
|
|
return LucideIcons.Icon(name, size, brushKey);
|
|
}
|
|
|
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
=> throw new NotSupportedException();
|
|
}
|