using System.Windows; using System.Windows.Media; namespace SheetMe.Designer.Services; /// /// 모달 대화상자 뒤를 어둡게 덮는다. /// /// 왜 필요한가. WPF 의 ShowDialog() 는 입력만 막고 화면은 그대로 둔다. /// 대화상자가 앱과 같은 밝기로 떠 있으면 "이게 떠 있는 동안은 뒤를 못 만진다"가 안 읽혀서, /// 뒤를 클릭했다가 아무 반응이 없으면 멈춘 것으로 오해한다. /// /// 왜 호출부마다 안 고쳤나. ShowDialog 호출부가 17곳이고 앞으로 더 는다. /// 한 곳이라도 빠지면 그 대화상자만 다르게 동작하는데, 그건 눈으로 전수 확인해야만 알 수 있다. /// 창이 뜨는 순간을 전역으로 잡아 모달일 때만 건다(WindowChromeTheme 이 제목 표시줄 색을 /// 같은 방식으로 거는 것과 같은 자리다). /// /// 진단 모드는 창을 모달이 아니라 화면 밖에 그냥 띄우므로 이 경로를 타지 않는다 — /// --dialog-shots 산출물은 영향받지 않는다. /// public static class ModalScrim { #region Member Fields /// 지금 떠 있는 가림막 — 대화상자가 겹쳐 뜨면 그만큼 쌓인다 private static readonly Stack Layers = new(); #endregion #region Methods /// /// 이 창이 모달이면 뒤에 가림막을 깐다 — 창이 보이기 시작할 때 불러야 한다. /// public static void Attach(Window window) { // 모달이 아닌 창(메인 셸, 진단 렌더용 오프스크린 창)은 대상이 아니다 if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal) { return; } var behind = window.Owner ?? Application.Current?.MainWindow; if (behind is null || ReferenceEquals(behind, window) || !behind.IsVisible) { return; } var scrim = new Window { WindowStyle = WindowStyle.None, ResizeMode = ResizeMode.NoResize, AllowsTransparency = true, Background = new SolidColorBrush(Color.FromArgb(0x66, 0, 0, 0)), ShowInTaskbar = false, ShowActivated = false, IsHitTestVisible = false, Focusable = false, WindowStartupLocation = WindowStartupLocation.Manual, Owner = behind, }; Cover(scrim, behind); // 뒤 창이 움직이거나 크기가 바뀌면 가림막도 따라가야 한다 — 안 그러면 어두운 사각형만 남는다 void Follow(object? sender, EventArgs e) => Cover(scrim, behind); behind.LocationChanged += Follow; behind.SizeChanged += Follow; behind.StateChanged += Follow; scrim.Show(); // 가림막을 나중에 띄웠으니 대화상자를 다시 앞으로 올린다 window.Activate(); Layers.Push(scrim); window.Closed += (_, _) => { behind.LocationChanged -= Follow; behind.SizeChanged -= Follow; behind.StateChanged -= Follow; if (Layers.Count > 0) { Layers.Pop(); } scrim.Close(); }; } /// 뒤 창을 정확히 덮는다 — 최대화 상태도 그 사각형 그대로다 private static void Cover(Window scrim, Window behind) { if (behind.WindowState == WindowState.Minimized) { scrim.Visibility = Visibility.Collapsed; return; } scrim.Visibility = Visibility.Visible; // 최대화된 창은 Left/Top 이 '복원했을 때의 값'이라 그대로 쓰면 가림막이 엉뚱한 데 깔린다. // 화면 좌표를 직접 물어 DIP 로 되돌린다. var origin = new Point(behind.Left, behind.Top); if (PresentationSource.FromVisual(behind) is { CompositionTarget: { } target }) { origin = target.TransformFromDevice.Transform(behind.PointToScreen(new Point(0, 0))); } scrim.Left = origin.X; scrim.Top = origin.Y; scrim.Width = behind.ActualWidth > 0 ? behind.ActualWidth : behind.Width; scrim.Height = behind.ActualHeight > 0 ? behind.ActualHeight : behind.Height; } #endregion }