Compare commits
2 Commits
deffb33cf9
...
0c997f0149
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c997f0149 | |||
| 08524466d2 |
40
CLAUDE.md
40
CLAUDE.md
@@ -4,6 +4,46 @@
|
||||
|
||||
---
|
||||
|
||||
## 0. 작업 완료 후 깃 푸시 규칙
|
||||
|
||||
### 매 작업 단위 완료 시 반드시 깃 푸시
|
||||
- Phase 작업(기능 개발, 리팩터링 등) 완료 → `dotnet build` 확인 → 소스 파일만 스테이징 → 커밋 → 푸시
|
||||
- **빌드 오류 없이 커밋** — `경고 0, 오류 0` 상태에서만 푸시
|
||||
- 커밋 메시지: `[PhaseXX] 작업 내용 요약 (1~2줄)`
|
||||
|
||||
### 오류 복구 불가 시 이전 버전 롤백
|
||||
작업 중 오류가 복구되지 않으면 깃에서 이전 버전을 받아 작업:
|
||||
```bash
|
||||
# 마지막 커밋으로 전체 복구
|
||||
git reset --hard HEAD
|
||||
|
||||
# 특정 커밋으로 복구 (git log로 커밋 해시 확인)
|
||||
git reset --hard <커밋해시>
|
||||
|
||||
# 원격 최신 버전으로 완전 복구
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
```
|
||||
- 복구 시도 2회 이상 실패 → 즉시 롤백, 사용자에게 알림
|
||||
- 롤백 후 원인 분석 → 더 작은 단위로 재작업
|
||||
|
||||
### 스테이징 규칙 (빌드 산출물 제외)
|
||||
```bash
|
||||
# 소스 코드만 스테이징 (bin/, obj/ 제외)
|
||||
git add src/AxCopilot/Views/
|
||||
git add src/AxCopilot/Services/
|
||||
git add src/AxCopilot/Models/
|
||||
git add src/AxCopilot/Handlers/
|
||||
git add src/AxCopilot/ViewModels/
|
||||
git add src/AxCopilot/Themes/
|
||||
git add src/AxCopilot/Core/
|
||||
git add docs/
|
||||
git add CLAUDE.md
|
||||
# 절대 추가 금지: bin/, obj/, *.dll, *.exe, *.pdb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. UI/UX 디자인 원칙
|
||||
|
||||
### 기본 컨트롤 사용 금지
|
||||
|
||||
@@ -4538,5 +4538,51 @@ Week 8: [23] AutoCompact + isEnabled + 최종 검증
|
||||
|
||||
---
|
||||
|
||||
최종 업데이트: 2026-04-03 (Phase 22~37 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 5차)
|
||||
## Phase 38 — SettingsWindow 파셜 클래스 분할 (v2.3) ✅ 완료
|
||||
|
||||
> **목표**: 3,216줄 SettingsWindow.xaml.cs를 3개 파셜 클래스 파일로 분할.
|
||||
|
||||
| 파일 | 줄 수 | 내용 |
|
||||
|------|-------|------|
|
||||
| `SettingsWindow.xaml.cs` (메인) | 373 | 생성자, 필드, 저장/닫기, 스니펫 이벤트 |
|
||||
| `SettingsWindow.UI.cs` | 802 | 섹션 헬퍼, 탭 전환, 독바, 스토리지, 핫키, 버전 |
|
||||
| `SettingsWindow.Tools.cs` | 875 | 도구/커넥터 카드 UI, AX Agent 탭, 도구 관리 |
|
||||
| `SettingsWindow.AgentConfig.cs` | 1,202 | 모델 등록, 스킬, 템플릿, AI토글, 네트워크모드, 훅, MCP |
|
||||
|
||||
- **메인 파일**: 3,216줄 → 373줄 (**88.4% 감소**)
|
||||
- **빌드**: 경고 0, 오류 0
|
||||
|
||||
---
|
||||
|
||||
## Phase 39 — FontFamily 캐싱 + LauncherWindow 파셜 분할 (v2.3) ✅ 완료
|
||||
|
||||
> **목표**: 89개 `new FontFamily(...)` 반복 생성 제거 + LauncherWindow 파셜 분할.
|
||||
|
||||
### FontFamily 캐싱 (25개 파일)
|
||||
|
||||
ThemeResourceHelper에 5개 정적 필드 추가:
|
||||
- `SegoeMdl2` — `new FontFamily("Segoe MDL2 Assets")` (기존)
|
||||
- `Consolas` — `new FontFamily("Consolas")` (기존)
|
||||
- `CascadiaCode` — `new FontFamily("Cascadia Code, Consolas, monospace")` (신규)
|
||||
- `ConsolasCode` — `new FontFamily("Consolas, Cascadia Code, Segoe UI")` (신규)
|
||||
- `ConsolasCourierNew` — `new FontFamily("Consolas, Courier New")` (신규)
|
||||
|
||||
총 89개 `new FontFamily(...)` 호출 → 정적 캐시 필드 참조로 교체 (25개 파일)
|
||||
|
||||
### LauncherWindow 파셜 분할
|
||||
|
||||
| 파일 | 줄 수 | 내용 |
|
||||
|------|-------|------|
|
||||
| `LauncherWindow.xaml.cs` (메인) | 578 | Win32 P/Invoke, 생성자, Show(), 아이콘 20종 애니메이션 |
|
||||
| `LauncherWindow.Theme.cs` | 116 | ApplyTheme, 테마 빌드, BuildCustomDictionary, IsSystemDarkMode |
|
||||
| `LauncherWindow.Animations.cs` | 153 | 무지개 글로우, 애니메이션 헬퍼, CenterOnScreen, AnimateIn |
|
||||
| `LauncherWindow.Keyboard.cs` | 593 | IME 검색, PreviewKeyDown, KeyDown 20여 단축키, ShowToast |
|
||||
| `LauncherWindow.Shell.cs` | 177 | Shell32 P/Invoke, SendToRecycleBin, ShowLargeType, 클릭 핸들러 |
|
||||
|
||||
- **메인 파일**: 1,563줄 → 578줄 (**63.0% 감소**)
|
||||
- **빌드**: 경고 0, 오류 0
|
||||
|
||||
---
|
||||
|
||||
최종 업데이트: 2026-04-03 (Phase 22~39 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 7차)
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ public class NotifyTool : IAgentTool
|
||||
titleRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = iconChar,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
Foreground = ThemeResourceHelper.HexBrush(iconColor),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Views;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
@@ -245,7 +246,7 @@ public static class MarkdownRenderer
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = m.Groups[7].Value,
|
||||
FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"),
|
||||
FontFamily = ThemeResourceHelper.CascadiaCode,
|
||||
FontSize = 12.5,
|
||||
Foreground = accentColor
|
||||
}
|
||||
@@ -484,7 +485,7 @@ public static class MarkdownRenderer
|
||||
var codeLines = code.Split('\n');
|
||||
var lineNumbers = new TextBlock
|
||||
{
|
||||
FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"),
|
||||
FontFamily = ThemeResourceHelper.CascadiaCode,
|
||||
FontSize = 12.5,
|
||||
Foreground = new SolidColorBrush(Color.FromArgb(0x50, 0xFF, 0xFF, 0xFF)),
|
||||
Padding = new Thickness(10, 10, 6, 14),
|
||||
@@ -497,7 +498,7 @@ public static class MarkdownRenderer
|
||||
|
||||
var codeText = new TextBlock
|
||||
{
|
||||
FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"),
|
||||
FontFamily = ThemeResourceHelper.CascadiaCode,
|
||||
FontSize = 12.5,
|
||||
Foreground = textColor,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
@@ -532,7 +533,7 @@ public static class MarkdownRenderer
|
||||
new TextBlock
|
||||
{
|
||||
Text = mdlIcon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 10, Foreground = fg, Opacity = 0.6,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
@@ -585,7 +586,7 @@ public static class MarkdownRenderer
|
||||
|
||||
var lineNumTb = new TextBlock
|
||||
{
|
||||
FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"),
|
||||
FontFamily = ThemeResourceHelper.CascadiaCode,
|
||||
FontSize = 13, LineHeight = 22,
|
||||
Foreground = new SolidColorBrush(Color.FromArgb(0x50, 0xFF, 0xFF, 0xFF)),
|
||||
Padding = new Thickness(16, 16, 8, 16),
|
||||
@@ -597,7 +598,7 @@ public static class MarkdownRenderer
|
||||
|
||||
var codeTb = new TextBlock
|
||||
{
|
||||
FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"),
|
||||
FontFamily = ThemeResourceHelper.CascadiaCode,
|
||||
FontSize = 13, LineHeight = 22,
|
||||
Foreground = textColor,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
|
||||
@@ -102,7 +102,7 @@ public partial class AgentStatsDashboardWindow : Window
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 16,
|
||||
Foreground = new SolidColorBrush(col),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
@@ -213,7 +213,7 @@ public partial class AgentStatsDashboardWindow : Window
|
||||
{
|
||||
Text = tool,
|
||||
FontSize = 11,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -81,7 +81,7 @@ public partial class CommandPaletteWindow : Window
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cmd.Icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = cmd.Icon, FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14, Foreground = FindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ internal sealed class CustomMessageBox : Window
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = iconText,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 18,
|
||||
Foreground = iconColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
@@ -91,7 +91,7 @@ internal sealed class CustomMessageBox : Window
|
||||
var closeBtn = new Button
|
||||
{
|
||||
Content = "\uE8BB",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
Background = Brushes.Transparent,
|
||||
@@ -283,7 +283,7 @@ internal sealed class CustomMessageBox : Window
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = iconText,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12,
|
||||
Foreground = iconColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -69,7 +69,7 @@ internal sealed partial class CustomMoodDialog : Window
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE771",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 18, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
@@ -123,7 +123,7 @@ internal sealed partial class CustomMoodDialog : Window
|
||||
AddLabel(stack, "CSS 스타일", primaryText);
|
||||
AddHint(stack, "문서에 적용될 CSS입니다. body, h1~h6, table, .callout 등의 스타일을 정의하세요.", secondaryText);
|
||||
_cssBox = CreateTextBox(existingCss, primaryText, itemBg, accentBrush, borderBrush, multiline: true, height: 200);
|
||||
_cssBox.FontFamily = new FontFamily("Consolas, Courier New, monospace");
|
||||
_cssBox.FontFamily = ThemeResourceHelper.ConsolasCourierNew;
|
||||
_cssBox.FontSize = 12;
|
||||
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _cssBox });
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ internal sealed class CustomPresetDialog : Window
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE710",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 18, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
@@ -166,7 +166,7 @@ internal sealed class CustomPresetDialog : Window
|
||||
_iconPreviewText = new TextBlock
|
||||
{
|
||||
Text = _selectedSymbol,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 20,
|
||||
Foreground = BrushFromHex(_selectedColor),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -355,7 +355,7 @@ internal sealed class CustomPresetDialog : Window
|
||||
});
|
||||
var closeBtn = new TextBlock
|
||||
{
|
||||
Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE711", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12, Foreground = secondaryText,
|
||||
Cursor = Cursors.Hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Top,
|
||||
@@ -397,7 +397,7 @@ internal sealed class CustomPresetDialog : Window
|
||||
iconBtn.Child = new TextBlock
|
||||
{
|
||||
Text = symbol,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 16,
|
||||
Foreground = isSelected ? BrushFromHex(_selectedColor) : primaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
|
||||
@@ -60,7 +60,7 @@ public class DiffViewerPanel : Border
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE89A",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 13, Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
@@ -143,7 +143,7 @@ public class DiffViewerPanel : Border
|
||||
|
||||
var oldLineText = new TextBlock
|
||||
{
|
||||
Text = lineNo, FontSize = 10, FontFamily = new FontFamily("Consolas"),
|
||||
Text = lineNo, FontSize = 10, FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xCC)),
|
||||
HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 0, 4, 0),
|
||||
};
|
||||
@@ -152,7 +152,7 @@ public class DiffViewerPanel : Border
|
||||
|
||||
var newLineText = new TextBlock
|
||||
{
|
||||
Text = newLineNo, FontSize = 10, FontFamily = new FontFamily("Consolas"),
|
||||
Text = newLineNo, FontSize = 10, FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xCC)),
|
||||
HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 0, 4, 0),
|
||||
};
|
||||
@@ -161,7 +161,7 @@ public class DiffViewerPanel : Border
|
||||
|
||||
var prefixText = new TextBlock
|
||||
{
|
||||
Text = prefix, FontSize = 11, FontFamily = new FontFamily("Consolas"),
|
||||
Text = prefix, FontSize = 11, FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = new SolidColorBrush(fg), FontWeight = FontWeights.Bold,
|
||||
};
|
||||
Grid.SetColumn(prefixText, 2);
|
||||
@@ -169,7 +169,7 @@ public class DiffViewerPanel : Border
|
||||
|
||||
var contentText = new TextBlock
|
||||
{
|
||||
Text = line.Content, FontSize = 11, FontFamily = new FontFamily("Consolas"),
|
||||
Text = line.Content, FontSize = 11, FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = new SolidColorBrush(fg),
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
};
|
||||
|
||||
@@ -165,7 +165,7 @@ public partial class DockBarWindow : Window
|
||||
var cpuPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
cpuPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE950", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE950", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 11, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0)
|
||||
});
|
||||
@@ -180,7 +180,7 @@ public partial class DockBarWindow : Window
|
||||
var ramPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
ramPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE7F4", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE7F4", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 11, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0)
|
||||
});
|
||||
@@ -199,7 +199,7 @@ public partial class DockBarWindow : Window
|
||||
var inputPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
inputPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE721", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE721", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 11, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0)
|
||||
});
|
||||
@@ -246,7 +246,7 @@ public partial class DockBarWindow : Window
|
||||
border.Child = new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14, Foreground = foreground,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -419,7 +419,7 @@ public partial class HelpDetailWindow : Window
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
Foreground = ThemeAccent,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -54,7 +54,7 @@ internal sealed class InputDialog : Window
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8AC", // 편집 아이콘
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 16, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
|
||||
153
src/AxCopilot/Views/LauncherWindow.Animations.cs
Normal file
153
src/AxCopilot/Views/LauncherWindow.Animations.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class LauncherWindow
|
||||
{
|
||||
// ─── 무지개 글로우 상시 애니메이션 ────────────────────────────────────────
|
||||
|
||||
/// <summary>선택 아이템 상시 무지개 글로우 효과를 적용하거나 제거합니다.</summary>
|
||||
private void UpdateSelectionGlow()
|
||||
{
|
||||
if (_vm.EnableSelectionGlow)
|
||||
{
|
||||
var gs = new System.Windows.Media.GradientStopCollection
|
||||
{
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x6B, 0x6B), 0.00),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFE, 0xCA, 0x57), 0.17),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x48, 0xDB, 0xFB), 0.33),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x9F, 0xF3), 0.50),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x54, 0xA0, 0xFF), 0.67),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x5F, 0x27, 0xCD), 0.83),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x6B, 0x6B), 1.00),
|
||||
};
|
||||
Resources["SelectionGlowBrush"] = new System.Windows.Media.LinearGradientBrush(
|
||||
gs,
|
||||
new System.Windows.Point(0, 0),
|
||||
new System.Windows.Point(1, 1));
|
||||
Resources["SelectionGlowVisibility"] = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
Resources["SelectionGlowBrush"] = System.Windows.Media.Brushes.Transparent;
|
||||
Resources["SelectionGlowVisibility"] = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>무지개 글로우를 정지하고 숨깁니다.</summary>
|
||||
private void StopRainbowGlow()
|
||||
{
|
||||
_rainbowTimer?.Stop();
|
||||
_rainbowTimer = null;
|
||||
if (RainbowGlowBorder != null) RainbowGlowBorder.Opacity = 0;
|
||||
}
|
||||
|
||||
/// <summary>런처 테두리 무지개 그라데이션 회전을 시작합니다.</summary>
|
||||
private void StartRainbowGlow()
|
||||
{
|
||||
_rainbowTimer?.Stop();
|
||||
if (LauncherRainbowBrush == null || RainbowGlowBorder == null) return;
|
||||
|
||||
_rainbowTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(20)
|
||||
};
|
||||
var startTime = DateTime.UtcNow;
|
||||
_rainbowTimer.Tick += (_, _) =>
|
||||
{
|
||||
if (!IsVisible) { _rainbowTimer?.Stop(); return; }
|
||||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
var shift = (elapsed / 2000.0) % 1.0; // 2초에 1바퀴 (느리게)
|
||||
var angle = shift * Math.PI * 2;
|
||||
LauncherRainbowBrush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(angle), 0.5 + 0.5 * Math.Sin(angle));
|
||||
LauncherRainbowBrush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(angle), 0.5 - 0.5 * Math.Sin(angle));
|
||||
};
|
||||
_rainbowTimer.Start();
|
||||
}
|
||||
|
||||
// ─── 애니메이션 헬퍼 ──────────────────────────────────────────────────────
|
||||
|
||||
private static KeyTime KT(double sec) => KeyTime.FromTimeSpan(TimeSpan.FromSeconds(sec));
|
||||
|
||||
private static void AddOpacityPulse(Storyboard sb, UIElement target, int index, double totalSec)
|
||||
{
|
||||
var a = new DoubleAnimationUsingKeyFrames();
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(index)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(0.25, KT(index + 0.5)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(index + 1)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(totalSec)));
|
||||
Storyboard.SetTarget(a, target);
|
||||
Storyboard.SetTargetProperty(a, new PropertyPath(UIElement.OpacityProperty));
|
||||
sb.Children.Add(a);
|
||||
}
|
||||
|
||||
private static void AddGroupFlash(Storyboard sb, UIElement[] group, double startSec, double totalSec)
|
||||
{
|
||||
foreach (var p in group)
|
||||
{
|
||||
var a = new DoubleAnimationUsingKeyFrames();
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(0)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(startSec)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(0.2, KT(startSec + 0.6)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(startSec + 1.2)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(totalSec)));
|
||||
Storyboard.SetTarget(a, p);
|
||||
Storyboard.SetTargetProperty(a, new PropertyPath(UIElement.OpacityProperty));
|
||||
sb.Children.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
private static DoubleAnimationUsingKeyFrames MakeKeyFrameAnim((double val, double sec)[] frames)
|
||||
{
|
||||
var a = new DoubleAnimationUsingKeyFrames();
|
||||
foreach (var (val, sec) in frames)
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(val, KT(sec)));
|
||||
return a;
|
||||
}
|
||||
|
||||
// ─── 화면 배치 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void CenterOnScreen()
|
||||
{
|
||||
var screen = SystemParameters.WorkArea;
|
||||
// ActualHeight/ActualWidth는 첫 Show() 전 레이아웃 패스 이전에 0일 수 있음 → 기본값으로 보호
|
||||
var w = ActualWidth > 0 ? ActualWidth : 640;
|
||||
var h = ActualHeight > 0 ? ActualHeight : 80;
|
||||
Left = (screen.Width - w) / 2 + screen.Left;
|
||||
Top = _vm.WindowPosition switch
|
||||
{
|
||||
"center" => (screen.Height - h) / 2 + screen.Top,
|
||||
"bottom" => screen.Height * 0.75 + screen.Top,
|
||||
_ => screen.Height * 0.2 + screen.Top, // "center-top" (기본)
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 등장 애니메이션 ──────────────────────────────────────────────────────
|
||||
|
||||
private void AnimateIn()
|
||||
{
|
||||
Opacity = 0;
|
||||
|
||||
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
|
||||
|
||||
var fadeAnim = new DoubleAnimation(0, 1,
|
||||
TimeSpan.FromMilliseconds(100)) { EasingFunction = ease };
|
||||
|
||||
var slideAnim = new DoubleAnimation(-8, 0,
|
||||
TimeSpan.FromMilliseconds(120)) { EasingFunction = ease };
|
||||
|
||||
BeginAnimation(OpacityProperty, fadeAnim);
|
||||
|
||||
// Window에 AllowsTransparency=True 일 때 RenderTransform을 Window에 직접 설정하면
|
||||
// InvalidOperationException 발생 → Content(루트 Border)에 적용
|
||||
if (Content is System.Windows.FrameworkElement root)
|
||||
{
|
||||
var translate = new TranslateTransform(0, -10);
|
||||
root.RenderTransform = translate;
|
||||
root.RenderTransformOrigin = new System.Windows.Point(0.5, 0);
|
||||
translate.BeginAnimation(TranslateTransform.YProperty, slideAnim);
|
||||
}
|
||||
}
|
||||
}
|
||||
593
src/AxCopilot/Views/LauncherWindow.Keyboard.cs
Normal file
593
src/AxCopilot/Views/LauncherWindow.Keyboard.cs
Normal file
@@ -0,0 +1,593 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.ViewModels;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class LauncherWindow
|
||||
{
|
||||
// ─── IME 보완 검색 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// WPF 바인딩(UpdateSourceTrigger=PropertyChanged)은 한글 IME 조합 중에는
|
||||
/// ViewModel 업데이트를 지연하므로, TextChanged에서 직접 검색을 트리거합니다.
|
||||
/// InputText 프로퍼티를 건드리지 않아 IME 조합 상태(音節)가 유지됩니다.
|
||||
/// </summary>
|
||||
private void InputBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
// 바인딩이 이미 ViewModel을 업데이트한 경우(조합 완료 후)에는 중복 실행 방지
|
||||
if (_vm.InputText == InputBox.Text) return;
|
||||
// 조합 중 텍스트로 즉시 검색 — InputText 바인딩 우회
|
||||
_ = _vm.TriggerImeSearchAsync(InputBox.Text);
|
||||
}
|
||||
|
||||
// ─── 키보드 이벤트 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Window 레벨 PreviewKeyDown — 터널링으로 먼저 실행되므로
|
||||
/// TextBox 내부 ScrollViewer가 Up/Down을 소비하기 전에 인터셉트합니다.
|
||||
/// </summary>
|
||||
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
bool shift = (Keyboard.Modifiers & ModifierKeys.Shift) != 0;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Escape:
|
||||
if (_vm.IsActionMode)
|
||||
_vm.ExitActionMode();
|
||||
else
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Enter:
|
||||
// Ctrl+Enter, Alt+Enter → Window_KeyDown에서 처리
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 ||
|
||||
(Keyboard.Modifiers & ModifierKeys.Alt) != 0)
|
||||
return;
|
||||
|
||||
if (shift)
|
||||
{
|
||||
// 퍼지 파일 검색 결과: Shift+Enter → 파일이 있는 폴더 열기
|
||||
if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry shiftEntry)
|
||||
{
|
||||
var expanded = Environment.ExpandEnvironmentVariables(shiftEntry.Path);
|
||||
Hide();
|
||||
// File.Exists/Directory.Exists 생략 — 탐색기가 없는 경로는 알아서 처리
|
||||
// 폴더인 경우 바로 열기, 파일인 경우 /select로 위치 표시
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (shiftEntry.Type == Services.IndexEntryType.Folder)
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"\"{expanded}\"");
|
||||
else
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{expanded}\"");
|
||||
}
|
||||
catch (Exception) { }
|
||||
});
|
||||
}
|
||||
// 캡처 모드: 지연 캡처 타이머 표시
|
||||
else if (_vm.ActivePrefix != null &&
|
||||
_vm.ActivePrefix.Equals("cap", StringComparison.OrdinalIgnoreCase) &&
|
||||
_vm.ShowDelayTimerItems())
|
||||
{
|
||||
// 타이머 선택 목록으로 전환됨 — Enter로 선택
|
||||
}
|
||||
else if (_vm.MergeCount > 0)
|
||||
_vm.ExecuteMerge();
|
||||
else
|
||||
ShowLargeType();
|
||||
}
|
||||
else if (_vm.IsActionMode && TryHandleSpecialAction())
|
||||
{
|
||||
// 삭제/이름 변경 등 특수 액션 처리됨 — 별도 처리
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Down:
|
||||
if (shift)
|
||||
{
|
||||
_vm.ToggleMergeItem(_vm.SelectedItem);
|
||||
_vm.SelectNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.SelectNext();
|
||||
}
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Up:
|
||||
if (shift)
|
||||
{
|
||||
_vm.ToggleMergeItem(_vm.SelectedItem);
|
||||
_vm.SelectPrev();
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.SelectPrev();
|
||||
}
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Right:
|
||||
// 커서가 입력 끝에 있고 선택된 항목이 파일/앱이면 액션 서브메뉴 진입
|
||||
if (InputBox.CaretIndex == InputBox.Text.Length
|
||||
&& InputBox.Text.Length > 0
|
||||
&& _vm.CanEnterActionMode())
|
||||
{
|
||||
_vm.EnterActionMode(_vm.SelectedItem!);
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.PageDown:
|
||||
for (int i = 0; i < 5 && _vm.Results.Count > 0; i++) _vm.SelectNext();
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.PageUp:
|
||||
for (int i = 0; i < 5 && _vm.Results.Count > 0; i++) _vm.SelectPrev();
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Home:
|
||||
// 입력창 커서가 맨 앞이거나 입력이 없을 때 → 목록 첫 항목으로 이동
|
||||
if (InputBox.CaretIndex == 0 || string.IsNullOrEmpty(InputBox.Text))
|
||||
{
|
||||
_vm.SelectFirst();
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.End:
|
||||
// 입력창 커서가 맨 끝이거나 입력이 없을 때 → 목록 마지막 항목으로 이동
|
||||
if (InputBox.CaretIndex == InputBox.Text.Length || string.IsNullOrEmpty(InputBox.Text))
|
||||
{
|
||||
_vm.SelectLast();
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.Tab:
|
||||
// 자동완성: 선택된 항목의 Title을 입력창에 채우고 커서를 끝으로 이동
|
||||
if (_vm.SelectedItem != null)
|
||||
{
|
||||
_vm.InputText = _vm.SelectedItem.Title;
|
||||
// 바인딩 업데이트 후 커서를 끝으로 — Dispatcher로 다음 렌더 사이클에 실행
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
InputBox.CaretIndex = InputBox.Text.Length;
|
||||
InputBox.Focus();
|
||||
}, System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
var mod = Keyboard.Modifiers;
|
||||
|
||||
// ─── Ctrl+, → 설정 창 열기 ─────────────────────────────────────────
|
||||
if (e.Key == Key.OemComma && mod == ModifierKeys.Control)
|
||||
{
|
||||
Hide();
|
||||
OpenSettingsAction?.Invoke();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── F1 → 도움말 창 열기 ────────────────────────────────────────────
|
||||
if (e.Key == Key.F1)
|
||||
{
|
||||
_vm.InputText = "help";
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── F5 → 인덱스 새로 고침 ──────────────────────────────────────────
|
||||
if (e.Key == Key.F5)
|
||||
{
|
||||
var app = (App)System.Windows.Application.Current;
|
||||
_ = app.IndexService?.BuildAsync(CancellationToken.None);
|
||||
IndexStatusText.Text = "⟳ 인덱스 재구축 중…";
|
||||
IndexStatusText.Visibility = Visibility.Visible;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Delete → 항목 삭제 ─────────────────────────────────────────────
|
||||
if (e.Key == Key.Delete && mod == ModifierKeys.None)
|
||||
{
|
||||
if (_vm.SelectedItem != null)
|
||||
{
|
||||
var input = _vm.InputText ?? "";
|
||||
// note 예약어 활성 상태에서 메모 개별 삭제
|
||||
if (input.StartsWith("note", StringComparison.OrdinalIgnoreCase)
|
||||
&& _vm.SelectedItem.Data is string noteContent
|
||||
&& noteContent != "__CLEAR__")
|
||||
{
|
||||
var title = _vm.SelectedItem.Title;
|
||||
var result = CustomMessageBox.Show(
|
||||
$"'{title}' 메모를 삭제하시겠습니까?",
|
||||
"AX Copilot",
|
||||
MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.OK)
|
||||
{
|
||||
Handlers.NoteHandler.DeleteNote(noteContent);
|
||||
// 결과 목록 새로고침 (InputText 재설정으로 SearchAsync 트리거)
|
||||
var current = _vm.InputText ?? "";
|
||||
_vm.InputText = current + " ";
|
||||
_vm.InputText = current;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var title = _vm.SelectedItem.Title;
|
||||
var result = CustomMessageBox.Show(
|
||||
$"'{title}' 항목을 목록에서 제거하시겠습니까?",
|
||||
"AX Copilot",
|
||||
MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.OK)
|
||||
_vm.RemoveSelectedFromRecent();
|
||||
}
|
||||
}
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+L → 입력창 초기화 ─────────────────────────────────────────
|
||||
if (e.Key == Key.L && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.ClearInput();
|
||||
InputBox.Focus();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+C → 선택 항목 이름 복사 (결과 선택 시) ────────────────────
|
||||
if (e.Key == Key.C && mod == ModifierKeys.Control && _vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry)
|
||||
{
|
||||
_vm.CopySelectedPath();
|
||||
ShowToast("이름 복사됨");
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+Shift+C → 전체 경로 복사 ──────────────────────────────────
|
||||
if (e.Key == Key.C && mod == (ModifierKeys.Control | ModifierKeys.Shift))
|
||||
{
|
||||
if (_vm.CopySelectedFullPath())
|
||||
ShowToast("경로 복사됨");
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+Shift+E → 파일 탐색기에서 열기 ────────────────────────────
|
||||
if (e.Key == Key.E && mod == (ModifierKeys.Control | ModifierKeys.Shift))
|
||||
{
|
||||
if (_vm.OpenSelectedInExplorer())
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+Enter → 관리자 권한 실행 ──────────────────────────────────
|
||||
if (e.Key == Key.Enter && mod == ModifierKeys.Control)
|
||||
{
|
||||
if (_vm.RunSelectedAsAdmin())
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Alt+Enter → 파일 속성 보기 ─────────────────────────────────────
|
||||
if (e.Key == Key.Enter && mod == ModifierKeys.Alt)
|
||||
{
|
||||
_vm.ShowSelectedProperties();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+H → 클립보드 히스토리 ─────────────────────────────────────
|
||||
if (e.Key == Key.H && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.InputText = "#";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+R → 최근 실행 항목 ────────────────────────────────────────
|
||||
if (e.Key == Key.R && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.InputText = "recent";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+B → 즐겨찾기 뷰 토글 (fav 이면 이전 검색으로, 아니면 fav로) ─
|
||||
if (e.Key == Key.B && mod == ModifierKeys.Control)
|
||||
{
|
||||
if (_vm.InputText.TrimStart().Equals("fav", StringComparison.OrdinalIgnoreCase))
|
||||
_vm.ClearInput();
|
||||
else
|
||||
_vm.InputText = "fav";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+K → 단축키 도움말 모달 창 ─────────────────────────────────
|
||||
if (e.Key == Key.K && mod == ModifierKeys.Control)
|
||||
{
|
||||
var helpWin = new ShortcutHelpWindow { Owner = this };
|
||||
helpWin.ShowDialog();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+T → 터미널 열기 (선택 항목 경로, 없으면 홈) ────────────────
|
||||
if (e.Key == Key.T && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.OpenSelectedInTerminal();
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+F → 파일 검색 모드 전환 ───────────────────────────────────
|
||||
if (e.Key == Key.F && mod == ModifierKeys.Control)
|
||||
{
|
||||
// 입력창 초기화 후 파일 타입 필터 힌트
|
||||
_vm.ClearInput();
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
InputBox.Focus();
|
||||
InputBox.CaretIndex = InputBox.Text.Length;
|
||||
}, System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+P → 클립보드 모드에서 핀 토글 / 일반 모드에서 즐겨찾기 ───
|
||||
if (e.Key == Key.P && mod == ModifierKeys.Control)
|
||||
{
|
||||
if (_vm.IsClipboardMode && _vm.SelectedItem?.Data is Services.ClipboardEntry clipEntry)
|
||||
{
|
||||
var clipSvc = CurrentApp?.ClipboardHistoryService;
|
||||
clipSvc?.TogglePin(clipEntry);
|
||||
ShowToast(clipEntry.IsPinned ? "클립보드 핀 고정 📌" : "클립보드 핀 해제");
|
||||
// 검색 결과 갱신
|
||||
_vm.InputText = _vm.InputText;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = _vm.ToggleFavorite();
|
||||
if (result == true)
|
||||
ShowToast("즐겨찾기에 추가됨 ⭐");
|
||||
else if (result == false)
|
||||
ShowToast("즐겨찾기에서 제거됨");
|
||||
else
|
||||
ShowToast("파일/폴더 항목을 선택하세요");
|
||||
}
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+D → 다운로드 폴더 열기 ───────────────────────────────────
|
||||
if (e.Key == Key.D && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.NavigateToDownloads();
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+W → 런처 창 닫기 ──────────────────────────────────────────
|
||||
if (e.Key == Key.W && mod == ModifierKeys.Control)
|
||||
{
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── F2 → 선택 파일 이름 바꾸기 ─────────────────────────────────────
|
||||
if (e.Key == Key.F2)
|
||||
{
|
||||
if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
_vm.InputText = $"rename {path}";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+1~9 → n번째 결과 즉시 실행 ───────────────────────────────
|
||||
if (mod == ModifierKeys.Control)
|
||||
{
|
||||
int num = e.Key switch
|
||||
{
|
||||
Key.D1 => 1, Key.D2 => 2, Key.D3 => 3,
|
||||
Key.D4 => 4, Key.D5 => 5, Key.D6 => 6,
|
||||
Key.D7 => 7, Key.D8 => 8, Key.D9 => 9,
|
||||
_ => 0
|
||||
};
|
||||
if (num > 0 && num <= _vm.Results.Count)
|
||||
{
|
||||
_vm.SelectedItem = _vm.Results[num - 1];
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 단축키 도움말 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>단축키 도움말 팝업</summary>
|
||||
private void ShowShortcutHelp()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"[ 전역 ]",
|
||||
"Alt+Space AX Commander 열기/닫기",
|
||||
"",
|
||||
"[ 탐색 ]",
|
||||
"↑ / ↓ 결과 이동",
|
||||
"Enter 선택 실행",
|
||||
"Tab 자동완성",
|
||||
"→ 액션 모드",
|
||||
"Escape 닫기 / 뒤로",
|
||||
"",
|
||||
"[ 기능 ]",
|
||||
"F1 도움말",
|
||||
"F2 파일 이름 바꾸기",
|
||||
"F5 인덱스 새로 고침",
|
||||
"Delete 항목 제거",
|
||||
"Ctrl+, 설정",
|
||||
"Ctrl+L 입력 초기화",
|
||||
"Ctrl+C 이름 복사",
|
||||
"Ctrl+H 클립보드 히스토리",
|
||||
"Ctrl+R 최근 실행",
|
||||
"Ctrl+B 즐겨찾기",
|
||||
"Ctrl+K 이 도움말",
|
||||
"Ctrl+1~9 N번째 실행",
|
||||
"Ctrl+Shift+C 경로 복사",
|
||||
"Ctrl+Shift+E 탐색기에서 열기",
|
||||
"Ctrl+Enter 관리자 실행",
|
||||
"Alt+Enter 속성 보기",
|
||||
"Shift+Enter 대형 텍스트",
|
||||
};
|
||||
|
||||
CustomMessageBox.Show(
|
||||
string.Join("\n", lines),
|
||||
"AX Commander — 단축키 도움말",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
// ─── 토스트 알림 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>오버레이 토스트 표시 (페이드인 → 2초 대기 → 페이드아웃)</summary>
|
||||
private void ShowToast(string message, string icon = "\uE73E")
|
||||
{
|
||||
ToastText.Text = message;
|
||||
ToastIcon.Text = icon;
|
||||
ToastOverlay.Visibility = Visibility.Visible;
|
||||
ToastOverlay.Opacity = 0;
|
||||
|
||||
// 페이드인
|
||||
var fadeIn = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeIn");
|
||||
fadeIn.Begin(this);
|
||||
|
||||
_indexStatusTimer?.Stop();
|
||||
_indexStatusTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(2)
|
||||
};
|
||||
_indexStatusTimer.Tick += (_, _) =>
|
||||
{
|
||||
_indexStatusTimer.Stop();
|
||||
// 페이드아웃 후 Collapsed
|
||||
var fadeOut = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeOut");
|
||||
EventHandler? onCompleted = null;
|
||||
onCompleted = (__, ___) =>
|
||||
{
|
||||
fadeOut.Completed -= onCompleted;
|
||||
ToastOverlay.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
fadeOut.Completed += onCompleted;
|
||||
fadeOut.Begin(this);
|
||||
};
|
||||
_indexStatusTimer.Start();
|
||||
}
|
||||
|
||||
// ─── 특수 액션 처리 ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 액션 모드에서 특수 처리가 필요한 동작(삭제/이름변경)을 처리합니다.
|
||||
/// 처리되면 true 반환 → ExecuteSelectedAsync 호출 생략.
|
||||
/// </summary>
|
||||
private bool TryHandleSpecialAction()
|
||||
{
|
||||
if (_vm.SelectedItem?.Data is not AxCopilot.ViewModels.FileActionData actionData)
|
||||
return false;
|
||||
|
||||
switch (actionData.Action)
|
||||
{
|
||||
case AxCopilot.ViewModels.FileAction.DeleteToRecycleBin:
|
||||
{
|
||||
var path = actionData.Path;
|
||||
var name = System.IO.Path.GetFileName(path);
|
||||
var r = CustomMessageBox.Show(
|
||||
$"'{name}'\n\n이 항목을 휴지통으로 보내겠습니까?",
|
||||
"AX Copilot — 삭제 확인",
|
||||
MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Warning);
|
||||
|
||||
if (r == MessageBoxResult.OK)
|
||||
{
|
||||
try
|
||||
{
|
||||
SendToRecycleBin(path);
|
||||
_vm.ExitActionMode();
|
||||
ShowToast("휴지통으로 이동됨", "\uE74D");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CustomMessageBox.Show($"삭제 실패: {ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.ExitActionMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case AxCopilot.ViewModels.FileAction.Rename:
|
||||
{
|
||||
var path = actionData.Path;
|
||||
_vm.ExitActionMode();
|
||||
_vm.InputText = $"rename {path}";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
177
src/AxCopilot/Views/LauncherWindow.Shell.cs
Normal file
177
src/AxCopilot/Views/LauncherWindow.Shell.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class LauncherWindow
|
||||
{
|
||||
// ─── Shell32 휴지통 삭제 ────────────────────────────────────────────────
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct SHFILEOPSTRUCT
|
||||
{
|
||||
public IntPtr hwnd;
|
||||
public uint wFunc;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string pFrom;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string? pTo;
|
||||
public ushort fFlags;
|
||||
[MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted;
|
||||
public IntPtr hNameMappings;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string? lpszProgressTitle;
|
||||
}
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int SHFileOperation(ref SHFILEOPSTRUCT lpFileOp);
|
||||
|
||||
private const uint FO_DELETE = 0x0003;
|
||||
private const ushort FOF_ALLOWUNDO = 0x0040;
|
||||
private const ushort FOF_NOCONFIRMATION = 0x0010;
|
||||
private const ushort FOF_SILENT = 0x0004;
|
||||
|
||||
/// <summary>파일·폴더를 Windows 휴지통으로 보냅니다.</summary>
|
||||
private void SendToRecycleBin(string path)
|
||||
{
|
||||
// pFrom은 null-terminated + 추가 null 필요
|
||||
var op = new SHFILEOPSTRUCT
|
||||
{
|
||||
hwnd = new WindowInteropHelper(this).Handle,
|
||||
wFunc = FO_DELETE,
|
||||
pFrom = path + '\0',
|
||||
fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT,
|
||||
};
|
||||
int result = SHFileOperation(ref op);
|
||||
if (result != 0)
|
||||
throw new System.ComponentModel.Win32Exception(result, $"SHFileOperation 실패 (코드 {result})");
|
||||
}
|
||||
|
||||
// ─── 대형 텍스트 / 클립보드 외부 뷰어 ──────────────────────────────────
|
||||
|
||||
private void ShowLargeType()
|
||||
{
|
||||
// 클립보드 항목 → 시스템 클립보드에 자동 복사 + 외부 앱에서 열기
|
||||
if (_vm.SelectedItem?.Data is Services.ClipboardEntry clipEntry)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 자동 클립보드 복사 억제 (히스토리 중복 방지)
|
||||
CurrentApp?.ClipboardHistoryService?.SuppressNextCapture();
|
||||
|
||||
if (!clipEntry.IsText && clipEntry.Image != null)
|
||||
{
|
||||
// 원본 이미지가 있으면 원본 사용, 없으면 썸네일 사용
|
||||
var originalImg = Services.ClipboardHistoryService.LoadOriginalImage(clipEntry.OriginalImagePath);
|
||||
var imgToUse = originalImg ?? clipEntry.Image;
|
||||
|
||||
// 시스템 클립보드에 원본 복사
|
||||
Clipboard.SetImage(imgToUse);
|
||||
|
||||
// 이미지: PNG로 저장 → 기본 이미지 뷰어
|
||||
string path;
|
||||
if (!string.IsNullOrEmpty(clipEntry.OriginalImagePath) &&
|
||||
System.IO.File.Exists(clipEntry.OriginalImagePath))
|
||||
{
|
||||
path = clipEntry.OriginalImagePath; // 원본 파일 직접 열기
|
||||
}
|
||||
else
|
||||
{
|
||||
path = Services.TempFileService.CreateTempFile("clip_image", ".png");
|
||||
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(imgToUse));
|
||||
using var fs = new System.IO.FileStream(path, System.IO.FileMode.Create);
|
||||
encoder.Save(fs);
|
||||
}
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(clipEntry.Text))
|
||||
{
|
||||
// 시스템 클립보드에 텍스트 복사
|
||||
Clipboard.SetText(clipEntry.Text);
|
||||
|
||||
// 텍스트: txt로 저장 → 메모장
|
||||
var path = Services.TempFileService.CreateTempFile("clip_text", ".txt");
|
||||
System.IO.File.WriteAllText(path, clipEntry.Text, System.Text.Encoding.UTF8);
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("notepad.exe", $"\"{path}\"") { UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Warn($"클립보드 외부 뷰어 실패: {ex.Message}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var text = _vm.GetLargeTypeText();
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
new LargeTypeWindow(text).Show();
|
||||
}
|
||||
|
||||
// ─── 마우스 클릭 처리 ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>이미 선택된 아이템을 클릭하면 Execute, 아직 선택되지 않은 아이템 클릭은 선택만.</summary>
|
||||
private SDK.LauncherItem? _lastClickedItem;
|
||||
private DateTime _lastClickTime;
|
||||
|
||||
private void ResultList_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 클릭한 ListViewItem 찾기
|
||||
var dep = e.OriginalSource as DependencyObject;
|
||||
while (dep != null && dep is not System.Windows.Controls.ListViewItem)
|
||||
dep = System.Windows.Media.VisualTreeHelper.GetParent(dep);
|
||||
if (dep is not System.Windows.Controls.ListViewItem lvi) return;
|
||||
|
||||
var clickedItem = lvi.Content as SDK.LauncherItem;
|
||||
if (clickedItem == null) return;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
|
||||
|
||||
if (_lastClickedItem == clickedItem && timeSinceLastClick < 600)
|
||||
{
|
||||
// 같은 아이템을 짧은 간격으로 재클릭 → 액션 모드 또는 실행
|
||||
if (!_vm.IsActionMode && _vm.CanEnterActionMode())
|
||||
{
|
||||
_vm.EnterActionMode(clickedItem);
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
e.Handled = true;
|
||||
}
|
||||
_lastClickedItem = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 첫 번째 클릭 → 선택만
|
||||
_lastClickedItem = clickedItem;
|
||||
_lastClickTime = now;
|
||||
}
|
||||
|
||||
private void ResultList_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
}
|
||||
|
||||
// ─── 창 이벤트 / 스크롤 / 알림 ─────────────────────────────────────────
|
||||
|
||||
private void Window_Deactivated(object sender, EventArgs e)
|
||||
{
|
||||
// 설정 › 기능 › "포커스 잃으면 닫기"가 켜진 경우에만 자동 숨김
|
||||
if (_vm.CloseOnFocusLost) Hide();
|
||||
}
|
||||
|
||||
private void ScrollToSelected()
|
||||
{
|
||||
if (_vm.SelectedItem != null)
|
||||
ResultList.ScrollIntoView(_vm.SelectedItem);
|
||||
}
|
||||
|
||||
private void ShowNotification(string message)
|
||||
{
|
||||
// 시스템 트레이 토스트 알림 표시
|
||||
// App.xaml.cs의 TrayIcon을 통해 처리
|
||||
}
|
||||
}
|
||||
116
src/AxCopilot/Views/LauncherWindow.Theme.cs
Normal file
116
src/AxCopilot/Views/LauncherWindow.Theme.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Win32;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class LauncherWindow
|
||||
{
|
||||
// ─── 테마 ────────────────────────────────────────────────────────────────
|
||||
|
||||
// 지원 테마 이름 목록
|
||||
private static readonly HashSet<string> KnownThemes =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{ "Dark", "Light", "OLED", "Nord", "Monokai", "Catppuccin", "Sepia", "Alfred", "AlfredLight", "Codex" };
|
||||
|
||||
internal void ApplyTheme() =>
|
||||
ApplyTheme(_vm.ThemeSetting, _vm.CustomThemeColors);
|
||||
|
||||
/// <summary>
|
||||
/// 테마를 즉시 교체합니다. 설정 창 미리보기에서도 호출됩니다.
|
||||
/// </summary>
|
||||
internal void ApplyTheme(string? themeKey, AxCopilot.Models.CustomThemeColors? customColors)
|
||||
{
|
||||
var appDicts = System.Windows.Application.Current.Resources.MergedDictionaries;
|
||||
// 기존 테마 딕셔너리 제거 (Source 기반 또는 커스텀 빌드)
|
||||
var existing = appDicts.FirstOrDefault(d =>
|
||||
d.Source?.ToString().Contains("/Themes/") == true || d.Contains("LauncherBackground"));
|
||||
if (existing != null)
|
||||
appDicts.Remove(existing);
|
||||
|
||||
var setting = (themeKey ?? "system").ToLowerInvariant();
|
||||
|
||||
if (setting == "custom" && customColors != null)
|
||||
{
|
||||
appDicts.Add(BuildCustomDictionary(customColors));
|
||||
UpdateSelectionGlow(); // 커스텀 테마도 AccentColor 적용
|
||||
return;
|
||||
}
|
||||
|
||||
var themeName = GetEffectiveThemeName(setting);
|
||||
appDicts.Add(new ResourceDictionary
|
||||
{
|
||||
Source = new Uri($"pack://application:,,,/Themes/{themeName}.xaml")
|
||||
});
|
||||
UpdateSelectionGlow(); // 테마 변경 시 AccentColor 기반으로 글로우 색 갱신
|
||||
}
|
||||
|
||||
private static string GetEffectiveThemeName(string setting) => setting switch
|
||||
{
|
||||
"dark" => "Dark",
|
||||
"light" => "Light",
|
||||
"oled" => "OLED",
|
||||
"nord" => "Nord",
|
||||
"monokai" => "Monokai",
|
||||
"catppuccin" => "Catppuccin",
|
||||
"sepia" => "Sepia",
|
||||
"alfred" => "Alfred",
|
||||
"alfredlight" => "AlfredLight",
|
||||
"codex" => "Codex",
|
||||
_ => IsSystemDarkMode() ? "Dark" : "Light" // "system" 또는 미지원 값
|
||||
};
|
||||
|
||||
private static ResourceDictionary BuildCustomDictionary(CustomThemeColors c)
|
||||
{
|
||||
SolidColorBrush Brush(string hex)
|
||||
{
|
||||
var color = (Color)ColorConverter.ConvertFromString(hex);
|
||||
return new SolidColorBrush(color);
|
||||
}
|
||||
|
||||
return new ResourceDictionary
|
||||
{
|
||||
{ "LauncherBackground", Brush(c.LauncherBackground) },
|
||||
{ "ItemBackground", Brush(c.ItemBackground) },
|
||||
{ "ItemSelectedBackground", Brush(c.ItemSelectedBackground) },
|
||||
{ "ItemSelectedHoverBackground", LightenBrush(Brush(c.ItemSelectedBackground), 0.15) },
|
||||
{ "ItemHoverBackground", Brush(c.ItemHoverBackground) },
|
||||
{ "PrimaryText", Brush(c.PrimaryText) },
|
||||
{ "SecondaryText", Brush(c.SecondaryText) },
|
||||
{ "PlaceholderText", Brush(c.PlaceholderText) },
|
||||
{ "AccentColor", Brush(c.AccentColor) },
|
||||
{ "SeparatorColor", Brush(c.SeparatorColor) },
|
||||
{ "HintBackground", Brush(c.HintBackground) },
|
||||
{ "HintText", Brush(c.HintText) },
|
||||
{ "BorderColor", Brush(c.BorderColor) },
|
||||
{ "ScrollbarThumb", Brush(c.ScrollbarThumb) },
|
||||
{ "ShadowColor", (Color)ColorConverter.ConvertFromString(c.ShadowColor) },
|
||||
// 커스텀 테마: 사용자가 설정한 라운딩 적용
|
||||
{ "WindowCornerRadius", new CornerRadius(Math.Clamp(c.WindowCornerRadius, 0, 30)) },
|
||||
{ "ItemCornerRadius", new CornerRadius(Math.Clamp(c.ItemCornerRadius, 0, 20)) },
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>SolidColorBrush를 지정 비율만큼 밝게 합니다.</summary>
|
||||
private static SolidColorBrush LightenBrush(SolidColorBrush brush, double amount)
|
||||
{
|
||||
var c = brush.Color;
|
||||
byte Clamp(int v) => (byte)Math.Min(255, Math.Max(0, v));
|
||||
return new SolidColorBrush(Color.FromRgb(
|
||||
Clamp(c.R + (int)(255 * amount)),
|
||||
Clamp(c.G + (int)(255 * amount)),
|
||||
Clamp(c.B + (int)(255 * amount))));
|
||||
}
|
||||
|
||||
private static bool IsSystemDarkMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
return key?.GetValue("AppsUseLightTheme") is int v && v == 0;
|
||||
}
|
||||
catch (Exception) { return true; }
|
||||
}
|
||||
}
|
||||
@@ -575,989 +575,4 @@ public partial class LauncherWindow : Window
|
||||
ApplyRandomIconAnimation();
|
||||
}
|
||||
|
||||
// ─── 무지개 글로우 상시 애니메이션 ────────────────────────────────────
|
||||
|
||||
/// <summary>선택 아이템 상시 무지개 글로우 효과를 적용하거나 제거합니다.</summary>
|
||||
private void UpdateSelectionGlow()
|
||||
{
|
||||
if (_vm.EnableSelectionGlow)
|
||||
{
|
||||
var gs = new System.Windows.Media.GradientStopCollection
|
||||
{
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x6B, 0x6B), 0.00),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFE, 0xCA, 0x57), 0.17),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x48, 0xDB, 0xFB), 0.33),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x9F, 0xF3), 0.50),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x54, 0xA0, 0xFF), 0.67),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x5F, 0x27, 0xCD), 0.83),
|
||||
new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x6B, 0x6B), 1.00),
|
||||
};
|
||||
Resources["SelectionGlowBrush"] = new System.Windows.Media.LinearGradientBrush(
|
||||
gs,
|
||||
new System.Windows.Point(0, 0),
|
||||
new System.Windows.Point(1, 1));
|
||||
Resources["SelectionGlowVisibility"] = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
Resources["SelectionGlowBrush"] = System.Windows.Media.Brushes.Transparent;
|
||||
Resources["SelectionGlowVisibility"] = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>무지개 글로우를 정지하고 숨깁니다.</summary>
|
||||
private void StopRainbowGlow()
|
||||
{
|
||||
_rainbowTimer?.Stop();
|
||||
_rainbowTimer = null;
|
||||
if (RainbowGlowBorder != null) RainbowGlowBorder.Opacity = 0;
|
||||
}
|
||||
|
||||
/// <summary>런처 테두리 무지개 그라데이션 회전을 시작합니다.</summary>
|
||||
private void StartRainbowGlow()
|
||||
{
|
||||
_rainbowTimer?.Stop();
|
||||
if (LauncherRainbowBrush == null || RainbowGlowBorder == null) return;
|
||||
|
||||
_rainbowTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(20)
|
||||
};
|
||||
var startTime = DateTime.UtcNow;
|
||||
_rainbowTimer.Tick += (_, _) =>
|
||||
{
|
||||
if (!IsVisible) { _rainbowTimer?.Stop(); return; }
|
||||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
var shift = (elapsed / 2000.0) % 1.0; // 2초에 1바퀴 (느리게)
|
||||
var angle = shift * Math.PI * 2;
|
||||
LauncherRainbowBrush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(angle), 0.5 + 0.5 * Math.Sin(angle));
|
||||
LauncherRainbowBrush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(angle), 0.5 - 0.5 * Math.Sin(angle));
|
||||
};
|
||||
_rainbowTimer.Start();
|
||||
}
|
||||
|
||||
// ─── 애니메이션 헬퍼 ──────────────────────────────────────────────────
|
||||
|
||||
private static KeyTime KT(double sec) => KeyTime.FromTimeSpan(TimeSpan.FromSeconds(sec));
|
||||
|
||||
private static void AddOpacityPulse(Storyboard sb, UIElement target, int index, double totalSec)
|
||||
{
|
||||
var a = new DoubleAnimationUsingKeyFrames();
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(index)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(0.25, KT(index + 0.5)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(index + 1)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(totalSec)));
|
||||
Storyboard.SetTarget(a, target);
|
||||
Storyboard.SetTargetProperty(a, new PropertyPath(UIElement.OpacityProperty));
|
||||
sb.Children.Add(a);
|
||||
}
|
||||
|
||||
private static void AddGroupFlash(Storyboard sb, UIElement[] group, double startSec, double totalSec)
|
||||
{
|
||||
foreach (var p in group)
|
||||
{
|
||||
var a = new DoubleAnimationUsingKeyFrames();
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(0)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(startSec)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(0.2, KT(startSec + 0.6)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(startSec + 1.2)));
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(totalSec)));
|
||||
Storyboard.SetTarget(a, p);
|
||||
Storyboard.SetTargetProperty(a, new PropertyPath(UIElement.OpacityProperty));
|
||||
sb.Children.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
private static DoubleAnimationUsingKeyFrames MakeKeyFrameAnim((double val, double sec)[] frames)
|
||||
{
|
||||
var a = new DoubleAnimationUsingKeyFrames();
|
||||
foreach (var (val, sec) in frames)
|
||||
a.KeyFrames.Add(new LinearDoubleKeyFrame(val, KT(sec)));
|
||||
return a;
|
||||
}
|
||||
|
||||
private void CenterOnScreen()
|
||||
{
|
||||
var screen = SystemParameters.WorkArea;
|
||||
// ActualHeight/ActualWidth는 첫 Show() 전 레이아웃 패스 이전에 0일 수 있음 → 기본값으로 보호
|
||||
var w = ActualWidth > 0 ? ActualWidth : 640;
|
||||
var h = ActualHeight > 0 ? ActualHeight : 80;
|
||||
Left = (screen.Width - w) / 2 + screen.Left;
|
||||
Top = _vm.WindowPosition switch
|
||||
{
|
||||
"center" => (screen.Height - h) / 2 + screen.Top,
|
||||
"bottom" => screen.Height * 0.75 + screen.Top,
|
||||
_ => screen.Height * 0.2 + screen.Top, // "center-top" (기본)
|
||||
};
|
||||
}
|
||||
|
||||
// 지원 테마 이름 목록
|
||||
private static readonly HashSet<string> KnownThemes =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{ "Dark", "Light", "OLED", "Nord", "Monokai", "Catppuccin", "Sepia", "Alfred", "AlfredLight", "Codex" };
|
||||
|
||||
internal void ApplyTheme() =>
|
||||
ApplyTheme(_vm.ThemeSetting, _vm.CustomThemeColors);
|
||||
|
||||
/// <summary>
|
||||
/// 테마를 즉시 교체합니다. 설정 창 미리보기에서도 호출됩니다.
|
||||
/// </summary>
|
||||
internal void ApplyTheme(string? themeKey, AxCopilot.Models.CustomThemeColors? customColors)
|
||||
{
|
||||
var appDicts = System.Windows.Application.Current.Resources.MergedDictionaries;
|
||||
// 기존 테마 딕셔너리 제거 (Source 기반 또는 커스텀 빌드)
|
||||
var existing = appDicts.FirstOrDefault(d =>
|
||||
d.Source?.ToString().Contains("/Themes/") == true || d.Contains("LauncherBackground"));
|
||||
if (existing != null)
|
||||
appDicts.Remove(existing);
|
||||
|
||||
var setting = (themeKey ?? "system").ToLowerInvariant();
|
||||
|
||||
if (setting == "custom" && customColors != null)
|
||||
{
|
||||
appDicts.Add(BuildCustomDictionary(customColors));
|
||||
UpdateSelectionGlow(); // 커스텀 테마도 AccentColor 적용
|
||||
return;
|
||||
}
|
||||
|
||||
var themeName = GetEffectiveThemeName(setting);
|
||||
appDicts.Add(new ResourceDictionary
|
||||
{
|
||||
Source = new Uri($"pack://application:,,,/Themes/{themeName}.xaml")
|
||||
});
|
||||
UpdateSelectionGlow(); // 테마 변경 시 AccentColor 기반으로 글로우 색 갱신
|
||||
}
|
||||
|
||||
private static string GetEffectiveThemeName(string setting) => setting switch
|
||||
{
|
||||
"dark" => "Dark",
|
||||
"light" => "Light",
|
||||
"oled" => "OLED",
|
||||
"nord" => "Nord",
|
||||
"monokai" => "Monokai",
|
||||
"catppuccin" => "Catppuccin",
|
||||
"sepia" => "Sepia",
|
||||
"alfred" => "Alfred",
|
||||
"alfredlight" => "AlfredLight",
|
||||
"codex" => "Codex",
|
||||
_ => IsSystemDarkMode() ? "Dark" : "Light" // "system" 또는 미지원 값
|
||||
};
|
||||
|
||||
private static ResourceDictionary BuildCustomDictionary(CustomThemeColors c)
|
||||
{
|
||||
SolidColorBrush Brush(string hex)
|
||||
{
|
||||
var color = (Color)ColorConverter.ConvertFromString(hex);
|
||||
return new SolidColorBrush(color);
|
||||
}
|
||||
|
||||
return new ResourceDictionary
|
||||
{
|
||||
{ "LauncherBackground", Brush(c.LauncherBackground) },
|
||||
{ "ItemBackground", Brush(c.ItemBackground) },
|
||||
{ "ItemSelectedBackground", Brush(c.ItemSelectedBackground) },
|
||||
{ "ItemSelectedHoverBackground", LightenBrush(Brush(c.ItemSelectedBackground), 0.15) },
|
||||
{ "ItemHoverBackground", Brush(c.ItemHoverBackground) },
|
||||
{ "PrimaryText", Brush(c.PrimaryText) },
|
||||
{ "SecondaryText", Brush(c.SecondaryText) },
|
||||
{ "PlaceholderText", Brush(c.PlaceholderText) },
|
||||
{ "AccentColor", Brush(c.AccentColor) },
|
||||
{ "SeparatorColor", Brush(c.SeparatorColor) },
|
||||
{ "HintBackground", Brush(c.HintBackground) },
|
||||
{ "HintText", Brush(c.HintText) },
|
||||
{ "BorderColor", Brush(c.BorderColor) },
|
||||
{ "ScrollbarThumb", Brush(c.ScrollbarThumb) },
|
||||
{ "ShadowColor", (Color)ColorConverter.ConvertFromString(c.ShadowColor) },
|
||||
// 커스텀 테마: 사용자가 설정한 라운딩 적용
|
||||
{ "WindowCornerRadius", new CornerRadius(Math.Clamp(c.WindowCornerRadius, 0, 30)) },
|
||||
{ "ItemCornerRadius", new CornerRadius(Math.Clamp(c.ItemCornerRadius, 0, 20)) },
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>SolidColorBrush를 지정 비율만큼 밝게 합니다.</summary>
|
||||
private static SolidColorBrush LightenBrush(SolidColorBrush brush, double amount)
|
||||
{
|
||||
var c = brush.Color;
|
||||
byte Clamp(int v) => (byte)Math.Min(255, Math.Max(0, v));
|
||||
return new SolidColorBrush(Color.FromRgb(
|
||||
Clamp(c.R + (int)(255 * amount)),
|
||||
Clamp(c.G + (int)(255 * amount)),
|
||||
Clamp(c.B + (int)(255 * amount))));
|
||||
}
|
||||
|
||||
private static bool IsSystemDarkMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
return key?.GetValue("AppsUseLightTheme") is int v && v == 0;
|
||||
}
|
||||
catch (Exception) { return true; }
|
||||
}
|
||||
|
||||
private void AnimateIn()
|
||||
{
|
||||
Opacity = 0;
|
||||
|
||||
var ease = new System.Windows.Media.Animation.CubicEase
|
||||
{ EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut };
|
||||
|
||||
var fadeAnim = new System.Windows.Media.Animation.DoubleAnimation(0, 1,
|
||||
TimeSpan.FromMilliseconds(100)) { EasingFunction = ease };
|
||||
|
||||
var slideAnim = new System.Windows.Media.Animation.DoubleAnimation(-8, 0,
|
||||
TimeSpan.FromMilliseconds(120)) { EasingFunction = ease };
|
||||
|
||||
BeginAnimation(OpacityProperty, fadeAnim);
|
||||
|
||||
// Window에 AllowsTransparency=True 일 때 RenderTransform을 Window에 직접 설정하면
|
||||
// InvalidOperationException 발생 → Content(루트 Border)에 적용
|
||||
if (Content is System.Windows.FrameworkElement root)
|
||||
{
|
||||
var translate = new System.Windows.Media.TranslateTransform(0, -10);
|
||||
root.RenderTransform = translate;
|
||||
root.RenderTransformOrigin = new System.Windows.Point(0.5, 0);
|
||||
translate.BeginAnimation(System.Windows.Media.TranslateTransform.YProperty, slideAnim);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── IME 보완 검색 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// WPF 바인딩(UpdateSourceTrigger=PropertyChanged)은 한글 IME 조합 중에는
|
||||
/// ViewModel 업데이트를 지연하므로, TextChanged에서 직접 검색을 트리거합니다.
|
||||
/// InputText 프로퍼티를 건드리지 않아 IME 조합 상태(音節)가 유지됩니다.
|
||||
/// </summary>
|
||||
private void InputBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
// 바인딩이 이미 ViewModel을 업데이트한 경우(조합 완료 후)에는 중복 실행 방지
|
||||
if (_vm.InputText == InputBox.Text) return;
|
||||
// 조합 중 텍스트로 즉시 검색 — InputText 바인딩 우회
|
||||
_ = _vm.TriggerImeSearchAsync(InputBox.Text);
|
||||
}
|
||||
|
||||
// ─── 키보드 이벤트 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Window 레벨 PreviewKeyDown — 터널링으로 먼저 실행되므로
|
||||
/// TextBox 내부 ScrollViewer가 Up/Down을 소비하기 전에 인터셉트합니다.
|
||||
/// </summary>
|
||||
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
bool shift = (Keyboard.Modifiers & ModifierKeys.Shift) != 0;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Escape:
|
||||
if (_vm.IsActionMode)
|
||||
_vm.ExitActionMode();
|
||||
else
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Enter:
|
||||
// Ctrl+Enter, Alt+Enter → Window_KeyDown에서 처리
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 ||
|
||||
(Keyboard.Modifiers & ModifierKeys.Alt) != 0)
|
||||
return;
|
||||
|
||||
if (shift)
|
||||
{
|
||||
// 퍼지 파일 검색 결과: Shift+Enter → 파일이 있는 폴더 열기
|
||||
if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry shiftEntry)
|
||||
{
|
||||
var expanded = Environment.ExpandEnvironmentVariables(shiftEntry.Path);
|
||||
Hide();
|
||||
// File.Exists/Directory.Exists 생략 — 탐색기가 없는 경로는 알아서 처리
|
||||
// 폴더인 경우 바로 열기, 파일인 경우 /select로 위치 표시
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (shiftEntry.Type == Services.IndexEntryType.Folder)
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"\"{expanded}\"");
|
||||
else
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{expanded}\"");
|
||||
}
|
||||
catch (Exception) { }
|
||||
});
|
||||
}
|
||||
// 캡처 모드: 지연 캡처 타이머 표시
|
||||
else if (_vm.ActivePrefix != null &&
|
||||
_vm.ActivePrefix.Equals("cap", StringComparison.OrdinalIgnoreCase) &&
|
||||
_vm.ShowDelayTimerItems())
|
||||
{
|
||||
// 타이머 선택 목록으로 전환됨 — Enter로 선택
|
||||
}
|
||||
else if (_vm.MergeCount > 0)
|
||||
_vm.ExecuteMerge();
|
||||
else
|
||||
ShowLargeType();
|
||||
}
|
||||
else if (_vm.IsActionMode && TryHandleSpecialAction())
|
||||
{
|
||||
// 삭제/이름 변경 등 특수 액션 처리됨 — 별도 처리
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Down:
|
||||
if (shift)
|
||||
{
|
||||
_vm.ToggleMergeItem(_vm.SelectedItem);
|
||||
_vm.SelectNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.SelectNext();
|
||||
}
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Up:
|
||||
if (shift)
|
||||
{
|
||||
_vm.ToggleMergeItem(_vm.SelectedItem);
|
||||
_vm.SelectPrev();
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.SelectPrev();
|
||||
}
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Right:
|
||||
// 커서가 입력 끝에 있고 선택된 항목이 파일/앱이면 액션 서브메뉴 진입
|
||||
if (InputBox.CaretIndex == InputBox.Text.Length
|
||||
&& InputBox.Text.Length > 0
|
||||
&& _vm.CanEnterActionMode())
|
||||
{
|
||||
_vm.EnterActionMode(_vm.SelectedItem!);
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.PageDown:
|
||||
for (int i = 0; i < 5 && _vm.Results.Count > 0; i++) _vm.SelectNext();
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.PageUp:
|
||||
for (int i = 0; i < 5 && _vm.Results.Count > 0; i++) _vm.SelectPrev();
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Home:
|
||||
// 입력창 커서가 맨 앞이거나 입력이 없을 때 → 목록 첫 항목으로 이동
|
||||
if (InputBox.CaretIndex == 0 || string.IsNullOrEmpty(InputBox.Text))
|
||||
{
|
||||
_vm.SelectFirst();
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.End:
|
||||
// 입력창 커서가 맨 끝이거나 입력이 없을 때 → 목록 마지막 항목으로 이동
|
||||
if (InputBox.CaretIndex == InputBox.Text.Length || string.IsNullOrEmpty(InputBox.Text))
|
||||
{
|
||||
_vm.SelectLast();
|
||||
ScrollToSelected();
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.Tab:
|
||||
// 자동완성: 선택된 항목의 Title을 입력창에 채우고 커서를 끝으로 이동
|
||||
if (_vm.SelectedItem != null)
|
||||
{
|
||||
_vm.InputText = _vm.SelectedItem.Title;
|
||||
// 바인딩 업데이트 후 커서를 끝으로 — Dispatcher로 다음 렌더 사이클에 실행
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
InputBox.CaretIndex = InputBox.Text.Length;
|
||||
InputBox.Focus();
|
||||
}, System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
var mod = Keyboard.Modifiers;
|
||||
|
||||
// ─── Ctrl+, → 설정 창 열기 ─────────────────────────────────────────
|
||||
if (e.Key == Key.OemComma && mod == ModifierKeys.Control)
|
||||
{
|
||||
Hide();
|
||||
OpenSettingsAction?.Invoke();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── F1 → 도움말 창 열기 ────────────────────────────────────────────
|
||||
if (e.Key == Key.F1)
|
||||
{
|
||||
_vm.InputText = "help";
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── F5 → 인덱스 새로 고침 ──────────────────────────────────────────
|
||||
if (e.Key == Key.F5)
|
||||
{
|
||||
var app = (App)System.Windows.Application.Current;
|
||||
_ = app.IndexService?.BuildAsync(CancellationToken.None);
|
||||
IndexStatusText.Text = "⟳ 인덱스 재구축 중…";
|
||||
IndexStatusText.Visibility = Visibility.Visible;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Delete → 항목 삭제 ─────────────────────────────────────────────
|
||||
if (e.Key == Key.Delete && mod == ModifierKeys.None)
|
||||
{
|
||||
if (_vm.SelectedItem != null)
|
||||
{
|
||||
var input = _vm.InputText ?? "";
|
||||
// note 예약어 활성 상태에서 메모 개별 삭제
|
||||
if (input.StartsWith("note", StringComparison.OrdinalIgnoreCase)
|
||||
&& _vm.SelectedItem.Data is string noteContent
|
||||
&& noteContent != "__CLEAR__")
|
||||
{
|
||||
var title = _vm.SelectedItem.Title;
|
||||
var result = CustomMessageBox.Show(
|
||||
$"'{title}' 메모를 삭제하시겠습니까?",
|
||||
"AX Copilot",
|
||||
MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.OK)
|
||||
{
|
||||
Handlers.NoteHandler.DeleteNote(noteContent);
|
||||
// 결과 목록 새로고침 (InputText 재설정으로 SearchAsync 트리거)
|
||||
var current = _vm.InputText ?? "";
|
||||
_vm.InputText = current + " ";
|
||||
_vm.InputText = current;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var title = _vm.SelectedItem.Title;
|
||||
var result = CustomMessageBox.Show(
|
||||
$"'{title}' 항목을 목록에서 제거하시겠습니까?",
|
||||
"AX Copilot",
|
||||
MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.OK)
|
||||
_vm.RemoveSelectedFromRecent();
|
||||
}
|
||||
}
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+L → 입력창 초기화 ─────────────────────────────────────────
|
||||
if (e.Key == Key.L && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.ClearInput();
|
||||
InputBox.Focus();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+C → 선택 항목 이름 복사 (결과 선택 시) ────────────────────
|
||||
if (e.Key == Key.C && mod == ModifierKeys.Control && _vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry)
|
||||
{
|
||||
_vm.CopySelectedPath();
|
||||
ShowToast("이름 복사됨");
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+Shift+C → 전체 경로 복사 ──────────────────────────────────
|
||||
if (e.Key == Key.C && mod == (ModifierKeys.Control | ModifierKeys.Shift))
|
||||
{
|
||||
if (_vm.CopySelectedFullPath())
|
||||
ShowToast("경로 복사됨");
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+Shift+E → 파일 탐색기에서 열기 ────────────────────────────
|
||||
if (e.Key == Key.E && mod == (ModifierKeys.Control | ModifierKeys.Shift))
|
||||
{
|
||||
if (_vm.OpenSelectedInExplorer())
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+Enter → 관리자 권한 실행 ──────────────────────────────────
|
||||
if (e.Key == Key.Enter && mod == ModifierKeys.Control)
|
||||
{
|
||||
if (_vm.RunSelectedAsAdmin())
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Alt+Enter → 파일 속성 보기 ─────────────────────────────────────
|
||||
if (e.Key == Key.Enter && mod == ModifierKeys.Alt)
|
||||
{
|
||||
_vm.ShowSelectedProperties();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+H → 클립보드 히스토리 ─────────────────────────────────────
|
||||
if (e.Key == Key.H && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.InputText = "#";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+R → 최근 실행 항목 ────────────────────────────────────────
|
||||
if (e.Key == Key.R && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.InputText = "recent";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+B → 즐겨찾기 뷰 토글 (fav 이면 이전 검색으로, 아니면 fav로) ─
|
||||
if (e.Key == Key.B && mod == ModifierKeys.Control)
|
||||
{
|
||||
if (_vm.InputText.TrimStart().Equals("fav", StringComparison.OrdinalIgnoreCase))
|
||||
_vm.ClearInput();
|
||||
else
|
||||
_vm.InputText = "fav";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+K → 단축키 도움말 모달 창 ─────────────────────────────────
|
||||
if (e.Key == Key.K && mod == ModifierKeys.Control)
|
||||
{
|
||||
var helpWin = new ShortcutHelpWindow { Owner = this };
|
||||
helpWin.ShowDialog();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+T → 터미널 열기 (선택 항목 경로, 없으면 홈) ────────────────
|
||||
if (e.Key == Key.T && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.OpenSelectedInTerminal();
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+F → 파일 검색 모드 전환 ───────────────────────────────────
|
||||
if (e.Key == Key.F && mod == ModifierKeys.Control)
|
||||
{
|
||||
// 입력창 초기화 후 파일 타입 필터 힌트
|
||||
_vm.ClearInput();
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
InputBox.Focus();
|
||||
InputBox.CaretIndex = InputBox.Text.Length;
|
||||
}, System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+P → 클립보드 모드에서 핀 토글 / 일반 모드에서 즐겨찾기 ───
|
||||
if (e.Key == Key.P && mod == ModifierKeys.Control)
|
||||
{
|
||||
if (_vm.IsClipboardMode && _vm.SelectedItem?.Data is Services.ClipboardEntry clipEntry)
|
||||
{
|
||||
var clipSvc = CurrentApp?.ClipboardHistoryService;
|
||||
clipSvc?.TogglePin(clipEntry);
|
||||
ShowToast(clipEntry.IsPinned ? "클립보드 핀 고정 📌" : "클립보드 핀 해제");
|
||||
// 검색 결과 갱신
|
||||
_vm.InputText = _vm.InputText;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = _vm.ToggleFavorite();
|
||||
if (result == true)
|
||||
ShowToast("즐겨찾기에 추가됨 ⭐");
|
||||
else if (result == false)
|
||||
ShowToast("즐겨찾기에서 제거됨");
|
||||
else
|
||||
ShowToast("파일/폴더 항목을 선택하세요");
|
||||
}
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+D → 다운로드 폴더 열기 ───────────────────────────────────
|
||||
if (e.Key == Key.D && mod == ModifierKeys.Control)
|
||||
{
|
||||
_vm.NavigateToDownloads();
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+W → 런처 창 닫기 ──────────────────────────────────────────
|
||||
if (e.Key == Key.W && mod == ModifierKeys.Control)
|
||||
{
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── F2 → 선택 파일 이름 바꾸기 ─────────────────────────────────────
|
||||
if (e.Key == Key.F2)
|
||||
{
|
||||
if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
_vm.InputText = $"rename {path}";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+1~9 → n번째 결과 즉시 실행 ───────────────────────────────
|
||||
if (mod == ModifierKeys.Control)
|
||||
{
|
||||
int num = e.Key switch
|
||||
{
|
||||
Key.D1 => 1, Key.D2 => 2, Key.D3 => 3,
|
||||
Key.D4 => 4, Key.D5 => 5, Key.D6 => 6,
|
||||
Key.D7 => 7, Key.D8 => 8, Key.D9 => 9,
|
||||
_ => 0
|
||||
};
|
||||
if (num > 0 && num <= _vm.Results.Count)
|
||||
{
|
||||
_vm.SelectedItem = _vm.Results[num - 1];
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
Hide();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>단축키 도움말 팝업</summary>
|
||||
private void ShowShortcutHelp()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"[ 전역 ]",
|
||||
"Alt+Space AX Commander 열기/닫기",
|
||||
"",
|
||||
"[ 탐색 ]",
|
||||
"↑ / ↓ 결과 이동",
|
||||
"Enter 선택 실행",
|
||||
"Tab 자동완성",
|
||||
"→ 액션 모드",
|
||||
"Escape 닫기 / 뒤로",
|
||||
"",
|
||||
"[ 기능 ]",
|
||||
"F1 도움말",
|
||||
"F2 파일 이름 바꾸기",
|
||||
"F5 인덱스 새로 고침",
|
||||
"Delete 항목 제거",
|
||||
"Ctrl+, 설정",
|
||||
"Ctrl+L 입력 초기화",
|
||||
"Ctrl+C 이름 복사",
|
||||
"Ctrl+H 클립보드 히스토리",
|
||||
"Ctrl+R 최근 실행",
|
||||
"Ctrl+B 즐겨찾기",
|
||||
"Ctrl+K 이 도움말",
|
||||
"Ctrl+1~9 N번째 실행",
|
||||
"Ctrl+Shift+C 경로 복사",
|
||||
"Ctrl+Shift+E 탐색기에서 열기",
|
||||
"Ctrl+Enter 관리자 실행",
|
||||
"Alt+Enter 속성 보기",
|
||||
"Shift+Enter 대형 텍스트",
|
||||
};
|
||||
|
||||
CustomMessageBox.Show(
|
||||
string.Join("\n", lines),
|
||||
"AX Commander — 단축키 도움말",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
/// <summary>오버레이 토스트 표시 (페이드인 → 2초 대기 → 페이드아웃)</summary>
|
||||
private void ShowToast(string message, string icon = "\uE73E")
|
||||
{
|
||||
ToastText.Text = message;
|
||||
ToastIcon.Text = icon;
|
||||
ToastOverlay.Visibility = Visibility.Visible;
|
||||
ToastOverlay.Opacity = 0;
|
||||
|
||||
// 페이드인
|
||||
var fadeIn = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeIn");
|
||||
fadeIn.Begin(this);
|
||||
|
||||
_indexStatusTimer?.Stop();
|
||||
_indexStatusTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(2)
|
||||
};
|
||||
_indexStatusTimer.Tick += (_, _) =>
|
||||
{
|
||||
_indexStatusTimer.Stop();
|
||||
// 페이드아웃 후 Collapsed
|
||||
var fadeOut = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeOut");
|
||||
EventHandler? onCompleted = null;
|
||||
onCompleted = (__, ___) =>
|
||||
{
|
||||
fadeOut.Completed -= onCompleted;
|
||||
ToastOverlay.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
fadeOut.Completed += onCompleted;
|
||||
fadeOut.Begin(this);
|
||||
};
|
||||
_indexStatusTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 액션 모드에서 특수 처리가 필요한 동작(삭제/이름변경)을 처리합니다.
|
||||
/// 처리되면 true 반환 → ExecuteSelectedAsync 호출 생략.
|
||||
/// </summary>
|
||||
private bool TryHandleSpecialAction()
|
||||
{
|
||||
if (_vm.SelectedItem?.Data is not AxCopilot.ViewModels.FileActionData actionData)
|
||||
return false;
|
||||
|
||||
switch (actionData.Action)
|
||||
{
|
||||
case AxCopilot.ViewModels.FileAction.DeleteToRecycleBin:
|
||||
{
|
||||
var path = actionData.Path;
|
||||
var name = System.IO.Path.GetFileName(path);
|
||||
var r = CustomMessageBox.Show(
|
||||
$"'{name}'\n\n이 항목을 휴지통으로 보내겠습니까?",
|
||||
"AX Copilot — 삭제 확인",
|
||||
MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Warning);
|
||||
|
||||
if (r == MessageBoxResult.OK)
|
||||
{
|
||||
try
|
||||
{
|
||||
SendToRecycleBin(path);
|
||||
_vm.ExitActionMode();
|
||||
ShowToast("휴지통으로 이동됨", "\uE74D");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CustomMessageBox.Show($"삭제 실패: {ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.ExitActionMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case AxCopilot.ViewModels.FileAction.Rename:
|
||||
{
|
||||
var path = actionData.Path;
|
||||
_vm.ExitActionMode();
|
||||
_vm.InputText = $"rename {path}";
|
||||
Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Shell32 휴지통 삭제 ────────────────────────────────────────────────
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct SHFILEOPSTRUCT
|
||||
{
|
||||
public IntPtr hwnd;
|
||||
public uint wFunc;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string pFrom;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string? pTo;
|
||||
public ushort fFlags;
|
||||
[MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted;
|
||||
public IntPtr hNameMappings;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string? lpszProgressTitle;
|
||||
}
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int SHFileOperation(ref SHFILEOPSTRUCT lpFileOp);
|
||||
|
||||
private const uint FO_DELETE = 0x0003;
|
||||
private const ushort FOF_ALLOWUNDO = 0x0040;
|
||||
private const ushort FOF_NOCONFIRMATION = 0x0010;
|
||||
private const ushort FOF_SILENT = 0x0004;
|
||||
|
||||
/// <summary>파일·폴더를 Windows 휴지통으로 보냅니다.</summary>
|
||||
private void SendToRecycleBin(string path)
|
||||
{
|
||||
// pFrom은 null-terminated + 추가 null 필요
|
||||
var op = new SHFILEOPSTRUCT
|
||||
{
|
||||
hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle,
|
||||
wFunc = FO_DELETE,
|
||||
pFrom = path + '\0',
|
||||
fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT,
|
||||
};
|
||||
int result = SHFileOperation(ref op);
|
||||
if (result != 0)
|
||||
throw new System.ComponentModel.Win32Exception(result, $"SHFileOperation 실패 (코드 {result})");
|
||||
}
|
||||
|
||||
private void ShowLargeType()
|
||||
{
|
||||
// 클립보드 항목 → 시스템 클립보드에 자동 복사 + 외부 앱에서 열기
|
||||
if (_vm.SelectedItem?.Data is Services.ClipboardEntry clipEntry)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 자동 클립보드 복사 억제 (히스토리 중복 방지)
|
||||
CurrentApp?.ClipboardHistoryService?.SuppressNextCapture();
|
||||
|
||||
if (!clipEntry.IsText && clipEntry.Image != null)
|
||||
{
|
||||
// 원본 이미지가 있으면 원본 사용, 없으면 썸네일 사용
|
||||
var originalImg = Services.ClipboardHistoryService.LoadOriginalImage(clipEntry.OriginalImagePath);
|
||||
var imgToUse = originalImg ?? clipEntry.Image;
|
||||
|
||||
// 시스템 클립보드에 원본 복사
|
||||
Clipboard.SetImage(imgToUse);
|
||||
|
||||
// 이미지: PNG로 저장 → 기본 이미지 뷰어
|
||||
string path;
|
||||
if (!string.IsNullOrEmpty(clipEntry.OriginalImagePath) &&
|
||||
System.IO.File.Exists(clipEntry.OriginalImagePath))
|
||||
{
|
||||
path = clipEntry.OriginalImagePath; // 원본 파일 직접 열기
|
||||
}
|
||||
else
|
||||
{
|
||||
path = Services.TempFileService.CreateTempFile("clip_image", ".png");
|
||||
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(imgToUse));
|
||||
using var fs = new System.IO.FileStream(path, System.IO.FileMode.Create);
|
||||
encoder.Save(fs);
|
||||
}
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(clipEntry.Text))
|
||||
{
|
||||
// 시스템 클립보드에 텍스트 복사
|
||||
Clipboard.SetText(clipEntry.Text);
|
||||
|
||||
// 텍스트: txt로 저장 → 메모장
|
||||
var path = Services.TempFileService.CreateTempFile("clip_text", ".txt");
|
||||
System.IO.File.WriteAllText(path, clipEntry.Text, System.Text.Encoding.UTF8);
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("notepad.exe", $"\"{path}\"") { UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Warn($"클립보드 외부 뷰어 실패: {ex.Message}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var text = _vm.GetLargeTypeText();
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
new LargeTypeWindow(text).Show();
|
||||
}
|
||||
|
||||
/// <summary>이미 선택된 아이템을 클릭하면 Execute, 아직 선택되지 않은 아이템 클릭은 선택만.</summary>
|
||||
private SDK.LauncherItem? _lastClickedItem;
|
||||
private DateTime _lastClickTime;
|
||||
|
||||
private void ResultList_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 클릭한 ListViewItem 찾기
|
||||
var dep = e.OriginalSource as DependencyObject;
|
||||
while (dep != null && dep is not System.Windows.Controls.ListViewItem)
|
||||
dep = System.Windows.Media.VisualTreeHelper.GetParent(dep);
|
||||
if (dep is not System.Windows.Controls.ListViewItem lvi) return;
|
||||
|
||||
var clickedItem = lvi.Content as SDK.LauncherItem;
|
||||
if (clickedItem == null) return;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
|
||||
|
||||
if (_lastClickedItem == clickedItem && timeSinceLastClick < 600)
|
||||
{
|
||||
// 같은 아이템을 짧은 간격으로 재클릭 → 액션 모드 또는 실행
|
||||
if (!_vm.IsActionMode && _vm.CanEnterActionMode())
|
||||
{
|
||||
_vm.EnterActionMode(clickedItem);
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
e.Handled = true;
|
||||
}
|
||||
_lastClickedItem = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 첫 번째 클릭 → 선택만
|
||||
_lastClickedItem = clickedItem;
|
||||
_lastClickTime = now;
|
||||
}
|
||||
|
||||
private void ResultList_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
}
|
||||
|
||||
private void Window_Deactivated(object sender, EventArgs e)
|
||||
{
|
||||
// 설정 › 기능 › "포커스 잃으면 닫기"가 켜진 경우에만 자동 숨김
|
||||
if (_vm.CloseOnFocusLost) Hide();
|
||||
}
|
||||
|
||||
private void ScrollToSelected()
|
||||
{
|
||||
if (_vm.SelectedItem != null)
|
||||
ResultList.ScrollIntoView(_vm.SelectedItem);
|
||||
}
|
||||
|
||||
private void ShowNotification(string message)
|
||||
{
|
||||
// 시스템 트레이 토스트 알림 표시
|
||||
// App.xaml.cs의 TrayIcon을 통해 처리
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ internal sealed class ModelRegistrationDialog : Window
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uEA86",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 18,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
@@ -323,7 +323,7 @@ internal sealed class ModelRegistrationDialog : Window
|
||||
securityNote.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE72E",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -91,7 +91,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
var titleSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
titleSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE9D2", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE9D2", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
@@ -109,7 +109,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "\uE8BB", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE8BB", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12, Foreground = secondaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
@@ -222,7 +222,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 9, Foreground = fg,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0),
|
||||
});
|
||||
@@ -408,7 +408,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "\uE8FD", // Sort/Lines 아이콘 (드래그 핸들)
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 11,
|
||||
Foreground = dimBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -493,7 +493,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
{
|
||||
badge = new TextBlock
|
||||
{
|
||||
Text = "\uE73E", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE73E", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 13, Foreground = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81)),
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||||
Width = 20, TextAlignment = TextAlignment.Center,
|
||||
@@ -503,7 +503,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
{
|
||||
badge = new TextBlock
|
||||
{
|
||||
Text = "\uE768", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE768", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 13, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||||
Width = 20, TextAlignment = TextAlignment.Center,
|
||||
@@ -563,7 +563,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = isExpanded ? "\uE70E" : "\uE70D", // ChevronUp / ChevronDown
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(Color.FromArgb(0x70, 0x80, 0x80, 0x80)),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -653,7 +653,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
var addSp = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
addSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE710", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = "\uE710", FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12, Foreground = st2,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
@@ -884,7 +884,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
Margin = new Thickness(1, 0, 1, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 10, Foreground = fg,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
@@ -914,7 +914,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12, Foreground = filled ? Brushes.White : textColor,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
|
||||
@@ -248,7 +248,7 @@ public partial class PreviewWindow : Window
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "\uE711",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 8,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -60,7 +60,7 @@ internal sealed class PromptTemplateDialog : Window
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8A5",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 18,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
1202
src/AxCopilot/Views/SettingsWindow.AgentConfig.cs
Normal file
1202
src/AxCopilot/Views/SettingsWindow.AgentConfig.cs
Normal file
File diff suppressed because it is too large
Load Diff
875
src/AxCopilot/Views/SettingsWindow.Tools.cs
Normal file
875
src/AxCopilot/Views/SettingsWindow.Tools.cs
Normal file
@@ -0,0 +1,875 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.ViewModels;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class SettingsWindow
|
||||
{
|
||||
// ─── 도구/커넥터 관리 UI (기타 탭) ─────────────────────────────────────────────
|
||||
|
||||
private void BuildToolRegistryPanel()
|
||||
{
|
||||
if (AgentEtcContent == null) return;
|
||||
|
||||
// 도구 목록 데이터 (카테고리별)
|
||||
var toolGroups = new (string Category, string Icon, string IconColor, (string Name, string Desc)[] Tools)[]
|
||||
{
|
||||
("파일/검색", "\uE8B7", "#F59E0B", new[]
|
||||
{
|
||||
("file_read", "파일 읽기 — 텍스트/바이너리 파일의 내용을 읽습니다"),
|
||||
("file_write", "파일 쓰기 — 새 파일을 생성하거나 기존 파일을 덮어씁니다"),
|
||||
("file_edit", "파일 편집 — 줄 번호 기반으로 파일의 특정 부분을 수정합니다"),
|
||||
("glob", "파일 패턴 검색 — 글로브 패턴으로 파일을 찾습니다 (예: **/*.cs)"),
|
||||
("grep_tool", "텍스트 검색 — 정규식으로 파일 내용을 검색합니다"),
|
||||
("folder_map", "폴더 구조 — 프로젝트의 디렉토리 트리를 조회합니다"),
|
||||
("document_read", "문서 읽기 — PDF, DOCX 등 문서 파일의 텍스트를 추출합니다"),
|
||||
}),
|
||||
("프로세스/빌드", "\uE756", "#06B6D4", new[]
|
||||
{
|
||||
("process", "프로세스 실행 — 외부 명령/프로그램을 실행합니다"),
|
||||
("build_run", "빌드/테스트 — 프로젝트 타입을 감지하여 빌드/테스트를 실행합니다"),
|
||||
("dev_env_detect", "개발 환경 감지 — IDE, 런타임, 빌드 도구를 자동으로 인식합니다"),
|
||||
}),
|
||||
("코드 분석", "\uE943", "#818CF8", new[]
|
||||
{
|
||||
("search_codebase", "코드 키워드 검색 — TF-IDF 기반으로 관련 코드를 찾습니다"),
|
||||
("code_review", "AI 코드 리뷰 — Git diff 분석, 파일 정적 검사, PR 요약"),
|
||||
("lsp", "LSP 인텔리전스 — 정의 이동, 참조 검색, 심볼 목록 (C#/TS/Python)"),
|
||||
("test_loop", "테스트 루프 — 테스트 생성/실행/분석 자동화"),
|
||||
("git_tool", "Git 작업 — status, log, diff, commit 등 Git 명령 실행"),
|
||||
("snippet_runner", "코드 실행 — C#/Python/JavaScript 스니펫 즉시 실행"),
|
||||
}),
|
||||
("문서 생성", "\uE8A5", "#34D399", new[]
|
||||
{
|
||||
("excel_create", "Excel 생성 — .xlsx 스프레드시트를 생성합니다"),
|
||||
("docx_create", "Word 생성 — .docx 문서를 생성합니다"),
|
||||
("csv_create", "CSV 생성 — CSV 파일을 생성합니다"),
|
||||
("markdown_create", "마크다운 생성 — .md 파일을 생성합니다"),
|
||||
("html_create", "HTML 생성 — HTML 파일을 생성합니다"),
|
||||
("chart_create", "차트 생성 — 데이터 시각화 차트를 생성합니다"),
|
||||
("batch_create", "배치 생성 — 스크립트 파일을 생성합니다"),
|
||||
("document_review", "문서 검증 — 문서 품질을 검사합니다"),
|
||||
("format_convert", "포맷 변환 — 문서 형식을 변환합니다"),
|
||||
}),
|
||||
("데이터 처리", "\uE9F5", "#F59E0B", new[]
|
||||
{
|
||||
("json_tool", "JSON 처리 — JSON 파싱, 변환, 검증, 포맷팅"),
|
||||
("regex_tool", "정규식 — 정규식 테스트, 추출, 치환"),
|
||||
("diff_tool", "텍스트 비교 — 두 파일/텍스트 비교, 통합 diff 출력"),
|
||||
("base64_tool", "인코딩 — Base64/URL 인코딩, 디코딩"),
|
||||
("hash_tool", "해시 계산 — MD5/SHA256 해시 계산 (파일/텍스트)"),
|
||||
("datetime_tool", "날짜/시간 — 날짜 변환, 타임존, 기간 계산"),
|
||||
}),
|
||||
("시스템/환경", "\uE770", "#06B6D4", new[]
|
||||
{
|
||||
("clipboard_tool", "클립보드 — Windows 클립보드 읽기/쓰기 (텍스트/이미지)"),
|
||||
("notify_tool", "알림 — Windows 시스템 알림 전송"),
|
||||
("env_tool", "환경변수 — 환경변수 읽기/설정 (프로세스 범위)"),
|
||||
("zip_tool", "압축 — 파일 압축(zip) / 해제"),
|
||||
("http_tool", "HTTP — 로컬/사내 HTTP API 호출 (GET/POST)"),
|
||||
("sql_tool", "SQLite — SQLite DB 쿼리 실행 (로컬 파일)"),
|
||||
}),
|
||||
("에이전트", "\uE99A", "#F472B6", new[]
|
||||
{
|
||||
("spawn_agent", "서브에이전트 — 하위 작업을 병렬로 실행하는 서브에이전트를 생성합니다"),
|
||||
("wait_agents", "에이전트 대기 — 실행 중인 서브에이전트의 결과를 수집합니다"),
|
||||
("memory", "에이전트 메모리 — 프로젝트 규칙, 선호도를 저장/검색합니다"),
|
||||
("skill_manager", "스킬 관리 — 스킬 목록 조회, 상세 정보, 재로드"),
|
||||
("project_rules", "프로젝트 지침 — AX.md 개발 지침을 읽고 씁니다"),
|
||||
}),
|
||||
};
|
||||
|
||||
// 도구 목록을 섹션으로 그룹화
|
||||
var toolCards = new List<UIElement>();
|
||||
|
||||
// 설명
|
||||
// 도구 헤더
|
||||
toolCards.Add(new TextBlock
|
||||
{
|
||||
Text = $"등록된 도구/커넥터 ({toolGroups.Sum(g => g.Tools.Length)})",
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
Margin = new Thickness(2, 4, 0, 4),
|
||||
});
|
||||
toolCards.Add(new TextBlock
|
||||
{
|
||||
Text = "에이전트가 사용할 수 있는 도구 목록입니다. 코워크/코드 탭에서 LLM이 자동으로 호출합니다.",
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#9999BB"),
|
||||
Margin = new Thickness(2, 0, 0, 10),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
|
||||
foreach (var group in toolGroups)
|
||||
{
|
||||
// 카테고리 카드
|
||||
var card = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 10, 14, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
|
||||
var cardPanel = new StackPanel();
|
||||
|
||||
// 도구 아이템 패널 (접기/열기 대상)
|
||||
var toolItemsPanel = new StackPanel { Visibility = Visibility.Collapsed };
|
||||
|
||||
// 접기/열기 화살표
|
||||
var arrow = new TextBlock
|
||||
{
|
||||
Text = "\uE70D",
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||
RenderTransform = new RotateTransform(0),
|
||||
};
|
||||
|
||||
// 카테고리 헤더 (클릭 가능)
|
||||
var catHeader = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var catHeaderPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
catHeaderPanel.Children.Add(arrow);
|
||||
catHeaderPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = group.Icon,
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
Foreground = ThemeResourceHelper.HexBrush(group.IconColor),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
var catTitle = new TextBlock
|
||||
{
|
||||
Text = $"{group.Category} ({group.Tools.Length})",
|
||||
FontSize = 12.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#1A1B2E"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
catHeaderPanel.Children.Add(catTitle);
|
||||
catHeader.Child = catHeaderPanel;
|
||||
|
||||
// 클릭 시 접기/열기 토글
|
||||
catHeader.MouseLeftButtonDown += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
bool isVisible = toolItemsPanel.Visibility == Visibility.Visible;
|
||||
toolItemsPanel.Visibility = isVisible ? Visibility.Collapsed : Visibility.Visible;
|
||||
arrow.RenderTransform = new RotateTransform(isVisible ? 0 : 90);
|
||||
};
|
||||
|
||||
cardPanel.Children.Add(catHeader);
|
||||
|
||||
// 구분선
|
||||
toolItemsPanel.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = TryFindResource("BorderColor") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#F0F0F4"),
|
||||
Margin = new Thickness(0, 6, 0, 4),
|
||||
});
|
||||
|
||||
// 도구 아이템
|
||||
foreach (var (name, toolDesc) in group.Tools)
|
||||
{
|
||||
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
|
||||
var nameBlock = new TextBlock
|
||||
{
|
||||
Text = name,
|
||||
FontSize = 12,
|
||||
FontFamily = ThemeResourceHelper.ConsolasCode,
|
||||
Foreground = TryFindResource("AccentColor") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#4B5EFC"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
MinWidth = 130,
|
||||
Margin = new Thickness(4, 0, 12, 0),
|
||||
};
|
||||
Grid.SetColumn(nameBlock, 0);
|
||||
row.Children.Add(nameBlock);
|
||||
|
||||
var descBlock = new TextBlock
|
||||
{
|
||||
Text = toolDesc,
|
||||
FontSize = 11.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#6666AA"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
Grid.SetColumn(descBlock, 1);
|
||||
row.Children.Add(descBlock);
|
||||
|
||||
toolItemsPanel.Children.Add(row);
|
||||
}
|
||||
|
||||
cardPanel.Children.Add(toolItemsPanel);
|
||||
card.Child = cardPanel;
|
||||
toolCards.Add(card);
|
||||
}
|
||||
|
||||
// ── 도구 목록을 직접 추가 (카테고리별 접기/열기) ──
|
||||
foreach (var card in toolCards)
|
||||
AgentEtcContent.Children.Insert(AgentEtcContent.Children.Count, card);
|
||||
|
||||
int insertIdx = AgentEtcContent.Children.Count;
|
||||
|
||||
// ── 로드된 스킬 목록 ──
|
||||
BuildSkillListSection(ref insertIdx);
|
||||
}
|
||||
|
||||
private void BuildSkillListSection(ref int insertIdx)
|
||||
{
|
||||
if (AgentEtcContent == null) return;
|
||||
|
||||
var skills = Services.Agent.SkillService.Skills;
|
||||
if (skills.Count == 0) return;
|
||||
|
||||
var accentColor = ThemeResourceHelper.HexColor("#4B5EFC");
|
||||
var subtleText = ThemeResourceHelper.HexColor("#6666AA");
|
||||
|
||||
// 스킬 콘텐츠를 모아서 접기/열기 섹션에 넣기
|
||||
var skillItems = new List<UIElement>();
|
||||
|
||||
// 설명
|
||||
skillItems.Add(new TextBlock
|
||||
{
|
||||
Text = "/ 명령으로 호출할 수 있는 스킬 목록입니다. 앱 내장 + 사용자 추가 스킬이 포함됩니다.\n" +
|
||||
"(스킬은 사용자가 직접 /명령어를 입력해야 실행됩니다. LLM이 자동 호출하지 않습니다.)",
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(subtleText),
|
||||
Margin = new Thickness(2, 0, 0, 10),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
|
||||
// 내장 스킬 / 고급 스킬 분류
|
||||
var builtIn = skills.Where(s => string.IsNullOrEmpty(s.Requires)).ToList();
|
||||
var advanced = skills.Where(s => !string.IsNullOrEmpty(s.Requires)).ToList();
|
||||
|
||||
// 내장 스킬 카드
|
||||
if (builtIn.Count > 0)
|
||||
{
|
||||
var card = CreateSkillGroupCard("내장 스킬", "\uE768", "#34D399", builtIn);
|
||||
skillItems.Add(card);
|
||||
}
|
||||
|
||||
// 고급 스킬 (런타임 의존) 카드
|
||||
if (advanced.Count > 0)
|
||||
{
|
||||
var card = CreateSkillGroupCard("고급 스킬 (런타임 필요)", "\uE9D9", "#A78BFA", advanced);
|
||||
skillItems.Add(card);
|
||||
}
|
||||
|
||||
// ── 스킬 가져오기/내보내기 버튼 ──
|
||||
var btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(2, 4, 0, 10),
|
||||
};
|
||||
|
||||
// 가져오기 버튼
|
||||
var importBtn = new Border
|
||||
{
|
||||
Background = ThemeResourceHelper.HexBrush("#4B5EFC"),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 7, 14, 7),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var importContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
importContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8B5",
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12,
|
||||
Foreground = Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
importContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "스킬 가져오기 (.zip)",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
importBtn.Child = importContent;
|
||||
importBtn.MouseLeftButtonUp += SkillImport_Click;
|
||||
importBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
importBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
btnPanel.Children.Add(importBtn);
|
||||
|
||||
// 내보내기 버튼
|
||||
var exportBtn = new Border
|
||||
{
|
||||
Background = ThemeResourceHelper.HexBrush("#F0F1F5"),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 7, 14, 7),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var exportContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
exportContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uEDE1",
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#555"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
exportContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "스킬 내보내기",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#444"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
exportBtn.Child = exportContent;
|
||||
exportBtn.MouseLeftButtonUp += SkillExport_Click;
|
||||
exportBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
exportBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
btnPanel.Children.Add(exportBtn);
|
||||
|
||||
skillItems.Add(btnPanel);
|
||||
|
||||
// ── 갤러리/통계 링크 버튼 ──
|
||||
var linkPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(2, 0, 0, 12),
|
||||
};
|
||||
|
||||
var galleryBtn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 7, 14, 7),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x18, 0x4B, 0x5E, 0xFC)),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var galleryContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
galleryContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE768",
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#4B5EFC"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
galleryContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "스킬 갤러리 열기",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#4B5EFC"),
|
||||
});
|
||||
galleryBtn.Child = galleryContent;
|
||||
galleryBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
var win = new SkillGalleryWindow();
|
||||
win.Owner = Window.GetWindow(this);
|
||||
win.ShowDialog();
|
||||
};
|
||||
galleryBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8;
|
||||
galleryBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
linkPanel.Children.Add(galleryBtn);
|
||||
|
||||
var statsBtn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 7, 14, 7),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x18, 0xA7, 0x8B, 0xFA)),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var statsContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
statsContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE9D9",
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#A78BFA"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
statsContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "실행 통계 보기",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#A78BFA"),
|
||||
});
|
||||
statsBtn.Child = statsContent;
|
||||
statsBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
var win = new AgentStatsDashboardWindow();
|
||||
win.Owner = Window.GetWindow(this);
|
||||
win.ShowDialog();
|
||||
};
|
||||
statsBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8;
|
||||
statsBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
linkPanel.Children.Add(statsBtn);
|
||||
|
||||
skillItems.Add(linkPanel);
|
||||
|
||||
// ── 스킬 섹션 직접 추가 ──
|
||||
// 스킬 헤더
|
||||
var skillHeader = new TextBlock
|
||||
{
|
||||
Text = $"슬래시 스킬 ({skills.Count})",
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
Margin = new Thickness(2, 16, 0, 8),
|
||||
};
|
||||
AgentEtcContent.Children.Insert(insertIdx++, skillHeader);
|
||||
foreach (var item in skillItems)
|
||||
AgentEtcContent.Children.Insert(insertIdx++, item);
|
||||
}
|
||||
|
||||
private Border CreateSkillGroupCard(string title, string icon, string colorHex,
|
||||
List<Services.Agent.SkillDefinition> skills)
|
||||
{
|
||||
var color = ThemeResourceHelper.HexColor(colorHex);
|
||||
var card = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 10, 14, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
|
||||
var panel = new StackPanel();
|
||||
|
||||
// 스킬 아이템 패널 (접기/열기 대상)
|
||||
var skillItemsPanel = new StackPanel { Visibility = Visibility.Collapsed };
|
||||
|
||||
// 접기/열기 화살표
|
||||
var arrow = new TextBlock
|
||||
{
|
||||
Text = "\uE70D",
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||
RenderTransform = new RotateTransform(0),
|
||||
};
|
||||
|
||||
// 카테고리 헤더 (클릭 가능)
|
||||
var catHeader = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var catHeaderPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
catHeaderPanel.Children.Add(arrow);
|
||||
catHeaderPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
Foreground = new SolidColorBrush(color),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
catHeaderPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{title} ({skills.Count})",
|
||||
FontSize = 12.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#1A1B2E"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
catHeader.Child = catHeaderPanel;
|
||||
|
||||
// 클릭 시 접기/열기 토글
|
||||
catHeader.MouseLeftButtonDown += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
bool isVisible = skillItemsPanel.Visibility == Visibility.Visible;
|
||||
skillItemsPanel.Visibility = isVisible ? Visibility.Collapsed : Visibility.Visible;
|
||||
arrow.RenderTransform = new RotateTransform(isVisible ? 0 : 90);
|
||||
};
|
||||
|
||||
panel.Children.Add(catHeader);
|
||||
|
||||
// 구분선
|
||||
skillItemsPanel.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = TryFindResource("BorderColor") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#F0F0F4"),
|
||||
Margin = new Thickness(0, 2, 0, 4),
|
||||
});
|
||||
|
||||
// 스킬 아이템
|
||||
foreach (var skill in skills)
|
||||
{
|
||||
var row = new StackPanel { Margin = new Thickness(0, 4, 0, 4) };
|
||||
|
||||
// 위 줄: 스킬 명칭 + 가용성 뱃지
|
||||
var namePanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
namePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"/{skill.Name}",
|
||||
FontSize = 12,
|
||||
FontFamily = ThemeResourceHelper.ConsolasCode,
|
||||
Foreground = skill.IsAvailable
|
||||
? (TryFindResource("AccentColor") as Brush ?? ThemeResourceHelper.HexBrush("#4B5EFC"))
|
||||
: Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(4, 0, 8, 0),
|
||||
Opacity = skill.IsAvailable ? 1.0 : 0.5,
|
||||
});
|
||||
|
||||
if (!skill.IsAvailable && !string.IsNullOrEmpty(skill.Requires))
|
||||
{
|
||||
namePanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x20, 0xF8, 0x71, 0x71)),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(5, 1, 5, 1),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = skill.UnavailableHint,
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xF8, 0x71, 0x71)),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
},
|
||||
});
|
||||
}
|
||||
else if (skill.IsAvailable && !string.IsNullOrEmpty(skill.Requires))
|
||||
{
|
||||
namePanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x20, 0x34, 0xD3, 0x99)),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(5, 1, 5, 1),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "✓ 사용 가능",
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xD3, 0x99)),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
row.Children.Add(namePanel);
|
||||
|
||||
// 아래 줄: 설명 (뱃지와 구분되도록 위 여백 추가)
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{skill.Label} — {skill.Description}",
|
||||
FontSize = 11.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#6666AA"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(4, 3, 0, 0),
|
||||
Opacity = skill.IsAvailable ? 1.0 : 0.5,
|
||||
});
|
||||
|
||||
skillItemsPanel.Children.Add(row);
|
||||
}
|
||||
|
||||
panel.Children.Add(skillItemsPanel);
|
||||
card.Child = panel;
|
||||
return card;
|
||||
}
|
||||
|
||||
// ─── AX Agent 서브탭 전환 ───────────────────────────────────────────
|
||||
|
||||
private void AgentSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (AgentPanelCommon == null) return; // 초기화 전 방어
|
||||
AgentPanelCommon.Visibility = AgentTabCommon.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
AgentPanelChat.Visibility = AgentTabChat.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
AgentPanelCoworkCode.Visibility = AgentTabCoworkCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
AgentPanelCowork.Visibility = AgentTabCowork.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
AgentPanelCode.Visibility = AgentTabCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (AgentPanelDev != null)
|
||||
AgentPanelDev.Visibility = AgentTabDev.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (AgentPanelEtc != null)
|
||||
AgentPanelEtc.Visibility = AgentTabEtc.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (AgentPanelTools != null)
|
||||
{
|
||||
var show = AgentTabTools.IsChecked == true;
|
||||
AgentPanelTools.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (show) LoadToolCards();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 도구 관리 카드 UI ──────────────────────────────────────────────
|
||||
|
||||
private bool _toolCardsLoaded;
|
||||
private HashSet<string> _disabledTools = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>도구 카드 UI를 카테고리별로 생성합니다.</summary>
|
||||
private void LoadToolCards()
|
||||
{
|
||||
if (_toolCardsLoaded || ToolCardsPanel == null) return;
|
||||
_toolCardsLoaded = true;
|
||||
|
||||
var app = CurrentApp;
|
||||
var settings = app?.SettingsService?.Settings.Llm;
|
||||
using var tools = Services.Agent.ToolRegistry.CreateDefault();
|
||||
_disabledTools = new HashSet<string>(settings?.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase);
|
||||
var disabled = _disabledTools;
|
||||
|
||||
// 카테고리 매핑
|
||||
var categories = new Dictionary<string, List<Services.Agent.IAgentTool>>
|
||||
{
|
||||
["파일/검색"] = new(),
|
||||
["문서 생성"] = new(),
|
||||
["문서 품질"] = new(),
|
||||
["코드/개발"] = new(),
|
||||
["데이터/유틸"] = new(),
|
||||
["시스템"] = new(),
|
||||
};
|
||||
|
||||
var toolCategoryMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// 파일/검색
|
||||
["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색",
|
||||
["glob"] = "파일/검색", ["grep"] = "파일/검색", ["folder_map"] = "파일/검색",
|
||||
["document_read"] = "파일/검색", ["file_watch"] = "파일/검색",
|
||||
// 문서 생성
|
||||
["excel_skill"] = "문서 생성", ["docx_skill"] = "문서 생성", ["csv_skill"] = "문서 생성",
|
||||
["markdown_skill"] = "문서 생성", ["html_skill"] = "문서 생성", ["chart_skill"] = "문서 생성",
|
||||
["batch_skill"] = "문서 생성", ["pptx_skill"] = "문서 생성",
|
||||
["document_planner"] = "문서 생성", ["document_assembler"] = "문서 생성",
|
||||
// 문서 품질
|
||||
["document_review"] = "문서 품질", ["format_convert"] = "문서 품질",
|
||||
["template_render"] = "문서 품질", ["text_summarize"] = "문서 품질",
|
||||
// 코드/개발
|
||||
["dev_env_detect"] = "코드/개발", ["build_run"] = "코드/개발", ["git_tool"] = "코드/개발",
|
||||
["lsp"] = "코드/개발", ["sub_agent"] = "코드/개발", ["wait_agents"] = "코드/개발",
|
||||
["code_search"] = "코드/개발", ["test_loop"] = "코드/개발",
|
||||
["code_review"] = "코드/개발", ["project_rule"] = "코드/개발",
|
||||
// 시스템
|
||||
["process"] = "시스템", ["skill_manager"] = "시스템", ["memory"] = "시스템",
|
||||
["clipboard"] = "시스템", ["notify"] = "시스템", ["env"] = "시스템",
|
||||
["image_analyze"] = "시스템",
|
||||
};
|
||||
|
||||
foreach (var tool in tools.All)
|
||||
{
|
||||
var cat = toolCategoryMap.TryGetValue(tool.Name, out var c) ? c : "데이터/유틸";
|
||||
if (categories.ContainsKey(cat))
|
||||
categories[cat].Add(tool);
|
||||
else
|
||||
categories["데이터/유틸"].Add(tool);
|
||||
}
|
||||
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xF5, 0xF5, 0xF8));
|
||||
|
||||
foreach (var kv in categories)
|
||||
{
|
||||
if (kv.Value.Count == 0) continue;
|
||||
|
||||
// 카테고리 헤더
|
||||
var header = new TextBlock
|
||||
{
|
||||
Text = $"{kv.Key} ({kv.Value.Count})",
|
||||
FontSize = 13, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
Margin = new Thickness(0, 8, 0, 6),
|
||||
};
|
||||
ToolCardsPanel.Children.Add(header);
|
||||
|
||||
// 카드 WrapPanel
|
||||
var wrap = new WrapPanel { Margin = new Thickness(0, 0, 0, 4) };
|
||||
foreach (var tool in kv.Value.OrderBy(t => t.Name))
|
||||
{
|
||||
var isEnabled = !disabled.Contains(tool.Name);
|
||||
var card = CreateToolCard(tool, isEnabled, disabled, accentBrush, secondaryText, itemBg);
|
||||
wrap.Children.Add(card);
|
||||
}
|
||||
ToolCardsPanel.Children.Add(wrap);
|
||||
}
|
||||
|
||||
// MCP 서버 상태
|
||||
LoadMcpStatus();
|
||||
}
|
||||
|
||||
/// <summary>개별 도구 카드를 생성합니다 (이름 + 설명 + 토글).</summary>
|
||||
private Border CreateToolCard(Services.Agent.IAgentTool tool, bool isEnabled,
|
||||
HashSet<string> disabled, Brush accentBrush, Brush secondaryText, Brush itemBg)
|
||||
{
|
||||
var card = new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Margin = new Thickness(0, 0, 8, 8),
|
||||
Width = 240,
|
||||
BorderBrush = isEnabled ? Brushes.Transparent : new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26)),
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
// 이름 + 설명
|
||||
var textStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
var nameBlock = new TextBlock
|
||||
{
|
||||
Text = tool.Name,
|
||||
FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
};
|
||||
textStack.Children.Add(nameBlock);
|
||||
|
||||
var desc = tool.Description;
|
||||
if (desc.Length > 50) desc = desc[..50] + "…";
|
||||
var descBlock = new TextBlock
|
||||
{
|
||||
Text = desc,
|
||||
FontSize = 10.5,
|
||||
Foreground = secondaryText,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
};
|
||||
textStack.Children.Add(descBlock);
|
||||
Grid.SetColumn(textStack, 0);
|
||||
grid.Children.Add(textStack);
|
||||
|
||||
// 토글 (CheckBox + ToggleSwitch 스타일)
|
||||
var toggle = new CheckBox
|
||||
{
|
||||
IsChecked = isEnabled,
|
||||
Style = TryFindResource("ToggleSwitch") as Style,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
};
|
||||
toggle.Checked += (_, _) =>
|
||||
{
|
||||
disabled.Remove(tool.Name);
|
||||
card.BorderBrush = Brushes.Transparent;
|
||||
};
|
||||
toggle.Unchecked += (_, _) =>
|
||||
{
|
||||
disabled.Add(tool.Name);
|
||||
card.BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26));
|
||||
};
|
||||
Grid.SetColumn(toggle, 1);
|
||||
grid.Children.Add(toggle);
|
||||
|
||||
card.Child = grid;
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>MCP 서버 연결 상태 표시를 생성합니다.</summary>
|
||||
private void LoadMcpStatus()
|
||||
{
|
||||
if (McpStatusPanel == null) return;
|
||||
McpStatusPanel.Children.Clear();
|
||||
|
||||
var app = CurrentApp;
|
||||
var settings = app?.SettingsService?.Settings.Llm;
|
||||
var mcpServers = settings?.McpServers;
|
||||
|
||||
if (mcpServers == null || mcpServers.Count == 0)
|
||||
{
|
||||
McpStatusPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "등록된 MCP 서버가 없습니다.",
|
||||
FontSize = 12,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xF5, 0xF5, 0xF8));
|
||||
|
||||
foreach (var server in mcpServers)
|
||||
{
|
||||
var row = new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(12, 8, 12, 8),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
// 상태 아이콘
|
||||
var statusDot = new Border
|
||||
{
|
||||
Width = 8, Height = 8,
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Background = new SolidColorBrush(Color.FromRgb(0x34, 0xA8, 0x53)), // 녹색 (등록됨)
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
Grid.SetColumn(statusDot, 0);
|
||||
grid.Children.Add(statusDot);
|
||||
|
||||
// 서버 이름 + 명령
|
||||
var infoStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
infoStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = server.Name ?? "(이름 없음)",
|
||||
FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
});
|
||||
infoStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = server.Command ?? "",
|
||||
FontSize = 10.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
Grid.SetColumn(infoStack, 1);
|
||||
grid.Children.Add(infoStack);
|
||||
|
||||
// 상태 텍스트
|
||||
var statusText = new TextBlock
|
||||
{
|
||||
Text = "등록됨",
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xA8, 0x53)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(statusText, 2);
|
||||
grid.Children.Add(statusText);
|
||||
|
||||
row.Child = grid;
|
||||
McpStatusPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
802
src/AxCopilot/Views/SettingsWindow.UI.cs
Normal file
802
src/AxCopilot/Views/SettingsWindow.UI.cs
Normal file
@@ -0,0 +1,802 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.ViewModels;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class SettingsWindow
|
||||
{
|
||||
// ─── 접기/열기 가능 섹션 헬퍼 ──────────────────────────────────────────
|
||||
private Border CreateCollapsibleSection(string title, UIElement content, bool expanded = true)
|
||||
{
|
||||
var headerColor = ThemeResourceHelper.HexBrush("#AAAACC");
|
||||
var section = new StackPanel();
|
||||
var contentPanel = new StackPanel { Visibility = expanded ? Visibility.Visible : Visibility.Collapsed };
|
||||
if (content is Panel panel)
|
||||
{
|
||||
// 패널의 자식들을 contentPanel로 옮김 (StackPanel 등)
|
||||
contentPanel.Children.Add(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentPanel.Children.Add(content);
|
||||
}
|
||||
|
||||
var arrow = new TextBlock
|
||||
{
|
||||
Text = "\uE70D",
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 9,
|
||||
Foreground = headerColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||
RenderTransform = new RotateTransform(expanded ? 90 : 0),
|
||||
};
|
||||
var titleBlock = new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 10.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = headerColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
headerPanel.Children.Add(arrow);
|
||||
headerPanel.Children.Add(titleBlock);
|
||||
|
||||
var header = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(2, 10, 2, 6),
|
||||
Child = headerPanel,
|
||||
};
|
||||
header.MouseLeftButtonUp += (s, e) =>
|
||||
{
|
||||
bool isNowVisible = contentPanel.Visibility == Visibility.Visible;
|
||||
contentPanel.Visibility = isNowVisible ? Visibility.Collapsed : Visibility.Visible;
|
||||
arrow.RenderTransform = new RotateTransform(isNowVisible ? 0 : 90);
|
||||
};
|
||||
header.MouseEnter += (s, _) => titleBlock.Foreground = ThemeResourceHelper.HexBrush("#CCCCEE");
|
||||
header.MouseLeave += (s, _) => titleBlock.Foreground = headerColor;
|
||||
|
||||
section.Children.Add(header);
|
||||
section.Children.Add(contentPanel);
|
||||
|
||||
return new Border
|
||||
{
|
||||
Child = section,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
};
|
||||
}
|
||||
|
||||
private Border CreateCollapsibleSection(string title, IEnumerable<UIElement> children, bool expanded = true)
|
||||
{
|
||||
var contentPanel = new StackPanel();
|
||||
foreach (var child in children)
|
||||
contentPanel.Children.Add(child);
|
||||
return CreateCollapsibleSection(title, contentPanel, expanded);
|
||||
}
|
||||
|
||||
// ─── 선택 텍스트 AI 명령 설정 ────────────────────────────────────────────────
|
||||
|
||||
private void BuildTextActionCommandsPanel()
|
||||
{
|
||||
if (TextActionCommandsPanel == null) return;
|
||||
TextActionCommandsPanel.Children.Clear();
|
||||
|
||||
var svc = CurrentApp?.SettingsService;
|
||||
if (svc == null) return;
|
||||
var enabled = svc.Settings.Launcher.TextActionCommands;
|
||||
var toggleStyle = TryFindResource("ToggleSwitch") as Style;
|
||||
|
||||
foreach (var (key, label) in TextActionPopup.AvailableCommands)
|
||||
{
|
||||
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var labelBlock = new TextBlock
|
||||
{
|
||||
Text = label, FontSize = 12.5,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
};
|
||||
Grid.SetColumn(labelBlock, 0);
|
||||
row.Children.Add(labelBlock);
|
||||
|
||||
var capturedKey = key;
|
||||
var cb = new CheckBox
|
||||
{
|
||||
IsChecked = enabled.Contains(key, StringComparer.OrdinalIgnoreCase),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
if (toggleStyle != null) cb.Style = toggleStyle;
|
||||
cb.Checked += (_, _) =>
|
||||
{
|
||||
if (!enabled.Contains(capturedKey)) enabled.Add(capturedKey);
|
||||
svc.Save();
|
||||
};
|
||||
cb.Unchecked += (_, _) =>
|
||||
{
|
||||
// 최소 1개 유지
|
||||
if (enabled.Count <= 1) { cb.IsChecked = true; return; }
|
||||
enabled.RemoveAll(x => x == capturedKey);
|
||||
svc.Save();
|
||||
};
|
||||
Grid.SetColumn(cb, 1);
|
||||
row.Children.Add(cb);
|
||||
|
||||
TextActionCommandsPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 테마 하위 탭 전환 ──────────────────────────────────────────────────────
|
||||
|
||||
private void ThemeSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ThemeSelectPanel == null || ThemeColorsPanel == null) return;
|
||||
if (ThemeSubTabSelect?.IsChecked == true)
|
||||
{
|
||||
ThemeSelectPanel.Visibility = Visibility.Visible;
|
||||
ThemeColorsPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThemeSelectPanel.Visibility = Visibility.Collapsed;
|
||||
ThemeColorsPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 일반 하위 탭 전환 (일반 + 알림) ──────────────────────────────────────
|
||||
|
||||
private bool _notifyMoved;
|
||||
|
||||
private void GeneralSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (GeneralMainPanel == null || GeneralNotifyPanel == null) return;
|
||||
|
||||
// 알림 탭 내용을 최초 1회 NotifyContent로 이동
|
||||
if (!_notifyMoved && NotifyContent != null)
|
||||
{
|
||||
MoveNotifyTabContent();
|
||||
_notifyMoved = true;
|
||||
}
|
||||
|
||||
GeneralMainPanel.Visibility = GeneralSubTabMain?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
GeneralNotifyPanel.Visibility = GeneralSubTabNotify?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (GeneralStoragePanel != null)
|
||||
{
|
||||
GeneralStoragePanel.Visibility = GeneralSubTabStorage?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (GeneralSubTabStorage?.IsChecked == true) RefreshStorageInfo2();
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveNotifyTabContent()
|
||||
{
|
||||
// XAML의 알림 TabItem에서 ScrollViewer → StackPanel 내용을 NotifyContent로 이동
|
||||
// 사이드 네비의 "알림" 탭을 찾아서 숨기기
|
||||
var mainTab = Content as System.Windows.Controls.Grid;
|
||||
var tabControl = mainTab?.Children.OfType<TabControl>().FirstOrDefault()
|
||||
?? FindVisualChild<TabControl>(this);
|
||||
if (tabControl == null) return;
|
||||
|
||||
TabItem? notifyTab = null;
|
||||
foreach (TabItem tab in tabControl.Items)
|
||||
{
|
||||
if (tab.Header?.ToString() == "알림") { notifyTab = tab; break; }
|
||||
}
|
||||
|
||||
if (notifyTab?.Content is ScrollViewer sv && sv.Content is StackPanel sp)
|
||||
{
|
||||
// 내용물을 NotifyContent로 복제 이동
|
||||
var children = sp.Children.Cast<UIElement>().ToList();
|
||||
sp.Children.Clear();
|
||||
foreach (var child in children)
|
||||
NotifyContent.Children.Add(child);
|
||||
}
|
||||
|
||||
// 알림 탭 숨기기
|
||||
if (notifyTab != null) notifyTab.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private static T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is T t) return t;
|
||||
var result = FindVisualChild<T>(child);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── 독 바 설정 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildDockBarSettings()
|
||||
{
|
||||
var svc = CurrentApp?.SettingsService;
|
||||
if (svc == null) return;
|
||||
var launcher = svc.Settings.Launcher;
|
||||
|
||||
// 토글 바인딩
|
||||
ChkDockAutoShow.IsChecked = launcher.DockBarAutoShow;
|
||||
ChkDockAutoShow.Checked += (_, _) =>
|
||||
{
|
||||
launcher.DockBarAutoShow = true; svc.Save();
|
||||
CurrentApp?.ToggleDockBar();
|
||||
};
|
||||
ChkDockAutoShow.Unchecked += (_, _) =>
|
||||
{
|
||||
launcher.DockBarAutoShow = false; svc.Save();
|
||||
// 끄기 시에는 설정만 저장 — 독 바를 토글하지 않음 (이미 표시 중이면 유지, 다음 재시작 시 안 뜸)
|
||||
};
|
||||
|
||||
ChkDockRainbowGlow.IsChecked = launcher.DockBarRainbowGlow;
|
||||
ChkDockRainbowGlow.Checked += (_, _) => { launcher.DockBarRainbowGlow = true; svc.Save(); RefreshDock(); };
|
||||
ChkDockRainbowGlow.Unchecked += (_, _) => { launcher.DockBarRainbowGlow = false; svc.Save(); RefreshDock(); };
|
||||
|
||||
SliderDockOpacity.Value = launcher.DockBarOpacity;
|
||||
SliderDockOpacity.ValueChanged += (_, e) => { launcher.DockBarOpacity = e.NewValue; svc.Save(); RefreshDock(); };
|
||||
|
||||
// 표시 항목 토글 리스트
|
||||
if (DockItemsPanel == null) return;
|
||||
DockItemsPanel.Children.Clear();
|
||||
var toggleStyle = TryFindResource("ToggleSwitch") as System.Windows.Style;
|
||||
var enabled = launcher.DockBarItems;
|
||||
|
||||
foreach (var (key, icon, tooltip) in DockBarWindow.AvailableItems)
|
||||
{
|
||||
var row = new System.Windows.Controls.Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = $"{tooltip}",
|
||||
FontSize = 12.5,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Black,
|
||||
};
|
||||
System.Windows.Controls.Grid.SetColumn(label, 0);
|
||||
row.Children.Add(label);
|
||||
|
||||
var capturedKey = key;
|
||||
var cb = new CheckBox
|
||||
{
|
||||
IsChecked = enabled.Contains(key),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
if (toggleStyle != null) cb.Style = toggleStyle;
|
||||
cb.Checked += (_, _) => { if (!enabled.Contains(capturedKey)) enabled.Add(capturedKey); svc.Save(); RefreshDock(); };
|
||||
cb.Unchecked += (_, _) => { enabled.RemoveAll(x => x == capturedKey); svc.Save(); RefreshDock(); };
|
||||
System.Windows.Controls.Grid.SetColumn(cb, 1);
|
||||
row.Children.Add(cb);
|
||||
|
||||
DockItemsPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static SolidColorBrush BrushFromHex(string hex)
|
||||
{
|
||||
try { return ThemeResourceHelper.HexBrush(hex); }
|
||||
catch (Exception) { return new SolidColorBrush(Colors.Gray); }
|
||||
}
|
||||
|
||||
private static void RefreshDock()
|
||||
{
|
||||
CurrentApp?.RefreshDockBar();
|
||||
}
|
||||
|
||||
private void BtnDockResetPosition_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var result = CustomMessageBox.Show(
|
||||
"독 바를 화면 하단 중앙으로 이동하시겠습니까?",
|
||||
"독 바 위치 초기화",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
var svc = CurrentApp?.SettingsService;
|
||||
if (svc == null) return;
|
||||
svc.Settings.Launcher.DockBarLeft = -1;
|
||||
svc.Settings.Launcher.DockBarTop = -1;
|
||||
svc.Save();
|
||||
|
||||
// 즉시 독 바 위치 이동
|
||||
CurrentApp?.RefreshDockBar();
|
||||
}
|
||||
|
||||
// ─── 저장 공간 관리 ──────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshStorageInfo()
|
||||
{
|
||||
if (StorageSummaryText == null) return;
|
||||
var report = StorageAnalyzer.Analyze();
|
||||
|
||||
StorageSummaryText.Text = $"앱 전체 사용량: {StorageAnalyzer.FormatSize(report.TotalAppUsage)}";
|
||||
StorageDriveText.Text = $"드라이브 {report.DriveLabel} 여유: {StorageAnalyzer.FormatSize(report.DriveFreeSpace)} / {StorageAnalyzer.FormatSize(report.DriveTotalSpace)}";
|
||||
|
||||
if (StorageDetailPanel == null) return;
|
||||
StorageDetailPanel.Children.Clear();
|
||||
|
||||
var items = new (string Label, long Size)[]
|
||||
{
|
||||
("대화 기록", report.Conversations),
|
||||
("감사 로그", report.AuditLogs),
|
||||
("앱 로그", report.Logs),
|
||||
("코드 인덱스", report.CodeIndex),
|
||||
("임베딩 DB", report.EmbeddingDb),
|
||||
("클립보드 히스토리", report.ClipboardHistory),
|
||||
("플러그인", report.Plugins),
|
||||
("JSON 스킬", report.Skills),
|
||||
};
|
||||
|
||||
foreach (var (label, size) in items)
|
||||
{
|
||||
if (size == 0) continue;
|
||||
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, VerticalAlignment = VerticalAlignment.Center };
|
||||
Grid.SetColumn(labelTb, 0);
|
||||
row.Children.Add(labelTb);
|
||||
|
||||
var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = ThemeResourceHelper.Consolas, Foreground = Brushes.Gray, VerticalAlignment = VerticalAlignment.Center };
|
||||
Grid.SetColumn(sizeTb, 1);
|
||||
row.Children.Add(sizeTb);
|
||||
|
||||
StorageDetailPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnStorageRefresh_Click(object sender, RoutedEventArgs e) => RefreshStorageInfo();
|
||||
|
||||
private void RefreshStorageInfo2()
|
||||
{
|
||||
if (StorageSummaryText2 == null) return;
|
||||
var report = StorageAnalyzer.Analyze();
|
||||
StorageSummaryText2.Text = $"앱 전체 사용량: {StorageAnalyzer.FormatSize(report.TotalAppUsage)}";
|
||||
StorageDriveText2.Text = $"드라이브 {report.DriveLabel} 여유: {StorageAnalyzer.FormatSize(report.DriveFreeSpace)} / {StorageAnalyzer.FormatSize(report.DriveTotalSpace)}";
|
||||
|
||||
if (StorageDetailPanel2 == null) return;
|
||||
StorageDetailPanel2.Children.Clear();
|
||||
var items = new (string Label, long Size)[]
|
||||
{
|
||||
("대화 기록", report.Conversations), ("감사 로그", report.AuditLogs),
|
||||
("앱 로그", report.Logs), ("코드 인덱스", report.CodeIndex),
|
||||
("임베딩 DB", report.EmbeddingDb),
|
||||
("클립보드 히스토리", report.ClipboardHistory),
|
||||
("플러그인", report.Plugins), ("JSON 스킬", report.Skills),
|
||||
};
|
||||
foreach (var (label, size) in items)
|
||||
{
|
||||
if (size == 0) continue;
|
||||
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black };
|
||||
Grid.SetColumn(labelTb, 0); row.Children.Add(labelTb);
|
||||
var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = ThemeResourceHelper.Consolas, Foreground = Brushes.Gray };
|
||||
Grid.SetColumn(sizeTb, 1); row.Children.Add(sizeTb);
|
||||
StorageDetailPanel2.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnStorageRefresh2_Click(object sender, RoutedEventArgs e) => RefreshStorageInfo2();
|
||||
|
||||
private void BtnStorageCleanup_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 테마 리소스 조회
|
||||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||||
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x22, 0xFF, 0xFF, 0xFF));
|
||||
var shadowColor = TryFindResource("ShadowColor") is Color sc ? sc : Colors.Black;
|
||||
|
||||
// 보관 기간 선택 팝업 — 커스텀 버튼으로 날짜 선택
|
||||
var popup = new Window
|
||||
{
|
||||
WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent,
|
||||
Width = 360, SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Owner = this, ShowInTaskbar = false, Topmost = true,
|
||||
};
|
||||
|
||||
int selectedDays = -1;
|
||||
|
||||
var outerBorder = new Border
|
||||
{
|
||||
Background = bgBrush, CornerRadius = new CornerRadius(14), BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1), Margin = new Thickness(16),
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3, Color = shadowColor },
|
||||
};
|
||||
|
||||
var stack = new StackPanel { Margin = new Thickness(24, 20, 24, 20) };
|
||||
stack.Children.Add(new TextBlock { Text = "보관 기간 선택", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 6) });
|
||||
stack.Children.Add(new TextBlock { Text = "선택한 기간 이전의 데이터를 삭제합니다.\n※ 통계/대화 기록은 삭제되지 않습니다.", FontSize = 12, Foreground = subFgBrush, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 16) });
|
||||
|
||||
var btnDays = new (int Days, string Label)[] { (7, "최근 7일만 보관"), (14, "최근 14일만 보관"), (30, "최근 30일만 보관"), (0, "전체 삭제") };
|
||||
foreach (var (days, label) in btnDays)
|
||||
{
|
||||
var d = days;
|
||||
var isDelete = d == 0;
|
||||
var deleteBg = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0x44, 0x44));
|
||||
var deleteBorder = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x44, 0x44));
|
||||
var deleteText = new SolidColorBrush(Color.FromRgb(0xFF, 0x66, 0x66));
|
||||
var btn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(10), Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 0, 0, 6),
|
||||
Background = isDelete ? deleteBg : itemBg,
|
||||
BorderBrush = isDelete ? deleteBorder : borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
btn.Child = new TextBlock { Text = label, FontSize = 13, Foreground = isDelete ? deleteText : fgBrush };
|
||||
var normalBg = isDelete ? deleteBg : itemBg;
|
||||
btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = normalBg; };
|
||||
btn.MouseLeftButtonUp += (_, _) => { selectedDays = d; popup.Close(); };
|
||||
stack.Children.Add(btn);
|
||||
}
|
||||
|
||||
// 취소
|
||||
var cancelBtn = new Border { CornerRadius = new CornerRadius(10), Cursor = Cursors.Hand, Padding = new Thickness(14, 8, 14, 8), Margin = new Thickness(0, 6, 0, 0), Background = Brushes.Transparent };
|
||||
cancelBtn.Child = new TextBlock { Text = "취소", FontSize = 12, Foreground = subFgBrush, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
cancelBtn.MouseLeftButtonUp += (_, _) => popup.Close();
|
||||
stack.Children.Add(cancelBtn);
|
||||
|
||||
outerBorder.Child = stack;
|
||||
popup.Content = outerBorder;
|
||||
popup.ShowDialog();
|
||||
|
||||
if (selectedDays < 0) return;
|
||||
|
||||
// 삭제 전 확인
|
||||
var confirmMsg = selectedDays == 0
|
||||
? "전체 데이터를 삭제합니다. 정말 진행하시겠습니까?"
|
||||
: $"최근 {selectedDays}일 이전의 데이터를 삭제합니다. 정말 진행하시겠습니까?";
|
||||
var confirm = CustomMessageBox.Show(confirmMsg, "삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||
if (confirm != MessageBoxResult.Yes) return;
|
||||
|
||||
var freed = StorageAnalyzer.Cleanup(
|
||||
retainDays: selectedDays,
|
||||
cleanConversations: false,
|
||||
cleanAuditLogs: true,
|
||||
cleanLogs: true,
|
||||
cleanCodeIndex: true,
|
||||
cleanClipboard: selectedDays == 0
|
||||
);
|
||||
|
||||
CustomMessageBox.Show(
|
||||
$"{StorageAnalyzer.FormatSize(freed)}를 확보했습니다.",
|
||||
"정리 완료", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
RefreshStorageInfo();
|
||||
}
|
||||
|
||||
// ─── 알림 카테고리 체크박스 ───────────────────────────────────────────────
|
||||
|
||||
private void BuildQuoteCategoryCheckboxes()
|
||||
{
|
||||
if (QuoteCategoryPanel == null) return;
|
||||
QuoteCategoryPanel.Children.Clear();
|
||||
|
||||
var enabled = _vm.GetReminderCategories();
|
||||
var toggleStyle = TryFindResource("ToggleSwitch") as Style;
|
||||
|
||||
foreach (var (key, label, countFunc) in Services.QuoteService.Categories)
|
||||
{
|
||||
var count = countFunc();
|
||||
var displayLabel = key == "today_events"
|
||||
? $"{label} ({count}개, 오늘 {Services.QuoteService.GetTodayMatchCount()}개)"
|
||||
: $"{label} ({count}개)";
|
||||
|
||||
// 좌: 라벨, 우: 토글 스위치
|
||||
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var labelBlock = new TextBlock
|
||||
{
|
||||
Text = displayLabel,
|
||||
FontSize = 12.5,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = TryFindResource("PrimaryText") as System.Windows.Media.Brush
|
||||
?? System.Windows.Media.Brushes.White,
|
||||
};
|
||||
Grid.SetColumn(labelBlock, 0);
|
||||
row.Children.Add(labelBlock);
|
||||
|
||||
var cb = new CheckBox
|
||||
{
|
||||
IsChecked = enabled.Contains(key, StringComparer.OrdinalIgnoreCase),
|
||||
Tag = key,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
if (toggleStyle != null) cb.Style = toggleStyle;
|
||||
cb.Checked += (s, _) =>
|
||||
{
|
||||
if (s is CheckBox c && c.Tag is string k && !enabled.Contains(k))
|
||||
enabled.Add(k);
|
||||
};
|
||||
cb.Unchecked += (s, _) =>
|
||||
{
|
||||
if (s is CheckBox c && c.Tag is string k)
|
||||
enabled.RemoveAll(x => x.Equals(k, StringComparison.OrdinalIgnoreCase));
|
||||
};
|
||||
Grid.SetColumn(cb, 1);
|
||||
row.Children.Add(cb);
|
||||
|
||||
QuoteCategoryPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 버전 표시 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 하단 버전 텍스트를 AxCopilot.csproj <Version> 값에서 동적으로 읽어 설정합니다.
|
||||
/// 버전을 올릴 때는 AxCopilot.csproj → <Version> 하나만 수정하면 됩니다.
|
||||
/// 이 함수와 SettingsWindow.xaml 의 VersionInfoText 는 항상 함께 유지됩니다.
|
||||
/// </summary>
|
||||
private void SetVersionText()
|
||||
{
|
||||
try
|
||||
{
|
||||
var asm = System.Reflection.Assembly.GetExecutingAssembly();
|
||||
// FileVersionInfo 에서 읽어야 csproj <Version> 이 반영됩니다.
|
||||
var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(asm.Location);
|
||||
var ver = fvi.ProductVersion ?? fvi.FileVersion ?? "?";
|
||||
// 빌드 메타데이터 제거 (예: "1.0.3+gitabcdef" → "1.0.3")
|
||||
var plusIdx = ver.IndexOf('+');
|
||||
if (plusIdx > 0) ver = ver[..plusIdx];
|
||||
VersionInfoText.Text = $"AX Copilot · v{ver}";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
VersionInfoText.Text = "AX Copilot";
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 핫키 (콤보박스 선택 방식) ──────────────────────────────────────────
|
||||
|
||||
/// <summary>이전 녹화기에서 호출되던 초기화 — 콤보박스 전환 후 무연산 (호환용)</summary>
|
||||
private void RefreshHotkeyBadges() { /* 콤보박스 SelectedValue 바인딩으로 대체 */ }
|
||||
|
||||
/// <summary>현재 핫키가 콤보박스 목록에 없으면 항목으로 추가합니다.</summary>
|
||||
private void EnsureHotkeyInCombo()
|
||||
{
|
||||
if (HotkeyCombo == null) return;
|
||||
var hotkey = _vm.Hotkey;
|
||||
if (string.IsNullOrWhiteSpace(hotkey)) return;
|
||||
|
||||
// 이미 목록에 있는지 확인
|
||||
foreach (System.Windows.Controls.ComboBoxItem item in HotkeyCombo.Items)
|
||||
{
|
||||
if (item.Tag is string tag && tag == hotkey) return;
|
||||
}
|
||||
|
||||
// 목록에 없으면 현재 값을 추가
|
||||
var display = hotkey.Replace("+", " + ");
|
||||
var newItem = new System.Windows.Controls.ComboBoxItem
|
||||
{
|
||||
Content = $"{display} (사용자 정의)",
|
||||
Tag = hotkey
|
||||
};
|
||||
HotkeyCombo.Items.Insert(0, newItem);
|
||||
HotkeyCombo.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Window-level PreviewKeyDown — 핫키 녹화 제거 후 잔여 호출 보호</summary>
|
||||
private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { }
|
||||
|
||||
/// <summary>WPF Key → HotkeyParser가 인식하는 문자열 이름.</summary>
|
||||
private static string GetKeyName(Key key) => key switch
|
||||
{
|
||||
Key.Space => "Space",
|
||||
Key.Enter or Key.Return => "Enter",
|
||||
Key.Tab => "Tab",
|
||||
Key.Back => "Backspace",
|
||||
Key.Delete => "Delete",
|
||||
Key.Escape => "Escape",
|
||||
Key.Home => "Home",
|
||||
Key.End => "End",
|
||||
Key.PageUp => "PageUp",
|
||||
Key.PageDown => "PageDown",
|
||||
Key.Left => "Left",
|
||||
Key.Right => "Right",
|
||||
Key.Up => "Up",
|
||||
Key.Down => "Down",
|
||||
Key.Insert => "Insert",
|
||||
// A–Z
|
||||
>= Key.A and <= Key.Z => key.ToString(),
|
||||
// 0–9 (메인 키보드)
|
||||
>= Key.D0 and <= Key.D9 => ((int)(key - Key.D0)).ToString(),
|
||||
// F1–F12
|
||||
>= Key.F1 and <= Key.F12 => key.ToString(),
|
||||
// 기호
|
||||
Key.OemTilde => "`",
|
||||
Key.OemMinus => "-",
|
||||
Key.OemPlus => "=",
|
||||
Key.OemOpenBrackets => "[",
|
||||
Key.OemCloseBrackets => "]",
|
||||
Key.OemPipe or Key.OemBackslash => "\\",
|
||||
Key.OemSemicolon => ";",
|
||||
Key.OemQuotes => "'",
|
||||
Key.OemComma => ",",
|
||||
Key.OemPeriod => ".",
|
||||
Key.OemQuestion => "/",
|
||||
_ => key.ToString()
|
||||
};
|
||||
|
||||
private void HotkeyCombo_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
||||
{
|
||||
// 콤보박스 선택이 바뀌면 ViewModel의 Hotkey를 업데이트
|
||||
// (바인딩이 SelectedValue에 연결되어 자동 처리되지만,
|
||||
// 기존 RefreshHotkeyBadges 호출은 콤보박스 도입으로 불필요)
|
||||
}
|
||||
|
||||
// ─── 기존 이벤트 핸들러 ──────────────────────────────────────────────────
|
||||
|
||||
private async void BtnTestConnection_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var btn = sender as Button;
|
||||
if (btn != null) btn.Content = "테스트 중...";
|
||||
try
|
||||
{
|
||||
// 현재 UI 값으로 임시 LLM 서비스 생성하여 테스트 (설정 저장/창 닫기 없이)
|
||||
var llm = new Services.LlmService(_vm.Service);
|
||||
var (ok, msg) = await llm.TestConnectionAsync();
|
||||
llm.Dispose();
|
||||
CustomMessageBox.Show(msg, ok ? "연결 성공" : "연결 실패",
|
||||
MessageBoxButton.OK,
|
||||
ok ? MessageBoxImage.Information : MessageBoxImage.Warning);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CustomMessageBox.Show(ex.Message, "오류", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (btn != null) btn.Content = "테스트";
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 기능 설정 서브탭 전환 ──────────────────────────────────────────
|
||||
private void FuncSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (FuncPanel_AI == null) return; // 초기화 전 방어
|
||||
|
||||
FuncPanel_AI.Visibility = FuncSubTab_AI.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
FuncPanel_Launcher.Visibility = FuncSubTab_Launcher.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
FuncPanel_Design.Visibility = FuncSubTab_Design.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// ─── 접기/열기 섹션 토글 ───────────────────────────────────────────
|
||||
private void CollapsibleSection_Toggle(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Border border && border.Tag is System.Windows.Controls.Expander expander)
|
||||
expander.IsExpanded = !expander.IsExpanded;
|
||||
}
|
||||
|
||||
private void ServiceSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SvcPanelOllama == null) return; // 초기화 전 방어
|
||||
SvcPanelOllama.Visibility = SvcTabOllama.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
SvcPanelVllm.Visibility = SvcTabVllm.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
SvcPanelGemini.Visibility = SvcTabGemini.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
SvcPanelClaude.Visibility = SvcTabClaude.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ThemeCard_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button btn && btn.Tag is string key)
|
||||
_vm.SelectTheme(key);
|
||||
}
|
||||
|
||||
private void ColorSwatch_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button btn && btn.Tag is ColorRowModel row)
|
||||
_vm.PickColor(row);
|
||||
}
|
||||
|
||||
private void DevModeCheckBox_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not CheckBox cb || !cb.IsChecked.GetValueOrDefault()) return;
|
||||
// 설정 창 로드 중 바인딩에 의한 자동 Checked 이벤트 무시 (이미 활성화된 상태 복원)
|
||||
if (!IsLoaded) return;
|
||||
|
||||
// 테마 리소스 조회
|
||||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||||
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60));
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
|
||||
|
||||
// 비밀번호 확인 다이얼로그
|
||||
var dlg = new Window
|
||||
{
|
||||
Title = "개발자 모드 — 비밀번호 확인",
|
||||
Width = 340, SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Owner = this, ResizeMode = ResizeMode.NoResize,
|
||||
WindowStyle = WindowStyle.None, AllowsTransparency = true,
|
||||
Background = Brushes.Transparent,
|
||||
};
|
||||
var border = new Border
|
||||
{
|
||||
Background = bgBrush, CornerRadius = new CornerRadius(12),
|
||||
BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20),
|
||||
};
|
||||
var stack = new StackPanel();
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\U0001f512 개발자 모드 활성화",
|
||||
FontSize = 15, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12),
|
||||
});
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "비밀번호를 입력하세요:",
|
||||
FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
var pwBox = new PasswordBox
|
||||
{
|
||||
FontSize = 14, Padding = new Thickness(8, 6, 8, 6),
|
||||
Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*',
|
||||
};
|
||||
stack.Children.Add(pwBox);
|
||||
|
||||
var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) };
|
||||
var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) };
|
||||
cancelBtn.Click += (_, _) => { dlg.DialogResult = false; };
|
||||
btnRow.Children.Add(cancelBtn);
|
||||
var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true };
|
||||
okBtn.Click += (_, _) =>
|
||||
{
|
||||
if (pwBox.Password == "mouse12#")
|
||||
dlg.DialogResult = true;
|
||||
else
|
||||
{
|
||||
pwBox.Clear();
|
||||
pwBox.Focus();
|
||||
}
|
||||
};
|
||||
btnRow.Children.Add(okBtn);
|
||||
stack.Children.Add(btnRow);
|
||||
border.Child = stack;
|
||||
dlg.Content = border;
|
||||
dlg.Loaded += (_, _) => pwBox.Focus();
|
||||
|
||||
if (dlg.ShowDialog() != true)
|
||||
{
|
||||
// 비밀번호 실패/취소 — 체크 해제 + DevMode 강제 false
|
||||
_vm.DevMode = false;
|
||||
cb.IsChecked = false;
|
||||
}
|
||||
UpdateDevModeContentVisibility();
|
||||
}
|
||||
|
||||
private void DevModeCheckBox_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateDevModeContentVisibility();
|
||||
}
|
||||
|
||||
/// <summary>개발자 모드 활성화 상태에 따라 개발자 탭 내용 표시/숨김.</summary>
|
||||
private void UpdateDevModeContentVisibility()
|
||||
{
|
||||
if (DevModeContent != null)
|
||||
DevModeContent.Visibility = _vm.DevMode ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -146,7 +146,7 @@ public partial class ShortcutHelpWindow : Window
|
||||
iconBorder.Child = new TextBlock
|
||||
{
|
||||
Text = row.Icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 10,
|
||||
Foreground = ParseBrush(colorHex),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -168,7 +168,7 @@ public partial class ShortcutHelpWindow : Window
|
||||
keyBorder.Child = new TextBlock
|
||||
{
|
||||
Text = row.Key,
|
||||
FontFamily = new FontFamily("Consolas, Courier New"),
|
||||
FontFamily = ThemeResourceHelper.ConsolasCourierNew,
|
||||
FontSize = 11,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -98,7 +98,7 @@ public partial class SkillEditorWindow : Window
|
||||
border.Child = new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
Foreground = isSelected ? Brushes.White : subBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -166,7 +166,7 @@ public partial class SkillEditorWindow : Window
|
||||
{
|
||||
Text = tool.Name,
|
||||
FontSize = 11.5,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = fgBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
@@ -364,7 +364,7 @@ public partial class SkillEditorWindow : Window
|
||||
var textBox = new TextBox
|
||||
{
|
||||
Text = content,
|
||||
FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"),
|
||||
FontFamily = ThemeResourceHelper.ConsolasCode,
|
||||
FontSize = 12.5,
|
||||
IsReadOnly = true,
|
||||
AcceptsReturn = true,
|
||||
|
||||
@@ -202,7 +202,7 @@ public partial class SkillGalleryWindow : Window
|
||||
iconBorder.Child = new TextBlock
|
||||
{
|
||||
Text = skill.Icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 16,
|
||||
Foreground = skill.IsAvailable
|
||||
? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))
|
||||
@@ -222,7 +222,7 @@ public partial class SkillGalleryWindow : Window
|
||||
Text = $"/{skill.Name}",
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = skill.IsAvailable
|
||||
? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))
|
||||
: subBrush,
|
||||
@@ -415,7 +415,7 @@ public partial class SkillGalleryWindow : Window
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 12,
|
||||
Foreground = new SolidColorBrush(col),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -488,7 +488,7 @@ public partial class SkillGalleryWindow : Window
|
||||
titleLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = skill.Icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
@@ -515,7 +515,7 @@ public partial class SkillGalleryWindow : Window
|
||||
closeBtn.Child = new TextBlock
|
||||
{
|
||||
Text = "\uE8BB",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 10,
|
||||
Foreground = subBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -611,7 +611,7 @@ public partial class SkillGalleryWindow : Window
|
||||
{
|
||||
Text = promptText,
|
||||
FontSize = 11.5,
|
||||
FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"),
|
||||
FontFamily = ThemeResourceHelper.ConsolasCode,
|
||||
Foreground = fgBrush,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Opacity = 0.85,
|
||||
|
||||
@@ -82,7 +82,7 @@ public partial class TextActionPopup : Window
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -116,4 +116,13 @@ public static class ThemeResourceHelper
|
||||
|
||||
/// <summary>Consolas FontFamily (캐시됨).</summary>
|
||||
public static readonly FontFamily Consolas = new("Consolas");
|
||||
|
||||
/// <summary>Cascadia Code → Consolas → monospace 폴백 체인 (코드 블록용, 캐시됨).</summary>
|
||||
public static readonly FontFamily CascadiaCode = new("Cascadia Code, Consolas, monospace");
|
||||
|
||||
/// <summary>Consolas → Cascadia Code → Segoe UI 폴백 체인 (인라인 코드용, 캐시됨).</summary>
|
||||
public static readonly FontFamily ConsolasCode = new("Consolas, Cascadia Code, Segoe UI");
|
||||
|
||||
/// <summary>Consolas → Courier New 폴백 체인 (터미널 스타일, 캐시됨).</summary>
|
||||
public static readonly FontFamily ConsolasCourierNew = new("Consolas, Courier New");
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ public partial class TrayMenuWindow : Window
|
||||
var glyphBlock = new TextBlock
|
||||
{
|
||||
Text = glyph,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
Foreground = isChecked ? BulbOnBrush : BulbOffBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
@@ -206,7 +206,7 @@ public partial class TrayMenuWindow : Window
|
||||
var glyphBlock = new TextBlock
|
||||
{
|
||||
Text = glyph,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 14,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Width = 20,
|
||||
|
||||
@@ -67,7 +67,7 @@ internal sealed class UserAskDialog : Window
|
||||
titleSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE9CE",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 16,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
|
||||
@@ -340,7 +340,7 @@ public partial class WorkflowAnalyzerWindow : Window
|
||||
{
|
||||
Text = Truncate(entry.Label, 12),
|
||||
FontSize = 10,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = new SolidColorBrush(barColor),
|
||||
Width = labelWidth,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
@@ -372,7 +372,7 @@ public partial class WorkflowAnalyzerWindow : Window
|
||||
Text = FormatMs(entry.DurationMs),
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(barColor),
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontFamily = ThemeResourceHelper.Consolas,
|
||||
};
|
||||
Canvas.SetLeft(timeText, labelWidth + 8 + barStart + barWidth + 4);
|
||||
Canvas.SetTop(timeText, y + 3);
|
||||
@@ -443,7 +443,7 @@ public partial class WorkflowAnalyzerWindow : Window
|
||||
{
|
||||
Text = Truncate(name, 12),
|
||||
FontSize = 11,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontFamily = ThemeResourceHelper.Consolas,
|
||||
Foreground = new SolidColorBrush(barColor),
|
||||
TextAlignment = TextAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
@@ -476,7 +476,7 @@ public partial class WorkflowAnalyzerWindow : Window
|
||||
Text = FormatMs(ms),
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush(barColor),
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontFamily = ThemeResourceHelper.Consolas,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
};
|
||||
@@ -691,7 +691,7 @@ public partial class WorkflowAnalyzerWindow : Window
|
||||
iconBorder.Child = new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 10,
|
||||
Foreground = Brushes.White,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
|
||||
Reference in New Issue
Block a user