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 ? "" : "";
}