205 lines
6.9 KiB
C#
205 lines
6.9 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
/// <summary>
|
|
/// 텍스트가 선택된 상태에서 핫키를 누르면 커서 위치에 표시되는 액션 선택 팝업.
|
|
/// ↑↓ 화살표로 이동, Enter로 실행, Esc로 닫기.
|
|
/// </summary>
|
|
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", "다시 쓰기"),
|
|
};
|
|
|
|
/// <summary>사용 가능한 전체 명령 키와 라벨 목록.</summary>
|
|
public static IReadOnlyList<(string Key, string Label)> AvailableCommands
|
|
=> AllCommands.Select(c => (c.Key, c.Label)).ToList();
|
|
|
|
public TextActionPopup(string selectedText, List<string>? 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();
|
|
}
|
|
}
|