- 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>
328 lines
13 KiB
C#
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();
|
|
}
|
|
}
|