diff --git a/src/SheetMe.Designer/App.xaml.cs b/src/SheetMe.Designer/App.xaml.cs index a34d533..b7ab9cd 100644 --- a/src/SheetMe.Designer/App.xaml.cs +++ b/src/SheetMe.Designer/App.xaml.cs @@ -277,6 +277,11 @@ public partial class App : Application return Diagnostics.SnapShots.Run(args[1]); } + if (args.Length >= 1 && args[0] == "--modal-check") + { + return Diagnostics.ModalCheck.Run(); + } + if (args.Length >= 1 && args[0] == "--maxrect") { return Diagnostics.MaximizeCheck.Run(args.Length >= 2 ? args[1] : null); diff --git a/src/SheetMe.Designer/Diagnostics/ModalCheck.cs b/src/SheetMe.Designer/Diagnostics/ModalCheck.cs new file mode 100644 index 0000000..daf15a1 --- /dev/null +++ b/src/SheetMe.Designer/Diagnostics/ModalCheck.cs @@ -0,0 +1,130 @@ +using System.Windows; +using System.Windows.Threading; + +namespace SheetMe.Designer.Diagnostics; + +/// +/// 모달 가림막 자동 검증 — --modal-check. +/// +/// 무엇을 재는가. 대화상자를 실제로 ShowDialog() 로 띄우고, +/// 그 순간 화면에 가림막 창이 실제로 존재하는지 센다. +/// +/// 왜 필요했나. 처음 만든 판정이 틀렸다. ShowDialog() 는 창을 먼저 보이고 +/// (그때 Loaded 가 뜬다) 그다음에 모달 루프에 들어가므로, +/// Loaded 시점의 ComponentDispatcher.IsThreadModal 은 아직 false 다. +/// 그래서 가림막이 한 번도 안 깔렸는데 코드는 멀쩡해 보였다 — 눈으로 열어 보기 전엔 모른다. +/// +public static class ModalCheck +{ + #region Methods + public static int Run() + { + var lines = new List(); + var failed = 0; + + void Check(string label, bool ok, string detail = "") + { + lines.Add($"{(ok ? "PASS" : "FAIL")} {label}{(detail.Length > 0 ? " — " + detail : string.Empty)}"); + if (!ok) + { + failed++; + } + } + + try + { + Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; + Services.ThemeManager.Apply(light: true); + + // 실제 앱은 OnStartup 에서 이 배선을 건다. 진단 분기는 그보다 먼저 반환하므로 + // 검사에서 같은 배선을 직접 걸어야 프로덕션과 같은 것을 재게 된다. + EventManager.RegisterClassHandler(typeof(Window), FrameworkElement.LoadedEvent, + new RoutedEventHandler((sender, _) => + { + if (sender is Window loaded) + { + Services.ModalScrim.Attach(loaded); + } + })); + + // 뒤에 깔릴 창 — 실제 앱에서는 메인 셸이 이 자리다 + var owner = new Window + { + Title = "가림막 검사 — 뒤 창", + Width = 600, + Height = 400, + Left = 80, + Top = 80, + ShowInTaskbar = false, + }; + owner.Show(); + Application.Current.MainWindow = owner; + + var dialog = new Window + { + Title = "가림막 검사 — 대화상자", + Width = 300, + Height = 200, + Owner = owner, + ShowInTaskbar = false, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + + var scrimsWhileOpen = -1; + var modalAtLoaded = false; + dialog.Loaded += (_, _) => + modalAtLoaded = System.Windows.Interop.ComponentDispatcher.IsThreadModal; + + // 대화상자가 떠 있는 동안 창 목록을 세고 닫는다. + // BeginInvoke 로는 안 된다 — 이 호출이 Loaded 보다 먼저 큐에 들어가므로 + // 가림막을 거는 콜백보다 앞서 실행돼 항상 0 을 센다(실제로 그렇게 헛다리를 짚었다). + var modalAtProbe = false; + var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(400) }; + timer.Tick += (_, _) => + { + timer.Stop(); + modalAtProbe = System.Windows.Interop.ComponentDispatcher.IsThreadModal; + scrimsWhileOpen = CountScrims(); + dialog.Close(); + }; + timer.Start(); + + dialog.ShowDialog(); + + lines.Add($"Loaded 시점 IsThreadModal = {modalAtLoaded}"); + lines.Add($"모달 루프 안 IsThreadModal = {modalAtProbe}"); + lines.Add($"대화상자가 떠 있는 동안의 가림막 수 = {scrimsWhileOpen}"); + + Check("모달일 때 가림막이 깔린다", scrimsWhileOpen == 1, + $"실제 {scrimsWhileOpen}개"); + Check("닫으면 가림막이 걷힌다", CountScrims() == 0, $"실제 {CountScrims()}개"); + + owner.Close(); + } + catch (Exception ex) + { + lines.Add($"EXCEPTION {ex.GetType().Name}: {ex.Message}"); + failed++; + } + + lines.Add($"결과: 실패 {failed}건"); + Console.Error.WriteLine(string.Join(Environment.NewLine, lines)); + return failed == 0 ? 0 : 1; + } + + /// 가림막은 투명 + 작업표시줄 미표시 + 소유자 있는 창이다 + private static int CountScrims() + { + var count = 0; + foreach (Window window in Application.Current.Windows) + { + if (window.AllowsTransparency && !window.ShowInTaskbar && window.Owner is not null + && window.WindowStyle == WindowStyle.None) + { + count++; + } + } + return count; + } + #endregion +} diff --git a/src/SheetMe.Designer/Services/ModalScrim.cs b/src/SheetMe.Designer/Services/ModalScrim.cs index 88c636b..e1dca07 100644 --- a/src/SheetMe.Designer/Services/ModalScrim.cs +++ b/src/SheetMe.Designer/Services/ModalScrim.cs @@ -31,9 +31,19 @@ public static class ModalScrim /// 이 창이 모달이면 뒤에 가림막을 깐다 — 창이 보이기 시작할 때 불러야 한다. /// public static void Attach(Window window) + { + // 모달 판정을 여기서 바로 하면 안 된다. ShowDialog() 는 창을 먼저 보이고(그때 Loaded 가 온다) + // 그다음에 모달 루프에 들어가므로, 이 시점의 IsThreadModal 은 아직 false 다. + // 실제로 그렇게 만들어서 가림막이 한 번도 안 깔렸다(--modal-check 가 그 상태를 잡았다). + // 모달 프레임이 돌기 시작한 뒤에 다시 본다 — 한 프레임 늦게 깔리는 것은 눈에 띄지 않는다. + window.Dispatcher.BeginInvoke(new Action(() => AttachIfModal(window)), + System.Windows.Threading.DispatcherPriority.Background); + } + + private static void AttachIfModal(Window window) { // 모달이 아닌 창(메인 셸, 진단 렌더용 오프스크린 창)은 대상이 아니다 - if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal) + if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal || !window.IsVisible) { return; }