Files
AX-Copilot/src/AxCopilot/Views/DockBarWindow.xaml.cs
lacvet 0c997f0149 [Phase 39] FontFamily 캐싱 + LauncherWindow 파셜 클래스 분할
- ThemeResourceHelper에 CascadiaCode/ConsolasCode/ConsolasCourierNew 정적 필드 추가
- 25개 파일, 89개 new FontFamily(...) 호출을 정적 캐시 참조로 교체
- LauncherWindow.xaml.cs (1,563줄) → 5개 파셜 파일로 분할 (63% 감소)
  - LauncherWindow.Theme.cs (116줄): ApplyTheme, 커스텀 딕셔너리 빌드
  - LauncherWindow.Animations.cs (153줄): 무지개 글로우, 애니메이션 헬퍼
  - LauncherWindow.Keyboard.cs (593줄): 단축키 20종, ShowToast, IME 검색
  - LauncherWindow.Shell.cs (177줄): Shell32, SendToRecycleBin, 클릭 핸들러
- 빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:54:35 +09:00

328 lines
13 KiB
C#

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;
/// <summary>
/// 화면 하단에 고정되는 미니 독 바.
/// 설정(DockBarItems)에 따라 표시 항목이 결정됩니다.
/// 가능한 항목: launcher, clipboard, capture, agent, clock, cpu, ram, quickinput
/// </summary>
public partial class DockBarWindow : Window
{
private DispatcherTimer? _timer;
private PerformanceCounter? _cpuCounter;
private TextBlock? _cpuText;
private TextBlock? _ramText;
private TextBlock? _clockText;
private TextBox? _quickInput;
/// <summary>런처에 검색어를 전달하는 콜백.</summary>
public Action<string>? OnQuickSearch { get; set; }
/// <summary>캡처 직접 실행 콜백.</summary>
public Action? OnCapture { get; set; }
/// <summary>AX Agent 대화창 열기 콜백.</summary>
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;
/// <summary>설정 저장 콜백 (위치 저장용).</summary>
public Action<double, double>? 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(); };
}
/// <summary>투명도, 위치, 글로우를 설정에서 적용합니다.</summary>
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" };
/// <summary>설정에서 표시 항목 목록을 받아 독 바를 빌드합니다. 표시 순서는 기획 순서 고정.</summary>
public void BuildFromSettings(List<string> 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<string>(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);
}
}
/// <summary>사용 가능한 모든 아이템 키와 라벨 목록을 반환합니다.</summary>
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<MEMORYSTATUSEX>() };
_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();
}
}