using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace AxCopilot.Views; /// /// 텍스트가 선택된 상태에서 핫키를 누르면 커서 위치에 표시되는 액션 선택 팝업. /// ↑↓ 화살표로 이동, Enter로 실행, Esc로 닫기. /// public partial class TextActionPopup : Window { [DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT lpPoint); [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X; public int Y; } public enum ActionResult { None, OpenLauncher, Translate, Summarize, GrammarFix, Explain, Rewrite } public ActionResult SelectedAction { get; private set; } = ActionResult.None; public string SelectedText { get; private set; } = ""; private int _selectedIndex; private readonly List<(ActionResult Action, string Icon, string Label)> _items; // 전체 명령 정의 (key → ActionResult, icon, label) private static readonly (string Key, ActionResult Action, string Icon, string Label)[] AllCommands = { ("translate", ActionResult.Translate, "\uE774", "번역"), ("summarize", ActionResult.Summarize, "\uE8D2", "요약"), ("grammar", ActionResult.GrammarFix, "\uE8AC", "문법 교정"), ("explain", ActionResult.Explain, "\uE946", "설명"), ("rewrite", ActionResult.Rewrite, "\uE70F", "다시 쓰기"), }; /// 사용 가능한 전체 명령 키와 라벨 목록. public static IReadOnlyList<(string Key, string Label)> AvailableCommands => AllCommands.Select(c => (c.Key, c.Label)).ToList(); public TextActionPopup(string selectedText, List? enabledCommands = null) { InitializeComponent(); SelectedText = selectedText; var preview = selectedText.Replace("\r\n", " ").Replace("\n", " "); PreviewText.Text = preview.Length > 60 ? preview[..57] + "…" : preview; // 설정에서 활성화된 명령만 표시 var enabled = enabledCommands ?? AllCommands.Select(c => c.Key).ToList(); _items = new() { (ActionResult.OpenLauncher, "\uE8A7", "AX Commander 열기") }; foreach (var cmd in AllCommands) { if (enabled.Contains(cmd.Key, StringComparer.OrdinalIgnoreCase)) _items.Add((cmd.Action, cmd.Icon, cmd.Label)); } BuildMenuItems(); PositionAtCursor(); Loaded += (_, _) => { Focus(); UpdateSelection(); }; } private void BuildMenuItems() { var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; for (int i = 0; i < _items.Count; i++) { var (action, icon, label) = _items[i]; var idx = i; var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), Width = 20, TextAlignment = TextAlignment.Center, }); sp.Children.Add(new TextBlock { Text = label, FontSize = 13, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); var border = new Border { Child = sp, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 14, 8), Margin = new Thickness(0, 1, 0, 1), Background = Brushes.Transparent, Cursor = Cursors.Hand, Tag = idx, }; border.MouseEnter += (s, _) => { if (s is Border b && b.Tag is int n) { _selectedIndex = n; UpdateSelection(); } }; border.MouseLeftButtonUp += (_, _) => { SelectedAction = action; Close(); }; MenuItems.Children.Add(border); } } private void UpdateSelection() { var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); var selectedBrush = TryFindResource("ItemSelectedBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x44, 0x4B, 0x5E, 0xFC)); for (int i = 0; i < MenuItems.Children.Count; i++) { if (MenuItems.Children[i] is Border b) b.Background = i == _selectedIndex ? selectedBrush : Brushes.Transparent; } } private void PositionAtCursor() { GetCursorPos(out var pt); // DPI 보정 var source = PresentationSource.FromVisual(this); double dpiX = 1.0, dpiY = 1.0; if (source?.CompositionTarget != null) { dpiX = source.CompositionTarget.TransformFromDevice.M11; dpiY = source.CompositionTarget.TransformFromDevice.M22; } Left = pt.X * dpiX; Top = pt.Y * dpiY + 10; // 커서 아래쪽에 표시 // 화면 밖으로 나가지 않도록 보정 var screen = SystemParameters.WorkArea; if (Left + 280 > screen.Right) Left = screen.Right - 280; if (Top + 300 > screen.Bottom) Top = pt.Y * dpiY - 300; // 위쪽으로 if (Left < screen.Left) Left = screen.Left; if (Top < screen.Top) Top = screen.Top; } private void Window_KeyDown(object sender, KeyEventArgs e) { switch (e.Key) { case Key.Up: _selectedIndex = (_selectedIndex - 1 + _items.Count) % _items.Count; UpdateSelection(); e.Handled = true; break; case Key.Down: _selectedIndex = (_selectedIndex + 1) % _items.Count; UpdateSelection(); e.Handled = true; break; case Key.Enter: SelectedAction = _items[_selectedIndex].Action; Close(); e.Handled = true; break; case Key.Escape: SelectedAction = ActionResult.None; Close(); e.Handled = true; break; } } private void Window_Deactivated(object? sender, EventArgs e) { // 포커스를 잃으면 자동 닫기 if (IsVisible) Close(); } }