using System.Windows; using SheetMe.Data.Stores; using SheetMe.Designer.Services; using SheetMe.Designer.ViewModels; namespace SheetMe.Designer.Views; /// /// 메인 셸 윈도우 — 커스텀 타이틀바(로고+메뉴) | 좌(서식 목록/도구상자/페이지/레이어) | 문서 탭 | 인스펙터. /// [200]SheetMe 크롬(WindowChrome CaptionHeight 40) 이식 — 창 제어/테마 토글은 뷰 전용 관심사라 코드비하인드 처리. /// public partial class MainView : Window { public MainView() { InitializeComponent(); // 환자는 앱 전체가 하나를 공유한다 — 어느 화면에서 바뀌어도 이 아이콘이 따라와야 한다. // 창이 살아 있는 동안만 듣는다(정적 이벤트라 떼지 않으면 창이 회수되지 않는다). RefreshPatientIcon(); PatientSession.Changed += OnPatientSessionChanged; Closed += (_, _) => PatientSession.Changed -= OnPatientSessionChanged; // StaysOpen=False 팝업은 바깥 클릭(버튼 포함)으로 먼저 닫힌다 — 토글 버튼 재클릭이 곧바로 다시 열지 않게 닫힌 시각 기록 AddControlPopup.Closed += (_, _) => addControlPopupClosedAt = Environment.TickCount; ZoomPopup.Closed += (_, _) => zoomPopupClosedAt = Environment.TickCount; // 진단(게이트)은 창 크기를 스스로 정한다 — 저장된 취향이 끼어들면 샷 크기가 PC 마다 달라진다 if (StartupArguments.DiagnosticMode) { return; } // Show 전에 복원해야 한 프레임 번쩍이지 않는다 WindowPlacement.Restore(this); LeftColumn.Width = new GridLength(WindowPlacement.LeftPanelWidth(LeftColumn.Width.Value)); RightColumn.Width = new GridLength(WindowPlacement.RightPanelWidth(RightColumn.Width.Value)); } /// /// 창 핸들이 막 생긴 시점 — 최대화 보정을 건다. /// /// Loaded 가 아니라 여기다. Loaded 는 창이 이미 보인 뒤라 창 생성 시점의 첫 WM_GETMINMAXINFO 를 놓친다. /// 지금은 기동 시 최대화하지 않아 결과가 같지만, '마지막 창 상태 기억'이 생기면 /// 기동 순간 잘못된 크기가 한 프레임 번쩍인다. /// protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); MaximizeToWorkArea.Attach(this); } /// 기동 — 테마 메뉴 체크 동기화 + 뷰모델 Loaded 커맨드 실행(구 CustomWindow.LoadedCommand 대체) private void OnWindowLoaded(object sender, RoutedEventArgs e) { ThemeMenuItem.IsChecked = !ThemeManager.IsLight; // 진단(게이트) 실행에서는 Loaded 커맨드를 돌리지 않는다 — 서식 목록 DB 조회가 붙어 있어서 // 검사 결과가 접속 상태와 조회 결과에 따라 달라진다. 게이트는 DB 없이 같은 답을 내야 한다. if (StartupArguments.DiagnosticMode) { return; } if (DataContext is MainViewModel viewModel && viewModel.LoadedCommand?.CanExecute(null) == true) { viewModel.LoadedCommand.Execute(null); } } /// 서식 목록 더블클릭 → 탭으로 열기(뷰모델 위임 — ListBox 더블클릭은 InputBinding 미지원) private void OnSheetListDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) { if (DataContext is MainViewModel viewModel) { viewModel.OpenSheetFromList(SheetListBox.SelectedItem as SheetSummary); } } private void OnThemeToggleClick(object sender, RoutedEventArgs e) { ThemeManager.Toggle(); ThemeMenuItem.IsChecked = !ThemeManager.IsLight; } /// /// 환자 선택 — 여기서 고르면 앱 전체가 그 환자를 본다. /// /// 안 골랐으면 바로 선택 창을 연다. 이미 골랐으면 메뉴를 띄운다 — /// 늘 보이는 아이콘이라 "누르면 해제"로 두면 실수로 지운다. /// 바꾸기·해제를 글자로 보여 주고 고르게 하는 쪽이 예측 가능하다. /// private void OnPickPatientClick(object sender, RoutedEventArgs e) { if (!PatientSession.HasPatient) { Pick(); return; } var menu = new System.Windows.Controls.ContextMenu { PlacementTarget = PatientIconButton, Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom, }; var header = new System.Windows.Controls.MenuItem { Header = PatientSession.Label, IsEnabled = false, }; var change = new System.Windows.Controls.MenuItem { Header = "환자 바꾸기..." }; change.Click += (_, _) => Pick(); var clear = new System.Windows.Controls.MenuItem { Header = "환자 해제" }; clear.Click += (_, _) => PatientSession.Set(null, string.Empty); menu.Items.Add(header); menu.Items.Add(new System.Windows.Controls.Separator()); menu.Items.Add(change); menu.Items.Add(clear); menu.IsOpen = true; void Pick() { // 권한 문·감사 기록·문맥 읽기는 전부 Pick 안에 있다 — 호출부마다 다시 확인하면 어긋난다 if (PatientPickerDialogView.Pick(this, out var label) is { } picked) { PatientSession.Set(picked, label); } } } /// /// 아이콘이 지금 상태를 말한다 — 늘 보이는 자리라 글자를 둘 폭이 없어 으로 알린다. /// 고른 환자 이름은 툴팁에 둔다(제목줄에 환자 이름을 상시 노출하지 않는다). /// private void OnPatientSessionChanged(object? sender, EventArgs e) => RefreshPatientIcon(); private void RefreshPatientIcon() { if (PatientIconButton is null) { return; } var on = PatientSession.HasPatient; // 안 고른 상태의 색은 B.Muted 다 — 옆 아이콘들(복사·삭제·테마·저장)이 XAML 컨버터의 // 기본값으로 쓰는 바로 그 키다. 전에는 null 을 넘겼고, 그러면 아이콘이 테마 토큰이 아니라 // 하드코딩 검정으로 칠해져 어두운 제목줄에서 혼자만 까맣게 묻혔다. // (컴파일러가 CS8604 로 계속 경고하고 있었는데 그 경고가 가리키던 것이 이것이다.) PatientIconButton.Content = Controls.LucideIcons.Icon("user", 16, on ? "B.Accent" : "B.Muted"); PatientIconButton.ToolTip = on ? $"환자: {PatientSession.Label}\n누르면 바꾸기·해제" : "환자 선택 — 태그와 MDataTable 에 실제 값이 들어갑니다(앱 전체 적용)"; } /// 플로팅 바 — 컨트롤 추가 플라이아웃 토글(닫힘 직후 재클릭 가드) private void OnAddControlFlyoutClick(object sender, RoutedEventArgs e) { if (Environment.TickCount - addControlPopupClosedAt > 150) { AddControlPopup.IsOpen = true; } } /// 플로팅 바 — 줌 플라이아웃 토글 private void OnZoomFlyoutClick(object sender, RoutedEventArgs e) { if (Environment.TickCount - zoomPopupClosedAt > 150) { ZoomPopup.IsOpen = true; } } /// 플라이아웃 타일 클릭 — 활성 페이지 중앙에 컨트롤 배치(팔레트 더블클릭과 동일 경로) private void OnFlyoutTileClick(object sender, RoutedEventArgs e) { AddControlPopup.IsOpen = false; if ((sender as FrameworkElement)?.DataContext is SheetMe.Core.Catalog.ControlDescriptor item && DataContext is MainViewModel viewModel) { viewModel.CurrentDesigner?.AddPaletteItemAtCenter(item.Type); } } /// 줌 플라이아웃 행 클릭 — 커맨드 실행 후 팝업 닫기 private void OnZoomRowClick(object sender, RoutedEventArgs e) => ZoomPopup.IsOpen = false; /// 플로팅 바 — 선택 도구(기본) private void OnSelectToolClick(object sender, RoutedEventArgs e) { if (DataContext is MainViewModel viewModel) { viewModel.IsHandTool = false; } } /// 플로팅 바 — 손(팬) 도구 private void OnHandToolClick(object sender, RoutedEventArgs e) { if (DataContext is MainViewModel viewModel) { viewModel.IsHandTool = true; } } private int addControlPopupClosedAt; private int zoomPopupClosedAt; private void OnMinimizeClick(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized; private void OnMaxRestoreClick(object sender, RoutedEventArgs e) => WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; private void OnCloseWinClick(object sender, RoutedEventArgs e) => Close(); /// /// 창 닫기(타이틀바 X / Alt+F4) 전 미저장 확인 — 판단은 VM 에 위임하고 여기서는 취소만 반영한다. /// 파일▸종료는 Application.Shutdown() 이라 이 이벤트의 e.Cancel 을 무시하므로 VM 이 먼저 확인하고 /// IsShuttingDown 을 세운다(그 경로에서 여기가 다시 묻지 않도록). /// private void OnWindowClosing(object sender, System.ComponentModel.CancelEventArgs e) { if (DataContext is MainViewModel viewModel && !viewModel.IsShuttingDown && !viewModel.ConfirmCloseAll()) { e.Cancel = true; return; } // 취소되지 않은 뒤에만 저장한다 — 닫기를 물렀는데 크기가 기억되면 조작하지 않은 값이 남는다 if (!StartupArguments.DiagnosticMode) { WindowPlacement.Save(this, LeftColumn.ActualWidth, RightColumn.ActualWidth); } } /// /// F4 — 인스펙터 첫 편집 칸으로 들어간다. /// /// 전에는 인스펙터로 포커스를 보내는 키가 저장소에 하나도 없었다. /// 캔버스에서 Tab 을 누르면 좌측 패널·문서 탭·플로팅 바를 다 지나야 인스펙터에 닿았다. /// 커맨드로 만들지 않는다 — 그러면 뷰모델이 뷰의 포커스를 알아야 한다. /// private void OnWindowPreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { if (e.Key == System.Windows.Input.Key.F4) { // 인스펙터가 접혀 있으면 먼저 편다 — 안 그러면 보이지도 않는 칸으로 포커스가 들어간다 if (panelsCollapsed) { TogglePanels(); } Inspector.FocusFirstRow(); e.Handled = true; return; } // Ctrl+\ — 좌우 패널을 함께 접었다 편다('종이만 보기'). // 개별 토글 두 개를 두지 않는다: 목적이 하나라 키도 하나여야 손이 기억한다. if (e.Key is System.Windows.Input.Key.OemBackslash or System.Windows.Input.Key.Oem5 && System.Windows.Input.Keyboard.Modifiers == System.Windows.Input.ModifierKeys.Control) { TogglePanels(); e.Handled = true; return; } // Ctrl+O — 서식 목록으로. 전에는 OS 파일 대화상자로 갔다. // 손이 먼저 가는 키인데 디스크로 가고, 운영 서식 1,271건은 전부 DB 에 있다. if (e.Key == System.Windows.Input.Key.O && System.Windows.Input.Keyboard.Modifiers == System.Windows.Input.ModifierKeys.Control) { FocusSheetList(); e.Handled = true; } } /// 지금 좌우 패널이 접혀 있는가 — 접힌 폭을 '사용자가 정한 폭'으로 오해하지 않기 위한 상태 private bool panelsCollapsed; /// 펼쳤을 때의 폭 — 접는 순간 기억해 두었다가 그대로 되돌린다 private GridLength expandedLeft; private GridLength expandedRight; private double expandedLeftMin; private double expandedRightMin; /// /// 좌우 패널을 함께 접었다 편다 — 종이만 보기. /// /// 좌 248 + 스플리터 5 + 스플리터 5 + 우 300 = 558px 가 항상 점유된다. /// MinWidth 180/250 때문에 스플리터를 끝까지 밀어도 사라지지 않고, 보기 메뉴에도 항목이 없었다. /// 가로 용지를 120% 로만 올려도 캔버스에 가로 스크롤이 생기는데 '종이만 보기' 상태가 없었다. /// /// MinWidth 도 함께 0 으로 내려야 실제로 사라진다 — Width=0 만으로는 최소폭이 이긴다. /// 되돌릴 때 쓰려고 펼친 폭을 따로 기억한다(접힌 0 을 사용자가 정한 폭으로 착각하면 안 된다). /// private void TogglePanels() { if (panelsCollapsed) { LeftColumn.MinWidth = expandedLeftMin; RightColumn.MinWidth = expandedRightMin; LeftColumn.Width = expandedLeft; RightColumn.Width = expandedRight; panelsCollapsed = false; return; } expandedLeft = LeftColumn.Width; expandedRight = RightColumn.Width; expandedLeftMin = LeftColumn.MinWidth; expandedRightMin = RightColumn.MinWidth; LeftColumn.MinWidth = 0; RightColumn.MinWidth = 0; LeftColumn.Width = new GridLength(0); RightColumn.Width = new GridLength(0); panelsCollapsed = true; } /// 파일 메뉴·빈 화면 버튼에서 서식 목록으로 보낸다 private void OnOpenFromListClick(object sender, RoutedEventArgs e) => FocusSheetList(); /// 서식 목록 탭으로 옮기고 검색칸에 커서를 둔다 private void FocusSheetList() { if (DataContext is MainViewModel viewModel) { viewModel.SelectedLeftTabIndex = 0; } // 탭이 방금 바뀌어 아직 시각 트리가 없다 — 레이아웃이 한 번 돈 뒤에 잡는다 // (LayerPanelView 의 검색칸 포커스와 같은 규범). Dispatcher.BeginInvoke(new Action(() => { SheetSearchBox.Focus(); System.Windows.Input.Keyboard.Focus(SheetSearchBox); SheetSearchBox.SelectAll(); }), System.Windows.Threading.DispatcherPriority.Input); } /// /// 서식 목록 키 처리 — Enter 로 연다. /// /// 전에는 더블클릭 전용이라 키보드로는 서식을 열 수 없었다. /// 'DB에서 열기' 대화상자에는 [열기] 버튼이 있어서 그 구멍이 가려져 있었다 — 그 창을 지우기 전에 메운다. /// private void OnSheetListKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { if (e.Key != System.Windows.Input.Key.Enter || DataContext is not MainViewModel viewModel) { return; } viewModel.OpenSheetFromList(SheetListBox.SelectedItem as SheetSummary); e.Handled = true; } /// 검색칸에서 ↓ 를 누르면 목록으로 내려간다 — 검색과 선택 사이에 마우스를 두지 않는다 private void OnSheetSearchKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { if (e.Key != System.Windows.Input.Key.Down || SheetListBox.Items.Count == 0) { return; } SheetListBox.SelectedIndex = Math.Max(0, SheetListBox.SelectedIndex); SheetListBox.Focus(); if (SheetListBox.ItemContainerGenerator.ContainerFromIndex(SheetListBox.SelectedIndex) is System.Windows.Controls.ListBoxItem item) { item.Focus(); } e.Handled = true; } /// 최대화/복원 글리프 전환(Segoe MDL2: E922=최대화, E923=복원) private void OnWindowStateChanged(object sender, EventArgs e) => MaxGlyph.Text = WindowState == WindowState.Maximized ? "" : ""; }