using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Text; using System.Runtime.InteropServices; using System.Windows.Forms; namespace AxCopilot.Views; /// /// 모던 카드 스타일 트레이 컨텍스트 메뉴 팩토리. /// 배경색은 현재 WPF 테마 리소스를 실시간으로 읽어 적용합니다. /// internal static class TrayContextMenu { // Segoe MDL2 Assets 글리프 상수 internal const string GlyphOpen = "\uE7C5"; // Launch internal const string GlyphSettings = "\uE713"; // Settings internal const string GlyphReload = "\uE72C"; // Refresh internal const string GlyphFolder = "\uE838"; // Folder internal const string GlyphInfo = "\uE946"; // Info internal const string GlyphStats = "\uE9D9"; // Chart internal const string GlyphChat = "\uE8BD"; // Chat internal const string GlyphGuide = "\uE736"; // ReadingList (사용 가이드) internal const string GlyphAutoRun = "\uE82F"; // Lightbulb (전구) internal const string GlyphExit = "\uE711"; // Cancel // 1×1 투명 더미 이미지 — OnRenderItemImage 콜백을 강제로 트리거하기 위함 private static readonly System.Drawing.Bitmap DummyImage = new(1, 1); /// /// 현재 화면의 DPI 배율을 반환합니다 (100% = 1.0, 150% = 1.5). /// WinForms는 논리 픽셀 값을 DPI 배율만큼 자동 확대하므로, /// 이 값으로 역산하여 모든 PC에서 동일한 물리적 크기를 유지합니다. /// private static float GetDpiScale() { try { using var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero); return Math.Max(1f, g.DpiX / 96f); } catch { return 1f; } } /// 논리 픽셀 값을 DPI 역산하여 물리적 크기가 동일하게 유지되도록 변환합니다. private static int Dp(int logicalPx, float dpiScale) => Math.Max(1, (int)Math.Round(logicalPx / dpiScale)); /// 외부에서 DPI 보정된 Padding을 생성할 때 사용합니다. public static System.Windows.Forms.Padding DpiPadding(int left, int top, int right, int bottom) { var s = GetDpiScale(); return new System.Windows.Forms.Padding(Dp(left, s), Dp(top, s), Dp(right, s), Dp(bottom, s)); } public static ToolStripMenuItem MakeItem(string text, string glyph, EventHandler onClick) { var s = GetDpiScale(); var item = new ToolStripMenuItem(text) { Tag = glyph, Image = DummyImage, // Image가 있어야 OnRenderItemImage가 호출됨 Padding = new Padding(Dp(4, s), Dp(10, s), Dp(16, s), Dp(10, s)), }; item.Click += onClick; return item; } /// 패딩이 적용된 ContextMenuStrip을 생성합니다. public static ContextMenuStrip CreateMenu() { var s = GetDpiScale(); var menu = new ContextMenuStrip(); menu.Renderer = new ModernTrayRenderer(); // Point 단위 폰트는 DPI 자동 확대되므로 역산 적용 // Point 단위는 DPI와 무관하게 동일한 물리 크기 → 역산하지 않음 menu.Font = new Font("Segoe UI", 10f, GraphicsUnit.Point); menu.ShowImageMargin = true; // 너비를 넉넉히 잡아 좌측 아이콘~테두리 간 여백 확보 menu.ImageScalingSize = new Size(Dp(52, s), Dp(32, s)); menu.MinimumSize = new Size(Dp(280, s), 0); // 팝업이 열릴 때 모서리를 둥글게 클리핑 menu.Opened += (_, _) => ApplyRoundedCorners(menu); return menu; } private static void ApplyRoundedCorners(ContextMenuStrip menu) { try { var rgn = CreateRoundRectRgn(0, 0, menu.Width, menu.Height, 16, 16); if (rgn == IntPtr.Zero) return; // SetWindowRgn 성공 시 시스템이 핸들 소유권을 가져감 → DeleteObject 불필요 // 실패 시 직접 해제해야 GDI 누수 방지 if (SetWindowRgn(menu.Handle, rgn, true) == 0) DeleteObject(rgn); } catch { /* 클리핑 실패 시 사각형으로 폴백 */ } } [DllImport("gdi32.dll")] private static extern IntPtr CreateRoundRectRgn(int x1, int y1, int x2, int y2, int cx, int cy); [DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr hObject); [DllImport("user32.dll")] private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw); /// /// 모든 아이템 추가 후 호출하여 상하 테두리 여백을 적용합니다. /// WinForms ContextMenuStrip은 Padding/DefaultPadding/DisplayRectangle 모두 /// 내부 레이아웃이 무시하므로, 첫/마지막 아이템의 Margin으로 처리합니다. /// public static void ApplySpacing(ContextMenuStrip menu, int top = 10, int bottom = 10, int left = 0, int right = 0) { if (menu.Items.Count == 0) return; var s = GetDpiScale(); int t = Dp(top, s), b = Dp(bottom, s), l = Dp(left, s), r = Dp(right, s); foreach (ToolStripItem item in menu.Items) { var m = item.Margin; item.Margin = new Padding(m.Left + l, m.Top, m.Right + r, m.Bottom); } var first = menu.Items[0]; first.Margin = new Padding(first.Margin.Left, t, first.Margin.Right, first.Margin.Bottom); var last = menu.Items[menu.Items.Count - 1]; last.Margin = new Padding(last.Margin.Left, last.Margin.Top, last.Margin.Right, b); } } // ───────────────────────────────────────────────────────────────────────────── internal sealed class ModernTrayRenderer : ToolStripProfessionalRenderer { // 전구(자동 실행) 활성 색상 — 앰버 글로우 private static readonly Color BulbOnColor = Color.FromArgb(255, 185, 0); private static readonly Color BulbGlowHalo = Color.FromArgb(50, 255, 185, 0); public ModernTrayRenderer() : base(new ModernColorTable()) { } // ─── 테마 색상 리더 ───────────────────────────────────────────────── private static Color ThemeColor(string key, Color fallback) { try { if (System.Windows.Application.Current?.Resources[key] is System.Windows.Media.SolidColorBrush b) return Color.FromArgb(b.Color.A, b.Color.R, b.Color.G, b.Color.B); } catch { } return fallback; } private static Color BgColor => ThemeColor("LauncherBackground", Color.FromArgb(250, 250, 252)); private static Color HoverColor => ThemeColor("ItemHoverBackground", Color.FromArgb(234, 234, 247)); private static Color AccentColor => ThemeColor("AccentColor", Color.FromArgb(75, 94, 252)); private static Color TextColor => ThemeColor("PrimaryText", Color.FromArgb(22, 23, 42)); private static Color TextDimColor => ThemeColor("SecondaryText", Color.FromArgb(90, 92, 128)); private static Color SepColor => ThemeColor("SeparatorColor", Color.FromArgb(228, 228, 242)); private static Color BorderColor => ThemeColor("BorderColor", Color.FromArgb(210, 210, 232)); private static Color IconColor => ThemeColor("SecondaryText", Color.FromArgb(110, 112, 148)); // ─── 배경 ──────────────────────────────────────────────────────────── protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) { using var br = new SolidBrush(BgColor); e.Graphics.FillRectangle(br, e.AffectedBounds); } protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; using var pen = new Pen(BorderColor, 1f); // ApplyRoundedCorners 의 clip radius(14) 보다 0.5px 안쪽에 그려야 // 창 경계 밖으로 삐져나오지 않음 var rect = new RectangleF(0.5f, 0.5f, e.ToolStrip.Width - 1f, e.ToolStrip.Height - 1f); using var path = BuildRoundedPath(rect, 15f); g.DrawPath(pen, path); g.SmoothingMode = SmoothingMode.Default; } protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) { // 거터 없음 — 전체 배경과 동일한 색 using var br = new SolidBrush(BgColor); e.Graphics.FillRectangle(br, e.AffectedBounds); } // ─── 아이템 배경 ───────────────────────────────────────────────────── protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) { var g = e.Graphics; var item = e.Item; // 기본 배경 (항상 메뉴 배경색으로 초기화) using var bg = new SolidBrush(BgColor); g.FillRectangle(bg, 0, 0, item.Width, item.Height); if (!item.Selected || !item.Enabled) return; // 호버: 안쪽에 둥근 모서리 하이라이트 g.SmoothingMode = SmoothingMode.AntiAlias; using var hoverBr = new SolidBrush(HoverColor); DrawRoundedFill(g, hoverBr, new RectangleF(6, 2, item.Width - 12, item.Height - 4), 8f); g.SmoothingMode = SmoothingMode.Default; } // ─── 구분선 ────────────────────────────────────────────────────────── protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) { int y = e.Item.Height / 2; using var pen = new Pen(SepColor); e.Graphics.DrawLine(pen, 16, y, e.Item.Width - 16, y); } // ─── 텍스트 ────────────────────────────────────────────────────────── protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) { e.TextColor = e.Item.Enabled ? TextColor : TextDimColor; e.TextFormat = TextFormatFlags.Left | TextFormatFlags.VerticalCenter; base.OnRenderItemText(e); } // ─── 아이콘 (일반 아이템 + CheckOnClick 모두 처리) ─────────────────── protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) { // 모든 아이템의 글리프를 여기서 그린다 DrawGlyph(e.Graphics, e.Item, e.ImageRectangle); } protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) { // CheckOnClick 항목: 기본 체크마크 대신 글리프 아이콘을 그린다 DrawGlyph(e.Graphics, e.Item, e.ImageRectangle); } // ─── 글리프 렌더러 ─────────────────────────────────────────────────── private static void DrawGlyph(Graphics g, ToolStripItem item, Rectangle bounds) { if (item.Tag is not string glyph || string.IsNullOrEmpty(glyph)) return; if (bounds.Width <= 0 || bounds.Height <= 0) return; g.TextRenderingHint = TextRenderingHint.AntiAlias; bool isLightbulb = glyph == TrayContextMenu.GlyphAutoRun; bool isChecked = item is ToolStripMenuItem { Checked: true }; Color glyphColor; if (isLightbulb && isChecked) { // 전구 켜짐 — 앰버 글로우 효과 DrawLightbulbGlow(g, glyph, bounds); glyphColor = BulbOnColor; } else if (isLightbulb && !isChecked) { // 전구 꺼짐 — 확실히 구분되는 진한 회색 glyphColor = Color.FromArgb(120, 120, 140); } else if (item.Selected && item.Enabled) { glyphColor = AccentColor; } else { glyphColor = IconColor; } // Point 단위는 DPI와 무관하게 동일한 물리 크기 → 역산하지 않음 using var font = new Font("Segoe MDL2 Assets", 12f, GraphicsUnit.Point); using var br = new SolidBrush(glyphColor); var fmt = StringFormat.GenericTypographic; var size = g.MeasureString(glyph, font, 0, fmt); // 넓은 아이콘 영역 내 중앙 정렬 float x = bounds.X + (bounds.Width - size.Width) / 2f + 2f; float y = bounds.Y + (bounds.Height - size.Height) / 2f; g.DrawString(glyph, font, br, x, y, fmt); } private static void DrawLightbulbGlow(Graphics g, string glyph, Rectangle bounds) { // 중심에서 균일하게 퍼지는 원형 앰버 글로우 g.SmoothingMode = SmoothingMode.AntiAlias; float cx = bounds.X + bounds.Width / 2f; float cy = bounds.Y + bounds.Height / 2f; float radius = Math.Max(bounds.Width, bounds.Height) * 0.85f; var glowRect = new RectangleF(cx - radius, cy - radius, radius * 2, radius * 2); using var path = new GraphicsPath(); path.AddEllipse(glowRect); using var brush = new PathGradientBrush(path) { CenterColor = Color.FromArgb(60, 255, 185, 0), SurroundColors = new[] { Color.FromArgb(0, 255, 185, 0) }, CenterPoint = new PointF(cx, cy) }; g.FillEllipse(brush, glowRect); g.SmoothingMode = SmoothingMode.Default; } // ─── 유틸 ──────────────────────────────────────────────────────────── private static GraphicsPath BuildRoundedPath(RectangleF rect, float radius) { float d = radius * 2f; var path = new GraphicsPath(); path.AddArc(rect.X, rect.Y, d, d, 180, 90); path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90); path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90); path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90); path.CloseFigure(); return path; } private static void DrawRoundedFill(Graphics g, Brush brush, RectangleF rect, float radius) { using var path = BuildRoundedPath(rect, radius); g.FillPath(brush, path); } } // ───────────────────────────────────────────────────────────────────────────── internal sealed class ModernColorTable : ProfessionalColorTable { private static Color TC(string key, Color fb) { try { if (System.Windows.Application.Current?.Resources[key] is System.Windows.Media.SolidColorBrush b) return Color.FromArgb(b.Color.A, b.Color.R, b.Color.G, b.Color.B); } catch { } return fb; } private static Color Bg => TC("LauncherBackground", Color.FromArgb(250, 250, 252)); private static Color Hover => TC("ItemHoverBackground", Color.FromArgb(234, 234, 247)); private static Color Border => TC("BorderColor", Color.FromArgb(210, 210, 232)); private static Color Sep => TC("SeparatorColor", Color.FromArgb(228, 228, 242)); public override Color MenuBorder => Border; public override Color ToolStripDropDownBackground => Bg; public override Color ImageMarginGradientBegin => Bg; public override Color ImageMarginGradientMiddle => Bg; public override Color ImageMarginGradientEnd => Bg; public override Color MenuItemSelected => Hover; public override Color MenuItemSelectedGradientBegin => Hover; public override Color MenuItemSelectedGradientEnd => Hover; public override Color MenuItemBorder => Color.Transparent; public override Color MenuItemPressedGradientBegin => Hover; public override Color MenuItemPressedGradientEnd => Hover; public override Color SeparatorLight => Sep; public override Color SeparatorDark => Sep; public override Color CheckBackground => Color.Transparent; public override Color CheckSelectedBackground => Color.Transparent; public override Color CheckPressedBackground => Color.Transparent; }