diff --git a/src/SheetMe.Designer/Diagnostics/DialogShots.cs b/src/SheetMe.Designer/Diagnostics/DialogShots.cs index cae91a6..aeebeec 100644 --- a/src/SheetMe.Designer/Diagnostics/DialogShots.cs +++ b/src/SheetMe.Designer/Diagnostics/DialogShots.cs @@ -227,6 +227,8 @@ public static class DialogShots page.Controls.Add(NewControl("Label", "Label1", "환자명", 40, 40, 120, 24)); page.Controls.Add(NewControl("TextBox", "TextBox1", string.Empty, 170, 40, 200, 26)); page.Controls.Add(NewControl("Label", "Label2", "생년월일", 40, 80, 120, 24)); + // 이름만 있고 문구가 없는 컨트롤 — 레이어 목록에서 이름 칸이 중복 표시되지 않는지 확인용 + page.Controls.Add(NewControl("TextBox", "TextBox2", string.Empty, 170, 80, 200, 26)); document.Pages.Add(page); return new DesignerViewModel(document); } diff --git a/src/SheetMe.Designer/ViewModels/Controls/ControlViewModel.cs b/src/SheetMe.Designer/ViewModels/Controls/ControlViewModel.cs index 4e80057..80d57ee 100644 --- a/src/SheetMe.Designer/ViewModels/Controls/ControlViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Controls/ControlViewModel.cs @@ -137,8 +137,7 @@ public abstract class ControlViewModel : ViewModelBase /// 레이어 목록 표시명 — Text 가 있으면 그것, 없으면 Id. /// /// Id 는 대부분 Label12 처럼 자동 생성된 이름이라 목록에서 아무것도 알려주지 않는다. - /// 다만 Id 는 외부 참조 키이기도 하므로(DataTableField 대상명, EventHandlerMappingTag 의 - /// ShtCod_컨트롤명 규약) 버리지 않고 툴팁·검색 대상으로 함께 유지한다. + /// 그래서 앞줄은 사람이 읽는 문구로 두고, 이름은 로 따로 보여준다. /// public string LayerName { @@ -153,6 +152,21 @@ public abstract class ControlViewModel : ViewModelBase } } + /// + /// 레이어 목록에 함께 보여줄 컨트롤 이름(Id). + /// + /// 이름은 외부 참조 키다 — 액션 대상 컨트롤·입력 파라미터 컨트롤이 이름 문자열로 + /// 서로를 가리키고, CalcBox 수식은 RadioButton1+CheckBox3 처럼 이름으로 합산하며, + /// EventHandlerMappingTag 는 ShtCod_컨트롤명 규약을 쓴다. 배선을 하려면 눈에 보여야 한다. + /// + /// 표시할 문구가 없어 이 이미 Id 인 경우에는 빈 값을 돌려준다 — + /// 같은 값을 한 줄에 두 번 쓰면 좁은 행에서 이름 칸만 잡아먹는다. + /// + public string LayerId => LayerName == Id ? string.Empty : Id; + + /// 이름 칸을 그릴지 — 문구와 이름이 다를 때만 + public bool HasLayerId => LayerId.Length > 0; + /// 레이어 잠금 토글 아이콘명 public string LockIcon => Model.Locked ? "lock" : "unlock"; diff --git a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs index ce9f0de..6040acf 100644 --- a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs @@ -621,6 +621,30 @@ public sealed class DesignerViewModel : ViewModelBase #region Events /// 인라인 텍스트 편집 요청 — 캔버스 뷰(EditorLayer)가 구독 public event Action? InlineEditRequested; + + /// 캔버스를 특정 컨트롤로 스크롤 요청 — 캔버스 뷰가 구독(뷰포트를 VM 이 모르므로 위임) + public event Action? ScrollToRequested; + #endregion + + #region Methods - 컨트롤로 이동 + /// + /// 레이어 목록에서 고른 컨트롤을 선택하고 화면을 그리로 옮긴다. + /// + /// 컨트롤이 수백 개인 서식(운영 최대 1,067개)에서는 목록에서 이름을 찾아도 그게 종이 어디에 + /// 있는지 알 수 없었다. 다중 페이지 서식이면 다른 페이지에 있을 수도 있어 더 그렇다. + /// 스크롤은 뷰포트 크기를 알아야 하므로 캔버스 뷰에 위임한다. + /// + public void RevealControl(ControlViewModel viewModel) + { + // 다른 페이지의 컨트롤이면 그 페이지를 활성으로 — 안 그러면 선택만 되고 화면은 안 따라간다 + var page = PageOf(viewModel); + if (page is not null && !ReferenceEquals(page, SelectedPage)) + { + SelectedPage = page; + } + Selection.SetSingle(viewModel); + ScrollToRequested?.Invoke(viewModel); + } #endregion #region Methods - 인라인 텍스트 편집 diff --git a/src/SheetMe.Designer/Views/DesignerCanvasView.xaml.cs b/src/SheetMe.Designer/Views/DesignerCanvasView.xaml.cs index 8969445..15c97e4 100644 --- a/src/SheetMe.Designer/Views/DesignerCanvasView.xaml.cs +++ b/src/SheetMe.Designer/Views/DesignerCanvasView.xaml.cs @@ -40,16 +40,60 @@ public partial class DesignerCanvasView : UserControl if (subscribedDesigner is not null) { subscribedDesigner.InlineEditRequested -= ShowInlineEditor; + subscribedDesigner.ScrollToRequested -= ScrollToControl; } CloseInlineEditor(commit: false); subscribedDesigner = e.NewValue as DesignerViewModel; if (subscribedDesigner is not null) { subscribedDesigner.InlineEditRequested += ShowInlineEditor; + subscribedDesigner.ScrollToRequested += ScrollToControl; subscribedDesigner.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight); } } + /// + /// 컨트롤이 화면 가운데 오도록 스크롤 — 레이어 목록 더블클릭 경로. + /// + /// 월드 좌표 → 화면 좌표 변환은 World 그리드의 LayoutTransform(줌)과 감싸는 Border 의 + /// Padding 24 를 모두 반영해야 한다. 이미 보이는 컨트롤은 건드리지 않는다 — + /// 목록을 훑을 때마다 화면이 튀면 오히려 방향 감각을 잃는다. + /// + private void ScrollToControl(ControlViewModel target) + { + if (subscribedDesigner is null) + { + return; + } + // 페이지 전환·선택 변경이 레이아웃에 반영된 뒤 계산해야 오프셋이 맞는다 + Dispatcher.BeginInvoke(new Action(() => + { + var bounds = subscribedDesigner.WorldBoundsOf(target); + var zoom = subscribedDesigner.Zoom; + const double CanvasPad = 24; // DesignerCanvasView.xaml 의 Border Padding + + var left = bounds.X * zoom + CanvasPad; + var top = bounds.Y * zoom + CanvasPad; + var width = bounds.Width * zoom; + var height = bounds.Height * zoom; + + var visibleLeft = Scroll.HorizontalOffset; + var visibleTop = Scroll.VerticalOffset; + var visibleRight = visibleLeft + Scroll.ViewportWidth; + var visibleBottom = visibleTop + Scroll.ViewportHeight; + + var fullyVisible = left >= visibleLeft && top >= visibleTop + && left + width <= visibleRight && top + height <= visibleBottom; + if (fullyVisible) + { + return; + } + + Scroll.ScrollToHorizontalOffset(left + (width / 2) - (Scroll.ViewportWidth / 2)); + Scroll.ScrollToVerticalOffset(top + (height / 2) - (Scroll.ViewportHeight / 2)); + }), System.Windows.Threading.DispatcherPriority.Loaded); + } + /// 뷰포트 크기를 VM 에 전달 — 화면 맞춤(ZoomFit)이 시각 트리를 몰라도 되게 한다 private void OnScrollSizeChanged(object sender, SizeChangedEventArgs e) => subscribedDesigner?.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight); diff --git a/src/SheetMe.Designer/Views/MainView.xaml b/src/SheetMe.Designer/Views/MainView.xaml index bb81c74..bdccae2 100644 --- a/src/SheetMe.Designer/Views/MainView.xaml +++ b/src/SheetMe.Designer/Views/MainView.xaml @@ -529,8 +529,10 @@ - @@ -545,18 +547,31 @@ Content="{Binding HiddenIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/> - - - - - + + + + diff --git a/src/SheetMe.Designer/Views/MainView.xaml.cs b/src/SheetMe.Designer/Views/MainView.xaml.cs index f3209be..f7f69ab 100644 --- a/src/SheetMe.Designer/Views/MainView.xaml.cs +++ b/src/SheetMe.Designer/Views/MainView.xaml.cs @@ -38,6 +38,20 @@ public partial class MainView : Window } } + /// + /// 레이어 더블클릭 → 캔버스를 그 컨트롤로 이동. + /// 컨트롤이 수백 개인 서식에서는 목록에서 이름을 찾아도 종이 어디에 있는지 알 수 없었다. + /// (ListBox 더블클릭은 InputBinding 을 지원하지 않아 코드비하인드로 위임한다 — 서식 목록과 같은 이유) + /// + private void OnLayerDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (DataContext is MainViewModel { CurrentDesigner: { } designer } + && LayerListBox.SelectedItem is ViewModels.Controls.ControlViewModel target) + { + designer.RevealControl(target); + } + } + private void OnThemeToggleClick(object sender, RoutedEventArgs e) { ThemeManager.Toggle();