Initial commit to new repository
This commit is contained in:
350
src/AxCopilot/Views/CustomMessageBox.cs
Normal file
350
src/AxCopilot/Views/CustomMessageBox.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 MessageBox를 대체하는 커스텀 다이얼로그.
|
||||
/// 테마 리소스를 사용하여 앱 디자인과 일관된 모습을 제공합니다.
|
||||
/// </summary>
|
||||
internal sealed class CustomMessageBox : Window
|
||||
{
|
||||
private MessageBoxResult _result = MessageBoxResult.None;
|
||||
|
||||
private CustomMessageBox(string message, string title, MessageBoxButton buttons, MessageBoxImage icon)
|
||||
{
|
||||
Title = title;
|
||||
Width = 400;
|
||||
MinWidth = 320;
|
||||
MaxWidth = 520;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
Topmost = true; // 다른 창 위에 표시
|
||||
|
||||
var bgBrush = Application.Current.TryFindResource("LauncherBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||||
var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
||||
|
||||
// 루트 컨테이너
|
||||
var root = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
CornerRadius = new CornerRadius(16),
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(28, 24, 28, 20),
|
||||
Effect = new DropShadowEffect
|
||||
{
|
||||
BlurRadius = 24, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black,
|
||||
},
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
// 타이틀 바 (드래그 가능)
|
||||
var titleBar = new Grid { Margin = new Thickness(0, 0, 0, 16) };
|
||||
titleBar.MouseLeftButtonDown += (_, _) => { try { DragMove(); } catch { } };
|
||||
|
||||
// 아이콘 + 제목
|
||||
var titlePanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
|
||||
var (iconText, iconColor) = GetIconInfo(icon);
|
||||
if (!string.IsNullOrEmpty(iconText))
|
||||
{
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = iconText,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 18,
|
||||
Foreground = iconColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
}
|
||||
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
titleBar.Children.Add(titlePanel);
|
||||
|
||||
// 닫기 버튼
|
||||
var closeBtn = new Button
|
||||
{
|
||||
Content = "\uE8BB",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(6),
|
||||
Cursor = Cursors.Hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
closeBtn.Click += (_, _) => { _result = MessageBoxResult.Cancel; Close(); };
|
||||
titleBar.Children.Add(closeBtn);
|
||||
stack.Children.Add(titleBar);
|
||||
|
||||
// 메시지 본문
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = message,
|
||||
FontSize = 13,
|
||||
Foreground = primaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
LineHeight = 20,
|
||||
Margin = new Thickness(0, 0, 0, 24),
|
||||
});
|
||||
|
||||
// 버튼 영역
|
||||
var btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
};
|
||||
|
||||
switch (buttons)
|
||||
{
|
||||
case MessageBoxButton.OK:
|
||||
btnPanel.Children.Add(CreateButton("확인", accentBrush, true, MessageBoxResult.OK));
|
||||
break;
|
||||
case MessageBoxButton.OKCancel:
|
||||
btnPanel.Children.Add(CreateButton("취소", itemBg, false, MessageBoxResult.Cancel));
|
||||
btnPanel.Children.Add(CreateButton("확인", accentBrush, true, MessageBoxResult.OK));
|
||||
break;
|
||||
case MessageBoxButton.YesNo:
|
||||
btnPanel.Children.Add(CreateButton("아니오", itemBg, false, MessageBoxResult.No));
|
||||
btnPanel.Children.Add(CreateButton("예", accentBrush, true, MessageBoxResult.Yes));
|
||||
break;
|
||||
case MessageBoxButton.YesNoCancel:
|
||||
btnPanel.Children.Add(CreateButton("취소", itemBg, false, MessageBoxResult.Cancel));
|
||||
btnPanel.Children.Add(CreateButton("아니오", itemBg, false, MessageBoxResult.No));
|
||||
btnPanel.Children.Add(CreateButton("예", accentBrush, true, MessageBoxResult.Yes));
|
||||
break;
|
||||
}
|
||||
|
||||
stack.Children.Add(btnPanel);
|
||||
root.Child = stack;
|
||||
Content = root;
|
||||
|
||||
// ESC로 닫기
|
||||
KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
_result = buttons == MessageBoxButton.YesNo ? MessageBoxResult.No : MessageBoxResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
else if (e.Key == Key.Enter)
|
||||
{
|
||||
_result = buttons switch
|
||||
{
|
||||
MessageBoxButton.YesNo or MessageBoxButton.YesNoCancel => MessageBoxResult.Yes,
|
||||
_ => MessageBoxResult.OK,
|
||||
};
|
||||
Close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Button CreateButton(string text, Brush bg, bool isPrimary, MessageBoxResult result)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Content = text,
|
||||
FontSize = 12.5,
|
||||
FontWeight = isPrimary ? FontWeights.SemiBold : FontWeights.Normal,
|
||||
Foreground = isPrimary ? Brushes.White : (Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White),
|
||||
Background = bg,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(20, 8, 20, 8),
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
MinWidth = 80,
|
||||
};
|
||||
|
||||
// 라운드 코너 템플릿
|
||||
var template = new ControlTemplate(typeof(Button));
|
||||
var border = new FrameworkElementFactory(typeof(Border));
|
||||
border.SetValue(Border.BackgroundProperty, bg);
|
||||
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(10));
|
||||
border.SetValue(Border.PaddingProperty, new Thickness(20, 8, 20, 8));
|
||||
var cp = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
cp.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
||||
cp.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
border.AppendChild(cp);
|
||||
template.VisualTree = border;
|
||||
btn.Template = template;
|
||||
|
||||
btn.Click += (_, _) => { _result = result; Close(); };
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static (string icon, Brush color) GetIconInfo(MessageBoxImage image) => image switch
|
||||
{
|
||||
MessageBoxImage.Error => ("\uEA39", new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E))),
|
||||
MessageBoxImage.Warning => ("\uE7BA", new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20))),
|
||||
MessageBoxImage.Information => ("\uE946", new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))),
|
||||
MessageBoxImage.Question => ("\uE9CE", new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))),
|
||||
_ => ("", Brushes.Transparent),
|
||||
};
|
||||
|
||||
// ─── 정적 호출 메서드 (기존 MessageBox.Show 시그니처 호환) ──────────────
|
||||
|
||||
public static MessageBoxResult Show(string message)
|
||||
=> Show(message, "AX Copilot", MessageBoxButton.OK, MessageBoxImage.None);
|
||||
|
||||
public static MessageBoxResult Show(string message, string title)
|
||||
=> Show(message, title, MessageBoxButton.OK, MessageBoxImage.None);
|
||||
|
||||
public static MessageBoxResult Show(string message, string title, MessageBoxButton buttons)
|
||||
=> Show(message, title, buttons, MessageBoxImage.None);
|
||||
|
||||
public static MessageBoxResult Show(string message, string title, MessageBoxButton buttons, MessageBoxImage icon)
|
||||
{
|
||||
var dlg = new CustomMessageBox(message, title, buttons, icon);
|
||||
|
||||
// 등장 애니메이션 (페이드인 + 스케일)
|
||||
dlg.Opacity = 0;
|
||||
dlg.Loaded += (_, _) =>
|
||||
{
|
||||
var fadeIn = new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(150));
|
||||
dlg.BeginAnimation(OpacityProperty, fadeIn);
|
||||
};
|
||||
|
||||
// ─── 부모 Window 하단에 토스트 알림 표시 ───
|
||||
var parentWindow = Application.Current.Windows.OfType<Window>()
|
||||
.FirstOrDefault(w => w.IsActive && w is not CustomMessageBox)
|
||||
?? Application.Current.MainWindow;
|
||||
|
||||
Border? toast = null;
|
||||
Grid? rootGrid = null;
|
||||
|
||||
if (parentWindow?.Content is Border outerBorder && outerBorder.Child is Grid grid)
|
||||
{
|
||||
rootGrid = grid;
|
||||
toast = CreateToastBanner(title, icon);
|
||||
rootGrid.Children.Add(toast);
|
||||
}
|
||||
else if (parentWindow?.Content is Grid directGrid)
|
||||
{
|
||||
rootGrid = directGrid;
|
||||
toast = CreateToastBanner(title, icon);
|
||||
rootGrid.Children.Add(toast);
|
||||
}
|
||||
|
||||
dlg.ShowDialog();
|
||||
|
||||
// 다이얼로그 닫힌 후 토스트 페이드아웃
|
||||
if (toast != null && rootGrid != null)
|
||||
{
|
||||
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(400))
|
||||
{
|
||||
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn },
|
||||
};
|
||||
fadeOut.Completed += (_, _) => rootGrid.Children.Remove(toast);
|
||||
toast.BeginAnimation(OpacityProperty, fadeOut);
|
||||
}
|
||||
|
||||
return dlg._result;
|
||||
}
|
||||
|
||||
/// <summary>하단 토스트 배너 생성.</summary>
|
||||
private static Border CreateToastBanner(string title, MessageBoxImage icon)
|
||||
{
|
||||
var (iconText, iconColor) = GetIconInfo(icon);
|
||||
if (string.IsNullOrEmpty(iconText))
|
||||
{
|
||||
iconText = "\uE946"; // default info icon
|
||||
iconColor = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
}
|
||||
|
||||
var panel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = iconText,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = iconColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 11.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
|
||||
var toast = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0xE8, 0x2A, 0x2B, 0x40)),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(16, 8, 16, 8),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
Margin = new Thickness(0, 0, 0, 12),
|
||||
Effect = new DropShadowEffect
|
||||
{
|
||||
BlurRadius = 12, ShadowDepth = 2, Opacity = 0.3, Color = Colors.Black,
|
||||
},
|
||||
Opacity = 0,
|
||||
Child = panel,
|
||||
};
|
||||
|
||||
// 모든 Grid Row/Column 스팬하도록 설정
|
||||
Grid.SetColumnSpan(toast, 10);
|
||||
Grid.SetRowSpan(toast, 10);
|
||||
|
||||
// 슬라이드업 + 페이드인 애니메이션
|
||||
var translate = new TranslateTransform(0, 20);
|
||||
toast.RenderTransform = translate;
|
||||
|
||||
toast.Loaded += (_, _) =>
|
||||
{
|
||||
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(250))
|
||||
{
|
||||
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut },
|
||||
};
|
||||
var slideUp = new DoubleAnimation(20, 0, TimeSpan.FromMilliseconds(300))
|
||||
{
|
||||
EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.3 },
|
||||
};
|
||||
toast.BeginAnimation(OpacityProperty, fadeIn);
|
||||
translate.BeginAnimation(TranslateTransform.YProperty, slideUp);
|
||||
};
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
/// <summary>WinForms 호환 — 인스톨러에서도 사용 가능하도록 Window 없이 표시.</summary>
|
||||
public static MessageBoxResult ShowStandalone(string message, string title,
|
||||
MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None)
|
||||
{
|
||||
var dlg = new CustomMessageBox(message, title, buttons, icon);
|
||||
dlg.ShowDialog();
|
||||
return dlg._result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user