using System.Windows; using System.Windows.Input; using SheetMe.Designer.Controls; using SheetMe.Designer.Services; namespace SheetMe.Designer.Views; /// /// 알림·확인 대화상자 — 순정 Win32 MessageBox 대체. /// /// 기각 의미론은 MessageBox 를 그대로 따른다: 닫기(X)·Alt+F4·Esc 는 /// 확인 1개면 '확인', 예/아니오면 '아니오', 예/아니오/취소면 '취소'로 돌아간다. /// /// 를 쓰지 않는다. Show() 로 띄운 창에 대입하면 /// InvalidOperationException 이고, 진단 렌더러(DialogShots)가 정확히 그 경로로 이 창을 찍는다. /// 같은 이유로 Closing 에서 e.Cancel 로 닫기를 막지도 않는다 — /// 렌더러가 Close() 로만 창을 정리하므로 취소되면 화면 밖 창이 살아남는다. /// public partial class MessageDialogView : Window { #region Member Fields private readonly string caption; private readonly string head; private readonly string? detail; private bool? answer; #endregion #region Properties /// 사용자 응답 — 예=true / 아니오=false / 취소=null public bool? Answer => answer; #endregion #region Constructors /// /// 공개 생성자 — 정적 헬퍼로 감싸지 말고 이 형태를 유지할 것. /// 진단 렌더러가 팩토리로 new 해서 라이트·다크 스냅샷을 찍는다(private 생성자면 편입 불가). /// public MessageDialogView(DialogKind kind, string caption, string head, string? detail, DialogButtons buttons, string yes = "예", string no = "아니오", bool destructive = false) { InitializeComponent(); this.caption = caption; this.head = head; this.detail = detail; // 노트북 해상도에서 창이 화면을 덮지 않도록 한 번 더 조인다 MaxHeight = Math.Min(620, SystemParameters.WorkArea.Height * 0.85); // 뒤를 어둡게 할 자격 — 지금 멈추고 결정해야 하는 것만. // 물음(되돌릴 수 없는 쓰기의 확인)과 오류(진행이 정의되지 않는 차단)가 그것이다. // 알림·경고는 읽고 지나가는 것이라 뒤를 가리지 않는다. Services.ModalScrim.SetDimBehind(this, kind is DialogKind.Question or DialogKind.Error); CaptionText.Text = caption; HeadText.Text = head; DetailText.Text = detail ?? string.Empty; DetailText.Visibility = string.IsNullOrEmpty(detail) ? Visibility.Collapsed : Visibility.Visible; var (icon, brushKey) = Visual(kind); // 색은 반드시 '키'로 넘긴다 — Brush 인스턴스를 대입하면 그 시점 값으로 굳어 테마 전환을 못 따라간다 SeverityIcon.Content = LucideIcons.Icon(icon, 22, brushKey); // 오류만 복사 버튼을 노출한다 — 오류 코드를 전화로 불러 주던 실사용 경로 CopyButton.Visibility = kind == DialogKind.Error ? Visibility.Visible : Visibility.Collapsed; Configure(buttons, yes, no, destructive); } #endregion #region Methods /// 성격 → (아이콘, 색 토큰) private static (string Icon, string BrushKey) Visual(DialogKind kind) => kind switch { DialogKind.Error => ("circle-alert", "B.Danger"), DialogKind.Warning => ("triangle-alert", "B.Warning"), DialogKind.Question => ("circle-help", "B.AccentText"), _ => ("info", "B.AccentText"), }; /// 버튼 구성·기본 응답·초기 포커스 private void Configure(DialogButtons buttons, string yes, string no, bool destructive) { switch (buttons) { case DialogButtons.YesNo: answer = false; AffirmButton.Content = yes; DenyButton.Content = no; DenyButton.Visibility = Visibility.Visible; DenyButton.IsCancel = true; break; case DialogButtons.YesNoCancel: answer = null; AffirmButton.Content = yes; DenyButton.Content = no; DenyButton.Visibility = Visibility.Visible; CancelButton.Visibility = Visibility.Visible; CancelButton.IsCancel = true; break; default: answer = true; AffirmButton.Content = "확인"; AffirmButton.IsCancel = true; break; } // 되돌리기 어려운 확인에서는 기본 버튼을 부정 쪽에 둔다 — Enter 한 번에 페이지가 지워지면 안 된다 var defaultButton = destructive && DenyButton.Visibility == Visibility.Visible ? DenyButton : AffirmButton; defaultButton.IsDefault = true; Loaded += (_, _) => defaultButton.Focus(); } private void OnAffirmClick(object sender, RoutedEventArgs e) { answer = true; Close(); } private void OnDenyClick(object sender, RoutedEventArgs e) { answer = false; Close(); } /// 닫기(X)·취소 — 생성자에서 정한 기각 기본값을 그대로 두고 닫는다 private void OnDismissClick(object sender, RoutedEventArgs e) => Close(); /// 본문이 넘칠 때만 버튼 줄 위에 구분선 — 창이 최대 높이에 걸렸다는 유일한 신호 private void OnBodyScrollChanged(object sender, System.Windows.Controls.ScrollChangedEventArgs e) => Footer.BorderThickness = new Thickness(0, BodyScroll.ScrollableHeight > 0.5 ? 1 : 0, 0, 0); /// 내용 복사 — MessageBox 의 Ctrl+C 동작 보존(오류 코드를 옮겨 적던 경로) private void OnCopyClick(object sender, RoutedEventArgs e) => CopyToClipboard(); protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.C && Keyboard.Modifiers == ModifierKeys.Control) { CopyToClipboard(); e.Handled = true; return; } base.OnKeyDown(e); } private void CopyToClipboard() { var text = string.IsNullOrEmpty(detail) ? $"{caption}\n---\n{head}" : $"{caption}\n---\n{head}\n\n{detail}"; try { Clipboard.SetText(text); } catch (Exception ex) { // 클립보드는 다른 프로세스가 잠글 수 있다 — 복사 실패가 알림 자체를 막으면 안 된다 AppLog.Warn($"알림 내용 복사 실패: {ex.Message}"); } } #endregion }