[200] 디자인 동일화 2/2 — 좌측 패널·타이틀바·인스펙터 정보 밀도

1/2 가 껍데기(스크롤바·창 기본값)를 맞췄다면 이번은 그 안에 든 정보다. [200] 과의 실제 격차는
장식이 아니라 "한 화면에서 얼마나 읽히는가"였고, 그중 둘은 데이터 손상 경로이기도 했다.

[레이어] 행 라벨을 Model.Id → 신규 LayerName(Text 우선, 없으면 Id)으로. 운영 서식의 Id 는
대부분 Label12 꼴 자동 이름이라 291개짜리 목록이 아무것도 알려주지 않았다. Id 는 외부 참조
키이므로(DataTableField 대상명, EventHandlerMappingTag 의 ShtCod_컨트롤명 규약) 버리지 않고
툴팁 + 검색 대상으로 유지한다. 앞에 타입 아이콘(Accent), 뒤 숨김/잠금을 MiniToggle(eye/eye-off,
lock/unlock)로 — 사각 체크박스로는 상태가 안 읽혔다.
검색칩 신설(LayerFilter, 이름·Id 부분일치). 컨트롤 최대 1,067개 서식에서 스크롤만으로는 대상을
찾을 수 없다. 칩 안 TextBox 에 MinHeight=0 필수 — 전역 TextBox MinHeight 30 이 32px 칩에서
글자를 자른다.

[인스펙터·여러 값] IsMixed 가 설정만 되고 화면 어디에도 바인딩돼 있지 않았다. 다중선택에서 값이
갈리면 편집기가 빈 칸이 되는데 그 빈 칸은 "값 없음"과 구별되지 않는다. 무심코 입력하면 서로
달랐던 값이 전부 한 값으로 덮인다. 편집기 위에 비파괴 오버레이("여러 값")를 얹고, 포커스가
들어오면 감춘다 — 값은 LostFocus 에만 올라가므로 타이핑 중 ValueText 는 계속 "" 이고 안내가
방금 친 글자 위에 남는다.
토글 행은 IsOn 을 bool? 로 바꿔 3상태로. false 로 내리면 "전부 꺼짐"으로 보이고, 사용자가 한 번
켰다 끄면 전부 False 가 되어 조용히 뭉개진다.

[인스펙터·접이식] 섹션 헤더를 클릭 가능한 접기/펴기로(셰브런 + 소속 행 수). 고급 속성까지 켜면
수십 행이 된다. Rows 는 평면 컬렉션을 유지하고 섹션이 자기 행을 들고 있다가 컨테이너
Visibility 만 토글한다 — 행 VM 을 재생성하지 않아 편집 중 값·포커스가 보존되고, 다중선택
재구성이 잦은 이 화면에서 트리 재구축 비용도 피한다.

[타이틀바] 로고 메뉴 루트(셰브런) 아래로 5개 메뉴를 흡수하고, 편집 아이콘 스트립(실행취소·다시
실행 | 복사·삭제·그룹·해제)과 우측 액션(테마·저장·미리보기)을 배치. 중앙 제목은 뺐다 — 문서 탭이
같은 정보를 이미 들고 있다.

[빈 상태] 문서 0개 → 회색 허공 대신 다음 행동 3개(새 서식 / DB에서 열기 / 파일 열기).
선택 0개 → 인스펙터에 "선택 없음" 한 줄만 떠 패널이 고장 난 것처럼 보이던 것을 안내로 교체.

[그 외] 서식 목록에 하단 상태 한 줄(로딩·건수·DB 미접속) — 종전에는 목록 위에 "불러오는 중"
오버레이가 덮여 목록이 가려졌고 건수는 창 하단 전역 상태줄에만 있었다. 활성 문서 탭 아이콘을
Accent 로. 플로팅 바의 ±버튼 제거([200] 동일 — Ctrl+휠이 주 조작이고 배율 버튼이 플라이아웃을
겸한다), 대신 "화면 맞춤" 추가. ZoomFit 은 문서 전체가 아니라 활성 페이지 한 장 기준이다
(7페이지 서식을 통째로 맞추면 14% 가 되어 아무것도 안 보인다). 뷰포트 크기는 캔버스 뷰가
SizeChanged 에서 VM 에 통지 — VM 이 시각 트리를 몰라도 되게 한다.
열 기본 폭 232→248 / 280→300.

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0.
**종이 렌더 P062 픽셀 대조 차이 0**(4,802바이트 전부 일치).
실 UI 확인: P062 레이어 행·인스펙터 접기, P093 291컨트롤에서 레이어 검색("알러지" → 10건),
116개 선택 시 X/Y/너비/높이 "여러 값" 표시.

남은 배치: E(선택 오버레이) · F(캔버스 크롬 — 종이에 닿는 유일한 배치) · G(토스트·ThemedDialog).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-12 10:33:48 +09:00
co-authored by Claude Opus 5
parent 7591fbed74
commit 9d20cc2971
9 changed files with 600 additions and 87 deletions
@@ -113,6 +113,7 @@ public abstract class ControlViewModel : ViewModelBase
{
Model.Locked = value;
OnPropertyChanged(nameof(IsLockedFlag));
OnPropertyChanged(nameof(LockIcon));
}
}
@@ -124,12 +125,39 @@ public abstract class ControlViewModel : ViewModelBase
{
Model.Hidden = value;
OnPropertyChanged(nameof(IsHiddenFlag));
OnPropertyChanged(nameof(HiddenIcon));
OnPropertyChanged(nameof(DesignOpacity));
}
}
/// <summary>디자인 표시 불투명도 — 숨김이면 0.25</summary>
public double DesignOpacity => Model.Hidden ? 0.25 : 1.0;
/// <summary>
/// 레이어 목록 표시명 — Text 가 있으면 그것, 없으면 Id.
///
/// Id 는 대부분 <c>Label12</c> 처럼 자동 생성된 이름이라 목록에서 아무것도 알려주지 않는다.
/// 다만 Id 는 외부 참조 키이기도 하므로(DataTableField 대상명, EventHandlerMappingTag 의
/// <c>ShtCod_컨트롤명</c> 규약) 버리지 않고 툴팁·검색 대상으로 함께 유지한다.
/// </summary>
public string LayerName
{
get
{
var text = Text.Replace('\r', ' ').Replace('\n', ' ').Trim();
if (text.Length == 0)
{
return Id;
}
return text.Length <= 40 ? text : string.Concat(text.AsSpan(0, 39), "…");
}
}
/// <summary>레이어 잠금 토글 아이콘명</summary>
public string LockIcon => Model.Locked ? "lock" : "unlock";
/// <summary>레이어 숨김 토글 아이콘명</summary>
public string HiddenIcon => Model.Hidden ? "eye-off" : "eye";
#endregion
#region Constructors
@@ -68,6 +68,9 @@ public sealed class DesignerViewModel : ViewModelBase
private double zoom = 1.0;
private FormDocument document;
private PageViewModel? selectedPage;
private string layerFilter = string.Empty;
private double viewportWidth;
private double viewportHeight;
private bool isTabOrderMode;
private readonly List<ControlViewModel> tabOrderSequence = new();
private readonly Dictionary<string, SpreadGridInfo>? spreadDesigns;
@@ -138,13 +141,53 @@ public sealed class DesignerViewModel : ViewModelBase
get => selectedPage;
set
{
if (SetProperty(ref selectedPage, value) && value is not null)
if (SetProperty(ref selectedPage, value))
{
Selection.ActivePage = value;
if (value is not null)
{
Selection.ActivePage = value;
}
OnPropertyChanged(nameof(VisibleLayers));
}
}
}
/// <summary>
/// 레이어 검색어 — 이름(LayerName)·Id 부분일치. 서식당 컨트롤이 수백 개인 경우가 흔해
/// (운영 최대 1,067개) 스크롤만으로는 대상을 찾을 수 없다.
/// </summary>
public string LayerFilter
{
get => layerFilter;
set
{
if (SetProperty(ref layerFilter, value ?? string.Empty))
{
OnPropertyChanged(nameof(VisibleLayers));
}
}
}
/// <summary>
/// 레이어 목록에 실제로 표시할 항목 — 활성 페이지의 컨트롤에 검색어를 적용한 결과.
/// 검색어가 비면 원본 컬렉션을 그대로 돌려준다(항목 추가·삭제가 즉시 반영되도록).
/// </summary>
public System.Collections.IEnumerable? VisibleLayers
{
get
{
var source = SelectedPage?.Controls;
if (source is null || layerFilter.Length == 0)
{
return source;
}
return source
.Where(c => c.LayerName.Contains(layerFilter, StringComparison.OrdinalIgnoreCase)
|| c.Id.Contains(layerFilter, StringComparison.OrdinalIgnoreCase))
.ToList();
}
}
/// <summary>레이어 목록 선택 항목 — 캔버스 선택과 동기</summary>
public ControlViewModel? SelectedLayerItem
{
@@ -401,6 +444,38 @@ public sealed class DesignerViewModel : ViewModelBase
/// <summary>줌 100%</summary>
public void ZoomReset() => Zoom = 1.0;
/// <summary>
/// 캔버스 뷰포트 크기 통지 — DesignerCanvasView 가 SizeChanged 에서 호출한다.
/// ZoomFit 은 뷰포트를 알아야 하는데 VM 은 시각 트리를 모르므로 값만 받아 둔다.
/// </summary>
public void ReportViewport(double width, double height)
{
viewportWidth = width;
viewportHeight = height;
}
/// <summary>
/// 화면 맞춤 — 활성 페이지 한 장이 뷰포트에 들어오는 배율.
/// 문서 전체(WorldHeight)가 아니라 페이지 한 장 기준이다. 7페이지 서식을 통째로 맞추면
/// 14% 가 되어 아무것도 안 보인다(운영 최대 7페이지).
/// </summary>
public void ZoomFit()
{
var page = SelectedPage ?? (Pages.Count > 0 ? Pages[0] : null);
if (page is null || viewportWidth <= 0 || viewportHeight <= 0)
{
return;
}
// Border Padding 24 양쪽 + 스크롤바 여유
var availableWidth = viewportWidth - 64;
var availableHeight = viewportHeight - 64;
if (availableWidth <= 0 || availableHeight <= 0)
{
return;
}
Zoom = Math.Round(Math.Min(availableWidth / page.WidthDip, availableHeight / page.HeightDip), 2);
}
#endregion
#region Methods - /
@@ -20,6 +20,7 @@ public sealed class InspectorViewModel : ViewModelBase
"System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
private readonly DesignerViewModel designer;
private readonly List<(PropertyRowViewModel Row, RowBinding Binding)> boundsRows = new();
private SectionRowViewModel? currentSection;
private bool showAdvanced;
/// <summary>공통/글꼴 섹션이 이미 다루는 키 — 고급 목록에서 제외</summary>
@@ -33,6 +34,9 @@ public sealed class InspectorViewModel : ViewModelBase
/// <summary>행 목록(섹션 헤더 포함)</summary>
public ObservableCollection<PropertyRowViewModel> Rows { get; } = new();
/// <summary>선택된 컨트롤이 있는지 — 인스펙터 빈 상태 판정</summary>
public bool HasSelection => designer.Selection.Items.Count > 0;
/// <summary>선택 요약 텍스트</summary>
public string Summary
=> designer.Selection.Items.Count switch
@@ -73,7 +77,9 @@ public sealed class InspectorViewModel : ViewModelBase
{
Rows.Clear();
boundsRows.Clear();
currentSection = null;
OnPropertyChanged(nameof(Summary));
OnPropertyChanged(nameof(HasSelection));
var items = designer.Selection.Items;
if (items.Count == 0)
@@ -82,7 +88,7 @@ public sealed class InspectorViewModel : ViewModelBase
}
// 공통: 이름/위치/크기
Rows.Add(new SectionRowViewModel("공통"));
AddSection("공통");
if (items.Count == 1)
{
AddRow(new TextRowViewModel("이름"), new RowBinding
@@ -98,7 +104,7 @@ public sealed class InspectorViewModel : ViewModelBase
AddBoundsRow("높이", vm => vm.Height, (vm, v) => vm.Height = Math.Max(1, v));
// 폰트(공통) — Font Property(상속 시 빈 값)
Rows.Add(new SectionRowViewModel("글꼴"));
AddSection("글꼴");
AddFontRow("글꼴", f => f.Family, (f, v) => f.Family = v.Length == 0 ? f.Family : v);
AddFontRow("크기(pt)", f => f.SizePt.ToString("0.##", CultureInfo.InvariantCulture),
(f, v) => f.SizePt = double.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var size) && size > 0 ? size : f.SizePt);
@@ -110,7 +116,7 @@ public sealed class InspectorViewModel : ViewModelBase
var curatedKeys = new HashSet<string>(HandledKeys, StringComparer.Ordinal);
if (items.All(i => i.Type == type) && ControlRegistry.Find(type) is { } descriptor && descriptor.Properties.Count > 0)
{
Rows.Add(new SectionRowViewModel(descriptor.DisplayName));
AddSection(descriptor.DisplayName);
foreach (var def in descriptor.Properties)
{
AddDefRow(def);
@@ -134,7 +140,8 @@ public sealed class InspectorViewModel : ViewModelBase
if (!showAdvanced)
{
Rows.Add(new ToggleAdvancedRowViewModel($"전체 속성 표시 ▾ ({advancedKeys.Count}개)", () =>
currentSection = null;
AddPlain(new ToggleAdvancedRowViewModel($"전체 속성 표시 ▾ ({advancedKeys.Count}개)", () =>
{
showAdvanced = true;
Rebuild();
@@ -142,8 +149,8 @@ public sealed class InspectorViewModel : ViewModelBase
return;
}
Rows.Add(new SectionRowViewModel("전체 속성 (고급 — 레거시 원문)"));
Rows.Add(new ToggleAdvancedRowViewModel("전체 속성 숨기기 ▴", () =>
AddSection("전체 속성 (고급 — 레거시 원문)");
AddPlain(new ToggleAdvancedRowViewModel("전체 속성 숨기기 ▴", () =>
{
showAdvanced = false;
Rebuild();
@@ -182,21 +189,21 @@ public sealed class InspectorViewModel : ViewModelBase
break;
case LegacyPropValue.NestedValue nested:
Rows.Add(new ReadOnlyRowViewModel(key, $"(중첩 속성 {nested.Children.Count}개 — 보존됨)"));
AddPlain(new ReadOnlyRowViewModel(key, $"(중첩 속성 {nested.Children.Count}개 — 보존됨)"));
break;
case LegacyPropValue.BinaryValue:
Rows.Add(new ReadOnlyRowViewModel(key, "(바이너리 — 원문 보존됨)"));
AddPlain(new ReadOnlyRowViewModel(key, "(바이너리 — 원문 보존됨)"));
break;
case LegacyPropValue.ReferenceValue reference:
Rows.Add(new ReadOnlyRowViewModel(key, $"(참조: {reference.Name})"));
AddPlain(new ReadOnlyRowViewModel(key, $"(참조: {reference.Name})"));
break;
}
}
// 속성 추가 — 레거시 컨트롤의 임의 속성 지정(오타 주의: 로더가 모르는 키는 경고 처리)
Rows.Add(new AddPropertyRowViewModel(key =>
AddPlain(new AddPropertyRowViewModel(key =>
{
if (target.Model.Props.Contains(key))
{
@@ -330,6 +337,20 @@ public sealed class InspectorViewModel : ViewModelBase
};
}
/// <summary>섹션 헤더 시작 — 이후 추가되는 행은 이 섹션에 편입된다</summary>
private void AddSection(string label)
{
currentSection = new SectionRowViewModel(label);
Rows.Add(currentSection);
}
/// <summary>바인딩 없는 행 추가(읽기 전용·버튼 행) — 섹션 편입만 처리</summary>
private void AddPlain(PropertyRowViewModel row)
{
currentSection?.Adopt(row);
Rows.Add(row);
}
private void AddRow(PropertyRowViewModel row, RowBinding binding, Action? afterCommit = null)
{
var items = designer.Selection.Items;
@@ -363,6 +384,7 @@ public sealed class InspectorViewModel : ViewModelBase
}
afterCommit?.Invoke();
};
currentSection?.Adopt(row);
Rows.Add(row);
}
@@ -21,6 +21,15 @@ public abstract class PropertyRowViewModel : ViewModelBase
/// <summary>선택 대상들의 값이 서로 다른지 — "여러 값" 표시</summary>
public bool IsMixed { get; private set; }
/// <summary>소속 섹션 — null 이면 섹션 밖(항상 표시)</summary>
public SectionRowViewModel? Section { get; set; }
/// <summary>접힌 섹션에 속하면 숨긴다 — ItemContainerStyle 이 바인딩</summary>
public bool IsRowVisible => Section is null || Section.IsExpanded;
/// <summary>표시 여부 통지(섹션 접기/펴기)</summary>
public void NotifyVisibilityChanged() => OnPropertyChanged(nameof(IsRowVisible));
/// <summary>정규화 문자열 값 — 파생 편집기가 형 변환</summary>
public string ValueText
{
@@ -59,6 +68,9 @@ public abstract class PropertyRowViewModel : ViewModelBase
OnPropertyChanged(nameof(ValueText));
OnPropertyChanged(nameof(IsMixed));
building = false;
// 파생 표시(토글 3상태·미리보기·요약)도 초기값 기준으로 맞춘다.
// 드래그 중 RefreshBoundsRows 가 Initialize 만 호출하는 경로가 있어 여기서 하지 않으면 파생이 뒤처진다.
OnValueApplied();
}
/// <summary>값 적용 후 파생 갱신 지점</summary>
@@ -66,10 +78,59 @@ public abstract class PropertyRowViewModel : ViewModelBase
#endregion
}
/// <summary>구분 헤더 행</summary>
/// <summary>
/// 구분 헤더 행 — 클릭으로 접기/펴기.
///
/// Rows 는 평면 컬렉션을 유지하고(다중선택 재구성이 잦아 트리 재구축 비용을 피한다) 섹션이 자기
/// 소속 행을 들고 있다가 IsRowVisible 만 갱신한다. 컨테이너 Visibility 만 바뀌므로 편집 중이던
/// 값·포커스 상태가 보존된다.
/// </summary>
public sealed class SectionRowViewModel : PropertyRowViewModel
{
public SectionRowViewModel(string label) : base(label) { }
private bool isExpanded = true;
/// <summary>이 섹션에 속한 행들</summary>
public List<PropertyRowViewModel> Children { get; } = new();
/// <summary>펼침 상태</summary>
public bool IsExpanded
{
get => isExpanded;
set
{
if (!SetProperty(ref isExpanded, value))
{
return;
}
OnPropertyChanged(nameof(ChevronIcon));
foreach (var child in Children)
{
child.NotifyVisibilityChanged();
}
}
}
/// <summary>펼침 표시 아이콘</summary>
public string ChevronIcon => isExpanded ? "chevron-down" : "chevron-right";
/// <summary>접힌 상태에서 몇 개가 숨어 있는지</summary>
public string CountText => Children.Count == 0 ? string.Empty : Children.Count.ToString();
/// <summary>헤더 클릭 — 접기/펴기</summary>
public M.Framework.WPF.ICustomCommand? ToggleCommand { get; }
public SectionRowViewModel(string label) : base(label)
{
ToggleCommand = new M.Framework.WPF.Command((sender, e) => IsExpanded = !IsExpanded);
}
/// <summary>행 편입 — InspectorViewModel 이 Rows.Add 와 함께 호출</summary>
public void Adopt(PropertyRowViewModel row)
{
row.Section = this;
Children.Add(row);
OnPropertyChanged(nameof(CountText));
}
}
/// <summary>한 줄 문자열 행</summary>
@@ -93,11 +154,21 @@ public sealed class NumberRowViewModel : PropertyRowViewModel
/// <summary>토글(참/거짓) 행</summary>
public sealed class ToggleRowViewModel : PropertyRowViewModel
{
/// <summary>체크 상태 — "True"/"False" 문자열과 동기</summary>
public bool IsOn
/// <summary>
/// 체크 상태 — "True"/"False" 문자열과 동기. 다중선택에서 값이 갈리면 null(불확정)이다.
/// false 로 내리면 "전부 꺼짐"으로 보이고, 사용자가 한 번 눌러 켰다 끄면 전부 False 가 되어
/// 조용히 값이 뭉개진다.
/// </summary>
public bool? IsOn
{
get => ValueText == "True";
set => ValueText = value ? "True" : "False";
get => IsMixed ? null : ValueText == "True";
set
{
if (value is bool on)
{
ValueText = on ? "True" : "False";
}
}
}
public ToggleRowViewModel(string label) : base(label) { }
@@ -78,9 +78,39 @@ internal sealed class MainViewModel : ViewModelBase
public bool IsSheetListLoading
{
get => isSheetListLoading;
set => SetProperty(ref isSheetListLoading, value);
set
{
if (SetProperty(ref isSheetListLoading, value))
{
OnPropertyChanged(nameof(SheetListStatusText));
}
}
}
/// <summary>
/// 서식 목록 하단 상태 한 줄 — 로딩·건수·미접속을 목록 바로 아래에서 알린다.
/// 종전에는 목록 위에 "불러오는 중..." 오버레이를 덮어 목록이 가려졌고, 건수는 창 하단
/// 전역 상태줄에만 떠서 목록과 시선 거리가 멀었다.
/// </summary>
public string SheetListStatusText
{
get
{
if (!CanUseDb)
{
return "DB 미접속 — 파일 모드";
}
if (IsSheetListLoading)
{
return "불러오는 중…";
}
return SheetList.Count == 0 ? "결과 없음" : $"{SheetList.Count}건";
}
}
/// <summary>열린 문서 존재 여부 — 빈 상태 오버레이 표시 판정</summary>
public bool HasOpenDocuments => OpenDesigners.Count > 0;
/// <summary>DB 사용 가능 여부(서식 목록 패널 안내용)</summary>
public bool CanUseDb => dataBusiness.CanUseDb;
@@ -119,6 +149,7 @@ internal sealed class MainViewModel : ViewModelBase
ZoomInCommand = new Command((sender, e) => CurrentDesigner?.ZoomIn());
ZoomOutCommand = new Command((sender, e) => CurrentDesigner?.ZoomOut());
ZoomResetCommand = new Command((sender, e) => CurrentDesigner?.ZoomReset());
ZoomFitCommand = new Command((sender, e) => CurrentDesigner?.ZoomFit());
// Application.Shutdown() 은 Window.Closing 의 e.Cancel 을 무시하므로 VM 에서 먼저 확인한다
ExitCommand = new Command((sender, e) =>
{
@@ -132,6 +163,9 @@ internal sealed class MainViewModel : ViewModelBase
SearchSheetsCommand = new Command(async (sender, e) => await LoadSheetListAsync());
OpenSheetCommand = new Command((object param) => OnOpenSheetFromList(param as SheetSummary));
CloseDocumentCommand = new Command((object param) => OnCloseDocument(param as DesignerViewModel));
SheetList.CollectionChanged += (s, e) => OnPropertyChanged(nameof(SheetListStatusText));
OpenDesigners.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasOpenDocuments));
}
#endregion
@@ -187,6 +221,9 @@ internal sealed class MainViewModel : ViewModelBase
/// <summary>줌 100%</summary>
public ICustomCommand? ZoomResetCommand { get; set; }
/// <summary>화면 맞춤 — 활성 페이지 한 장이 뷰포트에 들어오는 배율</summary>
public ICustomCommand? ZoomFitCommand { get; set; }
/// <summary>종료</summary>
public ICustomCommand? ExitCommand { get; set; }
@@ -23,6 +23,7 @@
VerticalScrollBarVisibility="Auto"
Background="{DynamicResource B.CanvasBg}"
Focusable="False"
SizeChanged="OnScrollSizeChanged"
PreviewMouseWheel="OnPreviewMouseWheel"
PreviewMouseLeftButtonDown="OnPanMouseDown"
PreviewMouseMove="OnPanMouseMove"
@@ -46,9 +46,14 @@ public partial class DesignerCanvasView : UserControl
if (subscribedDesigner is not null)
{
subscribedDesigner.InlineEditRequested += ShowInlineEditor;
subscribedDesigner.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight);
}
}
/// <summary>뷰포트 크기를 VM 에 전달 — 화면 맞춤(ZoomFit)이 시각 트리를 몰라도 되게 한다</summary>
private void OnScrollSizeChanged(object sender, SizeChangedEventArgs e)
=> subscribedDesigner?.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight);
/// <summary>대상 컨트롤 위에 편집기 표시 — 폰트/정렬 일치, 줌은 World LayoutTransform 이 처리</summary>
private void ShowInlineEditor(ControlViewModel target)
{
+149 -28
View File
@@ -4,60 +4,141 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ins="clr-namespace:SheetMe.Designer.ViewModels.Inspector"
xmlns:ctl="clr-namespace:SheetMe.Designer.Controls"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance ins:InspectorViewModel}">
<UserControl.Resources>
<ctl:IconNameToVisualConverter x:Key="IconName"/>
<!-- 행 VM 타입별 암시적 템플릿 -->
<Style x:Key="RowLabel" TargetType="TextBlock">
<Setter Property="Width" Value="86"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
</Style>
<Style x:Key="RowEditor" TargetType="Control">
<Setter Property="FontSize" Value="12"/>
</Style>
<!--
"여러 값" 표시 — 다중선택에서 값이 갈리면 편집기가 빈 칸으로 보인다.
그 빈 칸은 "값 없음"과 구별되지 않아, 사용자가 여기에 무심코 입력하면 서로 다르던 값이
전부 한 값으로 덮인다. 비파괴 오버레이로만 알린다(입력을 가로막지 않는다).
-->
<Style x:Key="MixedHint" TargetType="TextBlock">
<Setter Property="Text" Value="여러 값"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontStyle" Value="Italic"/>
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="7,0,0,0"/>
<Setter Property="IsHitTestVisible" Value="False"/>
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsMixed}" Value="True"/>
<Condition Binding="{Binding ValueText}" Value=""/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
<!--
편집기 위에 겹치는 변형 — 입력 중에는 비운다.
값은 LostFocus 에만 소스로 올라가므로 ValueText 는 타이핑 중 계속 "" 이고, 겹친 안내가
방금 친 글자 위에 그대로 남는다.
-->
<Style x:Key="MixedHintOverlay" TargetType="TextBlock" BasedOn="{StaticResource MixedHint}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsKeyboardFocusWithin, RelativeSource={RelativeSource AncestorType=Grid}}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
<!-- 접이식 섹션 헤더 — 클릭으로 접기/펴기. 인스펙터는 고급 속성까지 켜면 수십 행이 된다 -->
<DataTemplate DataType="{x:Type ins:SectionRowViewModel}">
<TextBlock Text="{Binding Label}" FontWeight="Bold" Foreground="{DynamicResource B.Muted}"
Margin="0,10,0,4" FontSize="12"/>
<Button Command="{Binding ToggleCommand}" Cursor="Hand" HorizontalContentAlignment="Stretch"
Margin="0,10,0,3" Padding="0,3">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="bd" Background="Transparent" CornerRadius="5" Padding="3,4">
<DockPanel>
<ContentControl DockPanel.Dock="Left" Margin="0,0,5,0" VerticalAlignment="Center"
IsTabStop="False" Focusable="False"
Content="{Binding ChevronIcon, Converter={StaticResource IconName}, ConverterParameter=12}"/>
<TextBlock DockPanel.Dock="Right" Text="{Binding CountText}" FontSize="11"
Foreground="{DynamicResource B.Muted}" VerticalAlignment="Center" Margin="6,0,2,0"/>
<TextBlock Text="{Binding Label}" FontWeight="Bold" FontSize="12"
Foreground="{DynamicResource B.Muted}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.Hover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</DataTemplate>
<DataTemplate DataType="{x:Type ins:TextRowViewModel}">
<DockPanel Margin="0,2">
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"/>
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" ToolTip="{Binding Label}"/>
<Grid>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"/>
<TextBlock Style="{StaticResource MixedHintOverlay}"/>
</Grid>
</DockPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type ins:MultilineTextRowViewModel}">
<DockPanel Margin="0,2">
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" VerticalAlignment="Top" Margin="0,4,0,0"/>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="48" MaxHeight="120"
VerticalScrollBarVisibility="Auto"/>
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" VerticalAlignment="Top"
Margin="0,4,0,0" ToolTip="{Binding Label}"/>
<Grid>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="48" MaxHeight="120"
VerticalScrollBarVisibility="Auto"/>
<TextBlock Style="{StaticResource MixedHintOverlay}" VerticalAlignment="Top" Margin="7,7,0,0"/>
</Grid>
</DockPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type ins:NumberRowViewModel}">
<DockPanel Margin="0,2">
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"/>
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" ToolTip="{Binding Label}"/>
<Grid>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"/>
<TextBlock Style="{StaticResource MixedHintOverlay}"/>
</Grid>
</DockPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type ins:ToggleRowViewModel}">
<DockPanel Margin="0,2">
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
<CheckBox IsChecked="{Binding IsOn}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" ToolTip="{Binding Label}"/>
<!-- 3상태: 다중선택에서 값이 갈리면 불확정(IsOn=null). 꺼짐으로 보이면 안 된다 -->
<CheckBox IsChecked="{Binding IsOn}" IsThreeState="{Binding IsMixed, Mode=OneWay}"
VerticalAlignment="Center"/>
<TextBlock Style="{StaticResource MixedHint}"/>
</DockPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type ins:ChoiceRowViewModel}">
<DockPanel Margin="0,2">
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
<ComboBox ItemsSource="{Binding Choices}" SelectedItem="{Binding ValueText}" FontSize="12"/>
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" ToolTip="{Binding Label}"/>
<Grid>
<ComboBox ItemsSource="{Binding Choices}" SelectedItem="{Binding ValueText}" FontSize="12"/>
<TextBlock Style="{StaticResource MixedHintOverlay}" Margin="9,0,0,0"/>
</Grid>
</DockPanel>
</DataTemplate>
@@ -66,8 +147,11 @@
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
<Button DockPanel.Dock="Right" Content="…" Width="24" Margin="4,0,0,0"
Command="{Binding BrowseCommand}" ToolTip="목록에서 선택"/>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
ToolTip="직접 입력 또는 … 버튼으로 선택"/>
<Grid>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
ToolTip="직접 입력 또는 … 버튼으로 선택"/>
<TextBlock Style="{StaticResource MixedHintOverlay}"/>
</Grid>
</DockPanel>
</DataTemplate>
@@ -113,9 +197,12 @@
<Button DockPanel.Dock="Right" Content="…" Padding="8,1" Margin="4,0,0,0"
Command="{Binding BrowseCommand}"
ToolTip="마스크 편집기 — 자주 쓰는 마스크 목록과 미리보기"/>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
FontFamily="Consolas, D2Coding, 굴림체"
ToolTip="0=필수 숫자, 9=선택 숫자, #=숫자·부호 — 오른쪽 … 버튼에서 목록·미리보기"/>
<Grid>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
FontFamily="Consolas, D2Coding, 굴림체"
ToolTip="0=필수 숫자, 9=선택 숫자, #=숫자·부호 — 오른쪽 … 버튼에서 목록·미리보기"/>
<TextBlock Style="{StaticResource MixedHintOverlay}"/>
</Grid>
</DockPanel>
<!-- 화면에 실제로 어떻게 보이는지 — 마스크 문법을 몰라도 결과로 확인 -->
<TextBlock Text="{Binding Preview}" FontSize="11" Margin="0,2,0,0"
@@ -143,17 +230,51 @@
</ControlTemplate>
</Button.Template>
</Button>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
ToolTip="R, G, B 또는 색 이름 (예: 224, 224, 224 / White) — 오른쪽 스와치 클릭 시 피커"/>
<Grid>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
ToolTip="R, G, B 또는 색 이름 (예: 224, 224, 224 / White) — 오른쪽 스와치 클릭 시 피커"/>
<TextBlock Style="{StaticResource MixedHintOverlay}"/>
</Grid>
</DockPanel>
</DataTemplate>
</UserControl.Resources>
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="{Binding Summary}" FontWeight="Bold"
Margin="10,10,10,2" Foreground="{DynamicResource B.Muted}" TextTrimming="CharacterEllipsis"/>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Rows}" Margin="10,0,10,10"/>
</ScrollViewer>
</DockPanel>
<Grid>
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="{Binding Summary}" FontWeight="Bold"
Margin="10,10,10,2" Foreground="{DynamicResource B.Muted}" TextTrimming="CharacterEllipsis"/>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Rows}" Margin="10,0,10,10">
<!-- 접힌 섹션의 행은 컨테이너만 숨긴다 — 행 VM 을 재생성하지 않아 편집 상태가 유지된다 -->
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Style.Triggers>
<DataTrigger Binding="{Binding IsRowVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</ScrollViewer>
</DockPanel>
<!-- 빈 상태 — 선택이 없으면 "선택 없음" 한 줄만 떠 있어 패널이 고장난 것처럼 보였다 -->
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24,0">
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasSelection, FallbackValue=False}" Value="False">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<ContentControl HorizontalAlignment="Center" IsTabStop="False" Focusable="False" Opacity="0.45"
Content="{Binding Source=mouse-pointer-click, Converter={StaticResource IconName}, ConverterParameter=30}"/>
<TextBlock Text="컨트롤을 선택하면 속성이 표시됩니다" FontSize="12" TextAlignment="Center"
TextWrapping="Wrap" Foreground="{DynamicResource B.Muted}" Margin="0,12,0,0"/>
</StackPanel>
</Grid>
</UserControl>
+193 -40
View File
@@ -118,14 +118,23 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- 좌: 로고 + 메뉴 -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0"
<!-- 좌: 로고 메뉴 루트 + 편집 아이콘 스트립 ([200] 타이틀바 구성) -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="8,0,0,0"
shell:WindowChrome.IsHitTestVisibleInChrome="True">
<Image Source="pack://application:,,,/SheetMe.Designer;component/Assets/sheetme-logo.png"
Width="20" Height="20" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality"/>
<Menu VerticalAlignment="Center" Margin="8,0,0,0">
<!-- 5개 텍스트 메뉴를 로고 하나 아래로 접어 넣는다 — 타이틀바가 아이콘 스트립 자리를 확보한다 -->
<Menu VerticalAlignment="Center">
<MenuItem>
<MenuItem.Header>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Image Source="pack://application:,,,/SheetMe.Designer;component/Assets/sheetme-logo.png"
Width="20" Height="20" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality"/>
<ContentControl Margin="3,1,0,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Source=chevron-down, Converter={StaticResource IconName}, ConverterParameter=11}"/>
</StackPanel>
</MenuItem.Header>
<MenuItem Header="파일(_F)">
<MenuItem Header="새 서식(_N)" Command="{Binding NewFileCommand}" InputGestureText="Ctrl+N"/>
<MenuItem Header="열기(_O)..." Command="{Binding OpenFileCommand}" InputGestureText="Ctrl+O"/>
@@ -209,15 +218,52 @@
<MenuItem Header="상용구 관리(_W)..." Command="{Binding RecordWordCommand}"/>
<MenuItem Header="폰트 일괄 변경(_F)..." Command="{Binding FontManagerCommand}"/>
</MenuItem>
</MenuItem>
</Menu>
<Border Width="1" Height="18" Background="{DynamicResource B.Line2}" Margin="6,0" VerticalAlignment="Center"/>
<!-- 편집 아이콘 스트립 — 메뉴 하위에만 있던 명령을 한 번에 닿게 -->
<Button Style="{StaticResource Subtle}" Width="30" Height="28" Padding="0" ToolTip="실행 취소 (Ctrl+Z)"
Command="{Binding CurrentDesigner.UndoCommand}"
Content="{Binding Source=undo-2, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<Button Style="{StaticResource Subtle}" Width="30" Height="28" Padding="0" ToolTip="다시 실행 (Ctrl+Y)"
Command="{Binding CurrentDesigner.RedoCommand}"
Content="{Binding Source=redo-2, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<Border Width="1" Height="18" Background="{DynamicResource B.Line2}" Margin="6,0" VerticalAlignment="Center"/>
<Button Style="{StaticResource Subtle}" Width="30" Height="28" Padding="0" ToolTip="복사 (Ctrl+C)"
Command="{Binding CurrentDesigner.CopyCommand}"
Content="{Binding Source=copy, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<Button Style="{StaticResource Subtle}" Width="30" Height="28" Padding="0" ToolTip="삭제 (Delete)"
Command="{Binding CurrentDesigner.DeleteCommand}"
Content="{Binding Source=trash-2, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<Button Style="{StaticResource Subtle}" Width="30" Height="28" Padding="0" ToolTip="그룹 (Ctrl+G)"
Command="{Binding CurrentDesigner.GroupCommand}"
Content="{Binding Source=group, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<Button Style="{StaticResource Subtle}" Width="30" Height="28" Padding="0" ToolTip="그룹 해제 (Ctrl+Shift+G)"
Command="{Binding CurrentDesigner.UngroupCommand}"
Content="{Binding Source=ungroup, Converter={StaticResource IconName}, ConverterParameter=16}"/>
</StackPanel>
<!-- 중앙: 창 제목 -->
<TextBlock Grid.Column="1" Text="{Binding Title}" Foreground="{DynamicResource B.Muted}"
HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="12"/>
<!-- 중앙: 비움 — 서식명은 문서 탭이 이미 보여준다(Window.Title 은 작업표시줄용으로 유지) -->
<Border Grid.Column="1"/>
<!-- 우: 액션 아이콘 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,8,0"
shell:WindowChrome.IsHitTestVisibleInChrome="True">
<Button Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0" ToolTip="다크/라이트 전환"
Click="OnThemeToggleClick"
Content="{Binding Source=sun, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<Button Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0" ToolTip="DB에 저장"
Command="{Binding SaveToDbCommand}"
Content="{Binding Source=save, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<Button Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0" ToolTip="미리보기"
Command="{Binding PreviewCommand}"
Content="{Binding Source=play, Converter={StaticResource IconName}, ConverterParameter=16|B.Accent}"/>
</StackPanel>
<!-- 우: 창 제어 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" shell:WindowChrome.IsHitTestVisibleInChrome="True">
<StackPanel Grid.Column="3" Orientation="Horizontal" shell:WindowChrome.IsHitTestVisibleInChrome="True">
<Button Style="{StaticResource CaptionBtn}" Click="OnMinimizeClick" ToolTip="최소화">
<TextBlock Text="&#xE921;" FontFamily="Segoe MDL2 Assets" FontSize="10"/>
</Button>
@@ -233,19 +279,21 @@
<!-- ===== 하단 상태바 (줌은 플로팅 바로 이동) ===== -->
<Border DockPanel.Dock="Bottom" Background="{DynamicResource B.Titlebar}"
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,1,0,0" Padding="12,6">
<TextBlock Text="{Binding StatusText}" Foreground="{DynamicResource B.Muted}" FontSize="12"
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,1,0,0" Padding="14,5">
<TextBlock Text="{Binding StatusText}" Foreground="{DynamicResource B.Muted}" FontSize="11.5"
TextTrimming="CharacterEllipsis"/>
</Border>
<!-- ===== 본문 3열: 좌 탭 패널 | 문서 탭+캔버스+플로팅 바 | 속성 ([200] 레이아웃) ===== -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="232" MinWidth="180" MaxWidth="480"/>
<!-- [200] 기본 폭 — 좌 248(레이어 행에 타입 아이콘 + 토글 2개가 들어가 232 로는 이름이 잘린다),
우 300(인스펙터 X|Y 쌍이 한 줄에 들어가는 최소치) -->
<ColumnDefinition Width="248" MinWidth="180" MaxWidth="480"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="280" MinWidth="220" MaxWidth="560"/>
<ColumnDefinition Width="300" MinWidth="220" MaxWidth="560"/>
</Grid.ColumnDefinitions>
<!-- 좌: 알약 탭 — 서식 목록 / 레이어 / 도구 상자 -->
@@ -257,15 +305,37 @@
<TabItem.Header><TextBlock Text="서식 목록" Style="{StaticResource TabHeaderText}"/></TabItem.Header>
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="8,8,8,4">
<Button DockPanel.Dock="Right" Content="검색" Margin="6,0,0,0"
Command="{Binding SearchSheetsCommand}"/>
<TextBox Text="{Binding SheetSearchKeyword, UpdateSourceTrigger=PropertyChanged}"
ToolTip="서식명/코드 검색 (Enter)">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding SearchSheetsCommand}"/>
</TextBox.InputBindings>
</TextBox>
<Button DockPanel.Dock="Right" Style="{StaticResource Subtle}" Width="30" Padding="0"
Margin="6,0,0,0" ToolTip="검색 (Enter)"
Command="{Binding SearchSheetsCommand}"
Content="{Binding Source=search, Converter={StaticResource IconName}, ConverterParameter=15}"/>
<!-- 자리표시자는 TextBox 위에 겹친 TextBlock — Text 가 비었을 때만 보인다 -->
<Grid>
<TextBox x:Name="SheetSearchBox"
Text="{Binding SheetSearchKeyword, UpdateSourceTrigger=PropertyChanged}">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding SearchSheetsCommand}"/>
</TextBox.InputBindings>
</TextBox>
<TextBlock Text="서식명 또는 코드 검색" FontSize="12" Foreground="{DynamicResource B.Muted}"
Margin="9,0,0,0" VerticalAlignment="Center" IsHitTestVisible="False">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=SheetSearchBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DockPanel>
<!-- 목록 상태 한 줄 — 로딩/결과 없음/조회 실패/건수를 같은 자리에서 보여준다 -->
<TextBlock DockPanel.Dock="Bottom" Text="{Binding SheetListStatusText}" FontSize="11"
Foreground="{DynamicResource B.Muted}" Margin="9,4,9,8" TextWrapping="Wrap"/>
<Grid>
<ListBox x:Name="SheetListBox" ItemsSource="{Binding SheetList}"
Margin="4,0,4,4"
@@ -314,9 +384,6 @@
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Text="불러오는 중..." HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource B.Muted}"
Visibility="{Binding IsSheetListLoading, Converter={StaticResource BoolToVisibility}}"/>
</Grid>
</DockPanel>
</TabItem>
@@ -347,19 +414,64 @@
</ListBox>
</DockPanel>
<Border DockPanel.Dock="Top" Height="1" Background="{DynamicResource B.Line}" Margin="8,2"/>
<!-- 레이어 검색칩 — 컨트롤이 수백 개인 서식에서 이름으로 좁혀 찾는다.
내부 TextBox 에 MinHeight=0 필수: 전역 TextBox MinHeight 30 때문에 32px 칩에서 글자가 잘린다. -->
<Border DockPanel.Dock="Top" Height="32" CornerRadius="7" Margin="8,2,8,4"
Background="{DynamicResource B.Input}">
<DockPanel>
<ContentControl DockPanel.Dock="Left" Margin="9,0,7,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Source=search, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<Grid>
<TextBox x:Name="LayerSearchBox" MinHeight="0" Padding="0" BorderThickness="0"
Background="Transparent" VerticalContentAlignment="Center"
Text="{Binding CurrentDesigner.LayerFilter, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="레이어 검색…" FontSize="12" Foreground="{DynamicResource B.Muted}"
VerticalAlignment="Center" IsHitTestVisible="False" Margin="1,0,0,0">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=LayerSearchBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DockPanel>
</Border>
<!-- 레이어 목록 (활성 페이지, 그리기 순서) -->
<ListBox ItemsSource="{Binding CurrentDesigner.SelectedPage.Controls}"
<ListBox ItemsSource="{Binding CurrentDesigner.VisibleLayers}"
SelectedItem="{Binding CurrentDesigner.SelectedLayerItem}"
Margin="4,2,4,6"
Margin="4,0,4,6"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel Margin="0,1">
<CheckBox DockPanel.Dock="Right" IsChecked="{Binding IsLockedFlag}"
ToolTip="잠금" Margin="4,0,0,0"/>
<CheckBox DockPanel.Dock="Right" IsChecked="{Binding IsHiddenFlag}"
ToolTip="숨김" Margin="4,0,0,0"/>
<TextBlock Text="{Binding Id}" FontSize="12" TextTrimming="CharacterEllipsis"/>
<!-- 눈·자물쇠는 아이콘이 상태에 따라 바뀐다 — 사각 체크로는 잠김/숨김이 안 읽힌다 -->
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}"
IsChecked="{Binding IsLockedFlag}" ToolTip="잠금"
Content="{Binding LockIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}"
IsChecked="{Binding IsHiddenFlag}" ToolTip="숨김"
Content="{Binding HiddenIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<ContentControl DockPanel.Dock="Left" Margin="2,0,6,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Type, Converter={StaticResource TypeToIcon}, ConverterParameter=14|B.Accent}"/>
<TextBlock Text="{Binding LayerName}" FontSize="12" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" ToolTip="{Binding Id}">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsHiddenFlag}" Value="True">
<Setter Property="Opacity" Value="0.55"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
@@ -419,11 +531,24 @@
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<ContentControl Content="{Binding Source=file-text, Converter={StaticResource IconName}, ConverterParameter=12}"
Margin="0,0,6,0" VerticalAlignment="Center" IsTabStop="False" Focusable="False"/>
<!-- 활성 탭만 아이콘을 강조색으로 — 탭이 여러 개일 때 어디가 활성인지가 배경색 차이만으로는 약하다 -->
<ContentControl Margin="0,0,6,0" VerticalAlignment="Center" IsTabStop="False" Focusable="False">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="Content"
Value="{Binding Source=file-text, Converter={StaticResource IconName}, ConverterParameter=12}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TabItem}}" Value="True">
<Setter Property="Content"
Value="{Binding Source=file-text, Converter={StaticResource IconName}, ConverterParameter=12|B.Accent}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
<TextBlock Text="{Binding DisplayName}" MaxWidth="170" Style="{StaticResource TabHeaderText}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
<Button Content="✕" FontSize="9" Margin="7,0,0,0"
<Button Content="✕" FontSize="11" Margin="7,0,0,0"
Style="{StaticResource Subtle}" Padding="3,0" Cursor="Hand" ToolTip="문서 닫기"
Command="{Binding DataContext.CloseDocumentCommand,
RelativeSource={RelativeSource AncestorType=Window}}"
@@ -462,9 +587,7 @@
ToolTip="컨트롤 추가 — 활성 페이지 중앙에 배치" Click="OnAddControlFlyoutClick"
Content="{Binding Source=layout-grid, Converter={StaticResource IconName}, ConverterParameter=18}"/>
<Border Width="1" Height="22" Background="{DynamicResource B.Line}" Margin="5,0" VerticalAlignment="Center"/>
<Button Style="{StaticResource Subtle}" Width="30" Height="34" Padding="0" ToolTip="축소 (Ctrl+휠↓)"
Command="{Binding ZoomOutCommand}"
Content="{Binding Source=zoom-out, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<!-- ±버튼은 두지 않는다([200] 와 동일) — Ctrl+휠이 주 조작이고, 배율 버튼이 플라이아웃을 겸한다 -->
<Button x:Name="ZoomBtn" Style="{StaticResource Subtle}" Height="34" Padding="8,0,6,0" ToolTip="줌"
Click="OnZoomFlyoutClick">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
@@ -474,9 +597,6 @@
Margin="3,1,0,0" VerticalAlignment="Center" IsTabStop="False" Focusable="False"/>
</StackPanel>
</Button>
<Button Style="{StaticResource Subtle}" Width="30" Height="34" Padding="0" ToolTip="확대 (Ctrl+휠↑)"
Command="{Binding ZoomInCommand}"
Content="{Binding Source=zoom-in, Converter={StaticResource IconName}, ConverterParameter=16}"/>
</StackPanel>
</Border>
@@ -552,9 +672,42 @@
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomResetCommand}" Click="OnZoomRowClick">
<TextBlock Text="100%" FontSize="12.5" VerticalAlignment="Center"/>
</Button>
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomFitCommand}" Click="OnZoomRowClick">
<TextBlock Text="화면 맞춤" FontSize="12.5" VerticalAlignment="Center"/>
</Button>
</StackPanel>
</Border>
</Popup>
<!-- 빈 상태 — 열린 문서가 없으면 회색 허공 대신 다음 행동을 제시한다.
플로팅 바보다 뒤에 두어 위에 덮인다(문서 없이 줌·컨트롤 추가는 의미가 없다). -->
<Border Background="{DynamicResource B.CanvasBg}">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasOpenDocuments}" Value="False">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="360">
<ContentControl HorizontalAlignment="Center" IsTabStop="False" Focusable="False" Opacity="0.5"
Content="{Binding Source=file-text, Converter={StaticResource IconName}, ConverterParameter=44}"/>
<TextBlock Text="열린 서식이 없습니다" FontSize="15" FontWeight="Bold" TextAlignment="Center"
HorizontalAlignment="Center" Margin="0,16,0,6"/>
<TextBlock Foreground="{DynamicResource B.Muted}" FontSize="12.5" TextAlignment="Center"
TextWrapping="Wrap" HorizontalAlignment="Center" Margin="0,0,0,20"
Text="새 서식을 만들거나, 왼쪽 서식 목록에서 기존 서식을 두 번 눌러 엽니다."/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="새 서식" Command="{Binding NewFileCommand}" Padding="16,7" Margin="0,0,8,0"/>
<Button Content="DB에서 열기" Command="{Binding OpenFromDbCommand}" Padding="16,7"
IsEnabled="{Binding CanUseDb}"/>
<Button Content="파일 열기" Command="{Binding OpenFileCommand}" Padding="16,7" Margin="8,0,0,0"/>
</StackPanel>
</StackPanel>
</Border>
</Grid>
<GridSplitter Grid.Column="3" Style="{StaticResource ColSplitter}" ToolTip="속성 패널 너비 조절"/>