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
}