using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using AxCopilot.Services; namespace AxCopilot.Views; /// /// 화면 하단에 고정되는 미니 독 바. /// 설정(DockBarItems)에 따라 표시 항목이 결정됩니다. /// 가능한 항목: launcher, clipboard, capture, agent, clock, cpu, ram, quickinput /// public partial class DockBarWindow : Window { private DispatcherTimer? _timer; private PerformanceCounter? _cpuCounter; private TextBlock? _cpuText; private TextBlock? _ramText; private TextBlock? _clockText; private TextBox? _quickInput; /// 런처에 검색어를 전달하는 콜백. public Action? OnQuickSearch { get; set; } /// 캡처 직접 실행 콜백. public Action? OnCapture { get; set; } /// AX Agent 대화창 열기 콜백. public Action? OnOpenAgent { get; set; } // 표시 가능한 모든 아이템 정의 private static readonly (string Key, string Icon, string Tooltip)[] AllItems = { ("launcher", "\uE721", "AX Commander"), ("clipboard", "\uE77F", "클립보드 히스토리"), ("capture", "\uE722", "화면 캡처"), ("agent", "\uE8BD", "AX Agent"), ("clock", "\uE823", "시계"), ("cpu", "\uE950", "CPU"), ("ram", "\uE7F4", "RAM"), ("quickinput", "\uE8D3", "빠른 입력"), }; private DispatcherTimer? _glowTimer; /// 설정 저장 콜백 (위치 저장용). public Action? OnPositionChanged { get; set; } public DockBarWindow() { InitializeComponent(); MouseLeftButtonDown += (_, e) => { if (e.LeftButton == MouseButtonState.Pressed) try { DragMove(); } catch (Exception) { } }; LocationChanged += (_, _) => OnPositionChanged?.Invoke(Left, Top); Loaded += (_, _) => PositionDock(); Closed += (_, _) => { _timer?.Stop(); _glowTimer?.Stop(); _cpuCounter?.Dispose(); }; } /// 투명도, 위치, 글로우를 설정에서 적용합니다. public void ApplySettings(double opacity, double left, double top, bool rainbowGlow) { Opacity = Math.Clamp(opacity, 0.3, 1.0); if (left >= 0 && top >= 0) { Left = left; Top = top; } else { // -1이면 중앙으로 이동 Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, PositionDock); } if (rainbowGlow) StartRainbowGlow(); else StopRainbowGlow(); } private void StartRainbowGlow() { RainbowGlowBorder.Visibility = Visibility.Visible; _glowTimer ??= new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) }; var startAngle = 0.0; _glowTimer.Tick += (_, _) => { startAngle += 2; if (startAngle >= 360) startAngle -= 360; var rad = startAngle * Math.PI / 180.0; RainbowBrush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(rad), 0.5 + 0.5 * Math.Sin(rad)); RainbowBrush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(rad), 0.5 - 0.5 * Math.Sin(rad)); }; _glowTimer.Start(); } private void StopRainbowGlow() { _glowTimer?.Stop(); RainbowGlowBorder.Visibility = Visibility.Collapsed; } // 기획된 고정 표시 순서 private static readonly string[] FixedOrder = { "launcher", "clipboard", "capture", "agent", "clock", "cpu", "ram", "quickinput" }; /// 설정에서 표시 항목 목록을 받아 독 바를 빌드합니다. 표시 순서는 기획 순서 고정. public void BuildFromSettings(List itemKeys) { DockContent.Children.Clear(); _cpuText = null; _ramText = null; _clockText = null; _quickInput = null; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; bool needTimer = false; bool addedFirst = false; // 기획 순서에서 활성화된 항목만 순서대로 표시 var enabledSet = new HashSet(itemKeys, StringComparer.OrdinalIgnoreCase); foreach (var key in FixedOrder.Where(k => enabledSet.Contains(k))) { if (addedFirst) AddSeparator(); addedFirst = true; switch (key) { case "launcher": AddButton("\uE721", "AX Commander", primaryText, () => OnQuickSearch?.Invoke("")); break; case "clipboard": AddButton("\uE77F", "클립보드", primaryText, () => OnQuickSearch?.Invoke("#")); break; case "capture": AddButton("\uE722", "캡처", primaryText, () => { if (OnCapture != null) OnCapture(); else OnQuickSearch?.Invoke("cap"); }); break; case "agent": AddButton("\uE8BD", "AI", primaryText, () => { if (OnOpenAgent != null) OnOpenAgent(); else OnQuickSearch?.Invoke("!"); }); break; case "clock": _clockText = new TextBlock { Text = DateTime.Now.ToString("HH:mm"), FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(6, 0, 6, 0), }; DockContent.Children.Add(_clockText); needTimer = true; break; case "cpu": var cpuPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; cpuPanel.Children.Add(new TextBlock { Text = "\uE950", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0) }); _cpuText = new TextBlock { Text = "0%", FontSize = 11, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, Width = 28, TextAlignment = TextAlignment.Right }; cpuPanel.Children.Add(_cpuText); DockContent.Children.Add(cpuPanel); needTimer = true; if (_cpuCounter == null) try { _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); _cpuCounter.NextValue(); } catch (Exception) { } break; case "ram": var ramPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; ramPanel.Children.Add(new TextBlock { Text = "\uE7F4", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0) }); _ramText = new TextBlock { Text = "0%", FontSize = 11, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, Width = 28, TextAlignment = TextAlignment.Right }; ramPanel.Children.Add(_ramText); DockContent.Children.Add(ramPanel); needTimer = true; break; case "quickinput": var inputBorder = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(8, 3, 8, 3), VerticalAlignment = VerticalAlignment.Center, }; var inputPanel = new StackPanel { Orientation = Orientation.Horizontal }; inputPanel.Children.Add(new TextBlock { Text = "\uE721", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0) }); _quickInput = new TextBox { Width = 100, FontSize = 11, Foreground = primaryText, CaretBrush = accentBrush, Background = Brushes.Transparent, BorderThickness = new Thickness(0), VerticalAlignment = VerticalAlignment.Center, }; _quickInput.KeyDown += QuickInput_KeyDown; inputPanel.Children.Add(_quickInput); inputBorder.Child = inputPanel; DockContent.Children.Add(inputBorder); break; } } if (needTimer) { _timer ??= new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _timer.Tick -= OnTick; _timer.Tick += OnTick; _timer.Start(); OnTick(null, EventArgs.Empty); } } /// 사용 가능한 모든 아이템 키와 라벨 목록을 반환합니다. public static IReadOnlyList<(string Key, string Icon, string Tooltip)> AvailableItems => AllItems; private void AddButton(string icon, string tooltip, Brush foreground, Action click) { var border = new Border { Width = 30, Height = 30, CornerRadius = new CornerRadius(8), Background = new SolidColorBrush(Color.FromArgb(0x01, 0xFF, 0xFF, 0xFF)), // 거의 투명하지만 히트 테스트 가능 Cursor = Cursors.Hand, ToolTip = tooltip, Margin = new Thickness(2, 0, 2, 0), }; border.Child = new TextBlock { Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = foreground, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, IsHitTestVisible = false, // 텍스트가 이벤트를 가로채지 않도록 }; border.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x22, 0xFF, 0xFF, 0xFF)); }; border.MouseLeave += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x01, 0xFF, 0xFF, 0xFF)); }; border.MouseLeftButtonDown += (_, e) => { e.Handled = true; click(); }; DockContent.Children.Add(border); } private void AddSeparator() { DockContent.Children.Add(new Border { Width = 1, Height = 20, Margin = new Thickness(6, 0, 6, 0), Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray, }); } private void PositionDock() { var screen = SystemParameters.WorkArea; Left = (screen.Width - ActualWidth) / 2 + screen.Left; Top = screen.Bottom - ActualHeight - 8; } private void OnTick(object? sender, EventArgs e) { if (_clockText != null) _clockText.Text = DateTime.Now.ToString("HH:mm"); if (_cpuText != null) { try { _cpuText.Text = $"{_cpuCounter?.NextValue() ?? 0:F0}%"; } catch (Exception) { _cpuText.Text = "-"; } } if (_ramText != null) { try { var m = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf() }; _ramText.Text = GlobalMemoryStatusEx(ref m) ? $"{m.dwMemoryLoad}%" : "-"; } catch (Exception) { _ramText.Text = "-"; } } } [DllImport("kernel32.dll")] private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); [StructLayout(LayoutKind.Sequential)] private struct MEMORYSTATUSEX { public uint dwLength; public uint dwMemoryLoad; public ulong ullTotalPhys, ullAvailPhys, ullTotalPageFile, ullAvailPageFile, ullTotalVirtual, ullAvailVirtual, ullAvailExtendedVirtual; } private void QuickInput_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter && _quickInput != null && !string.IsNullOrWhiteSpace(_quickInput.Text)) { OnQuickSearch?.Invoke(_quickInput.Text); _quickInput.Text = ""; e.Handled = true; } else if (e.Key == Key.Escape && _quickInput != null) { _quickInput.Text = ""; e.Handled = true; } } private void Window_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Escape) Hide(); } }