From 9d20cc2971c1dbde92f027ac37341c5f05455daa Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 12 Aug 2026 10:33:48 +0900 Subject: [PATCH] =?UTF-8?q?[200]=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=ED=99=94=202/2=20=E2=80=94=20=EC=A2=8C=EC=B8=A1=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=C2=B7=ED=83=80=EC=9D=B4=ED=8B=80=EB=B0=94?= =?UTF-8?q?=C2=B7=EC=9D=B8=EC=8A=A4=ED=8E=99=ED=84=B0=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EB=B0=80=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ViewModels/Controls/ControlViewModel.cs | 28 +++ .../ViewModels/DesignerViewModel.cs | 79 +++++- .../Inspector/InspectorViewModel.cs | 42 +++- .../ViewModels/Inspector/PropertyRows.cs | 83 ++++++- .../ViewModels/MainViewModel.cs | 39 ++- .../Views/DesignerCanvasView.xaml | 1 + .../Views/DesignerCanvasView.xaml.cs | 5 + src/SheetMe.Designer/Views/InspectorView.xaml | 177 ++++++++++--- src/SheetMe.Designer/Views/MainView.xaml | 233 +++++++++++++++--- 9 files changed, 600 insertions(+), 87 deletions(-) diff --git a/src/SheetMe.Designer/ViewModels/Controls/ControlViewModel.cs b/src/SheetMe.Designer/ViewModels/Controls/ControlViewModel.cs index e79d033..4e80057 100644 --- a/src/SheetMe.Designer/ViewModels/Controls/ControlViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Controls/ControlViewModel.cs @@ -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)); } } /// 디자인 표시 불투명도 — 숨김이면 0.25 public double DesignOpacity => Model.Hidden ? 0.25 : 1.0; + + /// + /// 레이어 목록 표시명 — Text 가 있으면 그것, 없으면 Id. + /// + /// Id 는 대부분 Label12 처럼 자동 생성된 이름이라 목록에서 아무것도 알려주지 않는다. + /// 다만 Id 는 외부 참조 키이기도 하므로(DataTableField 대상명, EventHandlerMappingTag 의 + /// ShtCod_컨트롤명 규약) 버리지 않고 툴팁·검색 대상으로 함께 유지한다. + /// + 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), "…"); + } + } + + /// 레이어 잠금 토글 아이콘명 + public string LockIcon => Model.Locked ? "lock" : "unlock"; + + /// 레이어 숨김 토글 아이콘명 + public string HiddenIcon => Model.Hidden ? "eye-off" : "eye"; #endregion #region Constructors diff --git a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs index 8da199c..ce9f0de 100644 --- a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs @@ -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 tabOrderSequence = new(); private readonly Dictionary? 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)); } } } + /// + /// 레이어 검색어 — 이름(LayerName)·Id 부분일치. 서식당 컨트롤이 수백 개인 경우가 흔해 + /// (운영 최대 1,067개) 스크롤만으로는 대상을 찾을 수 없다. + /// + public string LayerFilter + { + get => layerFilter; + set + { + if (SetProperty(ref layerFilter, value ?? string.Empty)) + { + OnPropertyChanged(nameof(VisibleLayers)); + } + } + } + + /// + /// 레이어 목록에 실제로 표시할 항목 — 활성 페이지의 컨트롤에 검색어를 적용한 결과. + /// 검색어가 비면 원본 컬렉션을 그대로 돌려준다(항목 추가·삭제가 즉시 반영되도록). + /// + 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(); + } + } + /// 레이어 목록 선택 항목 — 캔버스 선택과 동기 public ControlViewModel? SelectedLayerItem { @@ -401,6 +444,38 @@ public sealed class DesignerViewModel : ViewModelBase /// 줌 100% public void ZoomReset() => Zoom = 1.0; + + /// + /// 캔버스 뷰포트 크기 통지 — DesignerCanvasView 가 SizeChanged 에서 호출한다. + /// ZoomFit 은 뷰포트를 알아야 하는데 VM 은 시각 트리를 모르므로 값만 받아 둔다. + /// + public void ReportViewport(double width, double height) + { + viewportWidth = width; + viewportHeight = height; + } + + /// + /// 화면 맞춤 — 활성 페이지 한 장이 뷰포트에 들어오는 배율. + /// 문서 전체(WorldHeight)가 아니라 페이지 한 장 기준이다. 7페이지 서식을 통째로 맞추면 + /// 14% 가 되어 아무것도 안 보인다(운영 최대 7페이지). + /// + 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 - 좌표/히트테스트 diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs index fb5aabb..1b16e73 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs @@ -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; /// 공통/글꼴 섹션이 이미 다루는 키 — 고급 목록에서 제외 @@ -33,6 +34,9 @@ public sealed class InspectorViewModel : ViewModelBase /// 행 목록(섹션 헤더 포함) public ObservableCollection Rows { get; } = new(); + /// 선택된 컨트롤이 있는지 — 인스펙터 빈 상태 판정 + public bool HasSelection => designer.Selection.Items.Count > 0; + /// 선택 요약 텍스트 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(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 }; } + /// 섹션 헤더 시작 — 이후 추가되는 행은 이 섹션에 편입된다 + private void AddSection(string label) + { + currentSection = new SectionRowViewModel(label); + Rows.Add(currentSection); + } + + /// 바인딩 없는 행 추가(읽기 전용·버튼 행) — 섹션 편입만 처리 + 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); } diff --git a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs index 6bb6ddc..e0a5d42 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs @@ -21,6 +21,15 @@ public abstract class PropertyRowViewModel : ViewModelBase /// 선택 대상들의 값이 서로 다른지 — "여러 값" 표시 public bool IsMixed { get; private set; } + /// 소속 섹션 — null 이면 섹션 밖(항상 표시) + public SectionRowViewModel? Section { get; set; } + + /// 접힌 섹션에 속하면 숨긴다 — ItemContainerStyle 이 바인딩 + public bool IsRowVisible => Section is null || Section.IsExpanded; + + /// 표시 여부 통지(섹션 접기/펴기) + public void NotifyVisibilityChanged() => OnPropertyChanged(nameof(IsRowVisible)); + /// 정규화 문자열 값 — 파생 편집기가 형 변환 public string ValueText { @@ -59,6 +68,9 @@ public abstract class PropertyRowViewModel : ViewModelBase OnPropertyChanged(nameof(ValueText)); OnPropertyChanged(nameof(IsMixed)); building = false; + // 파생 표시(토글 3상태·미리보기·요약)도 초기값 기준으로 맞춘다. + // 드래그 중 RefreshBoundsRows 가 Initialize 만 호출하는 경로가 있어 여기서 하지 않으면 파생이 뒤처진다. + OnValueApplied(); } /// 값 적용 후 파생 갱신 지점 @@ -66,10 +78,59 @@ public abstract class PropertyRowViewModel : ViewModelBase #endregion } -/// 구분 헤더 행 +/// +/// 구분 헤더 행 — 클릭으로 접기/펴기. +/// +/// Rows 는 평면 컬렉션을 유지하고(다중선택 재구성이 잦아 트리 재구축 비용을 피한다) 섹션이 자기 +/// 소속 행을 들고 있다가 IsRowVisible 만 갱신한다. 컨테이너 Visibility 만 바뀌므로 편집 중이던 +/// 값·포커스 상태가 보존된다. +/// public sealed class SectionRowViewModel : PropertyRowViewModel { - public SectionRowViewModel(string label) : base(label) { } + private bool isExpanded = true; + + /// 이 섹션에 속한 행들 + public List Children { get; } = new(); + + /// 펼침 상태 + public bool IsExpanded + { + get => isExpanded; + set + { + if (!SetProperty(ref isExpanded, value)) + { + return; + } + OnPropertyChanged(nameof(ChevronIcon)); + foreach (var child in Children) + { + child.NotifyVisibilityChanged(); + } + } + } + + /// 펼침 표시 아이콘 + public string ChevronIcon => isExpanded ? "chevron-down" : "chevron-right"; + + /// 접힌 상태에서 몇 개가 숨어 있는지 + public string CountText => Children.Count == 0 ? string.Empty : Children.Count.ToString(); + + /// 헤더 클릭 — 접기/펴기 + public M.Framework.WPF.ICustomCommand? ToggleCommand { get; } + + public SectionRowViewModel(string label) : base(label) + { + ToggleCommand = new M.Framework.WPF.Command((sender, e) => IsExpanded = !IsExpanded); + } + + /// 행 편입 — InspectorViewModel 이 Rows.Add 와 함께 호출 + public void Adopt(PropertyRowViewModel row) + { + row.Section = this; + Children.Add(row); + OnPropertyChanged(nameof(CountText)); + } } /// 한 줄 문자열 행 @@ -93,11 +154,21 @@ public sealed class NumberRowViewModel : PropertyRowViewModel /// 토글(참/거짓) 행 public sealed class ToggleRowViewModel : PropertyRowViewModel { - /// 체크 상태 — "True"/"False" 문자열과 동기 - public bool IsOn + /// + /// 체크 상태 — "True"/"False" 문자열과 동기. 다중선택에서 값이 갈리면 null(불확정)이다. + /// false 로 내리면 "전부 꺼짐"으로 보이고, 사용자가 한 번 눌러 켰다 끄면 전부 False 가 되어 + /// 조용히 값이 뭉개진다. + /// + 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) { } diff --git a/src/SheetMe.Designer/ViewModels/MainViewModel.cs b/src/SheetMe.Designer/ViewModels/MainViewModel.cs index 04a27a4..ff387be 100644 --- a/src/SheetMe.Designer/ViewModels/MainViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/MainViewModel.cs @@ -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)); + } + } } + /// + /// 서식 목록 하단 상태 한 줄 — 로딩·건수·미접속을 목록 바로 아래에서 알린다. + /// 종전에는 목록 위에 "불러오는 중..." 오버레이를 덮어 목록이 가려졌고, 건수는 창 하단 + /// 전역 상태줄에만 떠서 목록과 시선 거리가 멀었다. + /// + public string SheetListStatusText + { + get + { + if (!CanUseDb) + { + return "DB 미접속 — 파일 모드"; + } + if (IsSheetListLoading) + { + return "불러오는 중…"; + } + return SheetList.Count == 0 ? "결과 없음" : $"{SheetList.Count}건"; + } + } + + /// 열린 문서 존재 여부 — 빈 상태 오버레이 표시 판정 + public bool HasOpenDocuments => OpenDesigners.Count > 0; + /// DB 사용 가능 여부(서식 목록 패널 안내용) 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 /// 줌 100% public ICustomCommand? ZoomResetCommand { get; set; } + /// 화면 맞춤 — 활성 페이지 한 장이 뷰포트에 들어오는 배율 + public ICustomCommand? ZoomFitCommand { get; set; } + /// 종료 public ICustomCommand? ExitCommand { get; set; } diff --git a/src/SheetMe.Designer/Views/DesignerCanvasView.xaml b/src/SheetMe.Designer/Views/DesignerCanvasView.xaml index a5205ea..ab23ae2 100644 --- a/src/SheetMe.Designer/Views/DesignerCanvasView.xaml +++ b/src/SheetMe.Designer/Views/DesignerCanvasView.xaml @@ -23,6 +23,7 @@ VerticalScrollBarVisibility="Auto" Background="{DynamicResource B.CanvasBg}" Focusable="False" + SizeChanged="OnScrollSizeChanged" PreviewMouseWheel="OnPreviewMouseWheel" PreviewMouseLeftButtonDown="OnPanMouseDown" PreviewMouseMove="OnPanMouseMove" diff --git a/src/SheetMe.Designer/Views/DesignerCanvasView.xaml.cs b/src/SheetMe.Designer/Views/DesignerCanvasView.xaml.cs index 52d3c4b..8969445 100644 --- a/src/SheetMe.Designer/Views/DesignerCanvasView.xaml.cs +++ b/src/SheetMe.Designer/Views/DesignerCanvasView.xaml.cs @@ -46,9 +46,14 @@ public partial class DesignerCanvasView : UserControl if (subscribedDesigner is not null) { subscribedDesigner.InlineEditRequested += ShowInlineEditor; + subscribedDesigner.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight); } } + /// 뷰포트 크기를 VM 에 전달 — 화면 맞춤(ZoomFit)이 시각 트리를 몰라도 되게 한다 + private void OnScrollSizeChanged(object sender, SizeChangedEventArgs e) + => subscribedDesigner?.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight); + /// 대상 컨트롤 위에 편집기 표시 — 폰트/정렬 일치, 줌은 World LayoutTransform 이 처리 private void ShowInlineEditor(ControlViewModel target) { diff --git a/src/SheetMe.Designer/Views/InspectorView.xaml b/src/SheetMe.Designer/Views/InspectorView.xaml index 630cd47..f9c4c92 100644 --- a/src/SheetMe.Designer/Views/InspectorView.xaml +++ b/src/SheetMe.Designer/Views/InspectorView.xaml @@ -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}"> + + + + + + + + + - + - - + + + + + - - + + + + + - - + + + + + - - + + + + - - + + + + + @@ -66,8 +147,11 @@ - + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SheetMe.Designer/Views/MainView.xaml b/src/SheetMe.Designer/Views/MainView.xaml index 149c1ff..cada4b6 100644 --- a/src/SheetMe.Designer/Views/MainView.xaml +++ b/src/SheetMe.Designer/Views/MainView.xaml @@ -118,14 +118,23 @@ + - - + - - + + + + + + + + + @@ -209,15 +218,52 @@ + + + + + + @@ -233,19 +279,21 @@ - + - + + - + @@ -257,15 +305,37 @@ - - + + + + + + + + + + + + +