Initial commit to new repository
This commit is contained in:
776
src/AxCopilot/App.xaml.cs
Normal file
776
src/AxCopilot/App.xaml.cs
Normal file
@@ -0,0 +1,776 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Win32;
|
||||
using AxCopilot.Core;
|
||||
using AxCopilot.Handlers;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using AxCopilot.ViewModels;
|
||||
using AxCopilot.Views;
|
||||
|
||||
namespace AxCopilot;
|
||||
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
private System.Threading.Mutex? _singleInstanceMutex;
|
||||
private InputListener? _inputListener;
|
||||
private LauncherWindow? _launcher;
|
||||
private NotifyIcon? _trayIcon;
|
||||
private Views.TrayMenuWindow? _trayMenu;
|
||||
private SettingsService? _settings;
|
||||
private SettingsWindow? _settingsWindow;
|
||||
private PluginHost? _pluginHost;
|
||||
private ClipboardHistoryService? _clipboardHistory;
|
||||
private DockBarWindow? _dockBar;
|
||||
private FileDialogWatcher? _fileDialogWatcher;
|
||||
private volatile IndexService? _indexService;
|
||||
public IndexService? IndexService => _indexService;
|
||||
public SettingsService? SettingsService => _settings;
|
||||
public ClipboardHistoryService? ClipboardHistoryService => _clipboardHistory;
|
||||
private SessionTrackingService? _sessionTracking;
|
||||
private WorktimeReminderService? _worktimeReminder;
|
||||
private ScreenCaptureHandler? _captureHandler;
|
||||
private AgentMemoryService? _memoryService;
|
||||
public AgentMemoryService? MemoryService => _memoryService;
|
||||
private LlmService? _sharedLlm;
|
||||
private PluginInstallService? _pluginInstallService;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
// ─── 보안: 디버거/디컴파일러 감지 (Release 빌드만 활성) ───
|
||||
Security.AntiTamper.Check();
|
||||
|
||||
// 전역 예외 핸들러 — 미처리 예외 시 로그 + 메시지 (크래시 방지)
|
||||
DispatcherUnhandledException += (_, ex) =>
|
||||
{
|
||||
LogService.Error($"미처리 예외: {ex.Exception}");
|
||||
CustomMessageBox.Show(
|
||||
$"오류가 발생했습니다:\n{ex.Exception.Message}\n\n로그 폴더를 확인하세요.",
|
||||
"AX Copilot 오류", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
||||
ex.Handled = true;
|
||||
};
|
||||
|
||||
// ─── 중복 실행 방지 ──────────────────────────────────────────────────
|
||||
_singleInstanceMutex = new System.Threading.Mutex(
|
||||
initiallyOwned: true, "Global\\AXCopilot_SingleInstance", out bool createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
_singleInstanceMutex.Dispose();
|
||||
_singleInstanceMutex = null;
|
||||
CustomMessageBox.Show("AX Copilot가 이미 실행 중입니다.\n트레이 아이콘을 확인하세요.",
|
||||
"AX Copilot", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
|
||||
Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
LogService.Info("=== AX Copilot 시작 ===");
|
||||
|
||||
// ─── 서비스 초기화 ────────────────────────────────────────────────────
|
||||
_settings = new SettingsService();
|
||||
var settings = _settings;
|
||||
settings.Load();
|
||||
|
||||
// Phase L3 공유 서비스
|
||||
_sharedLlm = new LlmService(settings);
|
||||
_pluginInstallService = new PluginInstallService();
|
||||
|
||||
// 마이그레이션 알림 (UI가 준비된 후 표시)
|
||||
if (settings.MigrationSummary != null)
|
||||
{
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
CustomMessageBox.Show(
|
||||
settings.MigrationSummary,
|
||||
"AX Copilot — 설정 업데이트",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Information);
|
||||
}, System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
// 언어 초기화
|
||||
L10n.SetLanguage(settings.Settings.Launcher.Language);
|
||||
|
||||
// 첫 실행 시 자동 시작 등록 (레지스트리에 아직 없으면)
|
||||
if (!IsAutoStartEnabled())
|
||||
SetAutoStart(true);
|
||||
|
||||
_memoryService = new AgentMemoryService();
|
||||
_memoryService.Load(settings.Settings.Llm.WorkFolder);
|
||||
|
||||
_indexService = new IndexService(settings);
|
||||
var indexService = _indexService;
|
||||
var fuzzyEngine = new FuzzyEngine(indexService);
|
||||
var commandResolver = new CommandResolver(fuzzyEngine, settings);
|
||||
var contextManager = new ContextManager(settings);
|
||||
|
||||
// ─── 세션 추적 / 사용 통계 ────────────────────────────────────────────
|
||||
_sessionTracking = new SessionTrackingService();
|
||||
|
||||
// ─── 잠금 해제 알림 서비스 ────────────────────────────────────────────
|
||||
_worktimeReminder = new WorktimeReminderService(settings, Dispatcher);
|
||||
|
||||
// ─── 클립보드 히스토리 서비스 ──────────────────────────────────────────
|
||||
_clipboardHistory = new ClipboardHistoryService(settings);
|
||||
|
||||
// ─── 빌트인 핸들러 등록 ──────────────────────────────────────────────
|
||||
commandResolver.RegisterHandler(new CalculatorHandler());
|
||||
commandResolver.RegisterHandler(new SystemCommandHandler(settings));
|
||||
commandResolver.RegisterHandler(new SnippetHandler(settings));
|
||||
commandResolver.RegisterHandler(new ClipboardHistoryHandler(_clipboardHistory));
|
||||
commandResolver.RegisterHandler(new WorkspaceHandler(contextManager, settings));
|
||||
commandResolver.RegisterHandler(new UrlAliasHandler(settings));
|
||||
commandResolver.RegisterHandler(new FolderAliasHandler(settings));
|
||||
commandResolver.RegisterHandler(new BatchHandler(settings));
|
||||
commandResolver.RegisterHandler(new ClipboardHandler(settings));
|
||||
commandResolver.RegisterHandler(new BookmarkHandler());
|
||||
commandResolver.RegisterHandler(new WebSearchHandler(settings));
|
||||
commandResolver.RegisterHandler(new ProcessHandler());
|
||||
commandResolver.RegisterHandler(new MediaHandler());
|
||||
commandResolver.RegisterHandler(new SystemInfoHandler());
|
||||
commandResolver.RegisterHandler(new StarInfoHandler()); // * 단축키 (info와 동일)
|
||||
commandResolver.RegisterHandler(new EmojiHandler());
|
||||
commandResolver.RegisterHandler(new ColorHandler());
|
||||
commandResolver.RegisterHandler(new RecentFilesHandler());
|
||||
commandResolver.RegisterHandler(new NoteHandler());
|
||||
commandResolver.RegisterHandler(new UninstallHandler());
|
||||
commandResolver.RegisterHandler(new PortHandler());
|
||||
commandResolver.RegisterHandler(new EnvHandler());
|
||||
commandResolver.RegisterHandler(new JsonHandler());
|
||||
commandResolver.RegisterHandler(new EncodeHandler());
|
||||
commandResolver.RegisterHandler(new SnapHandler());
|
||||
_captureHandler = new ScreenCaptureHandler(settings);
|
||||
commandResolver.RegisterHandler(_captureHandler);
|
||||
commandResolver.RegisterHandler(new ColorPickHandler());
|
||||
commandResolver.RegisterHandler(new DateCalcHandler());
|
||||
commandResolver.RegisterHandler(new ServiceHandler(_clipboardHistory));
|
||||
commandResolver.RegisterHandler(new ClipboardPipeHandler());
|
||||
commandResolver.RegisterHandler(new JournalHandler());
|
||||
commandResolver.RegisterHandler(new RoutineHandler());
|
||||
commandResolver.RegisterHandler(new BatchTextHandler());
|
||||
commandResolver.RegisterHandler(new DiffHandler(_clipboardHistory));
|
||||
commandResolver.RegisterHandler(new WindowSwitchHandler());
|
||||
commandResolver.RegisterHandler(new RunHandler());
|
||||
commandResolver.RegisterHandler(new TextStatsHandler());
|
||||
commandResolver.RegisterHandler(new FavoriteHandler());
|
||||
commandResolver.RegisterHandler(new RenameHandler());
|
||||
commandResolver.RegisterHandler(new MonitorHandler());
|
||||
commandResolver.RegisterHandler(new ScaffoldHandler());
|
||||
commandResolver.RegisterHandler(new EverythingHandler());
|
||||
commandResolver.RegisterHandler(new HelpHandler(settings));
|
||||
commandResolver.RegisterHandler(new ChatHandler(settings));
|
||||
|
||||
// ─── Phase L3 핸들러 ──────────────────────────────────────────────────
|
||||
commandResolver.RegisterHandler(new QuickLinkHandler(settings));
|
||||
var snippetTemplateSvc = new SnippetTemplateService(settings, _sharedLlm);
|
||||
commandResolver.RegisterHandler(new AiSnippetHandler(settings, snippetTemplateSvc));
|
||||
commandResolver.RegisterHandler(new WebSearchSummaryHandler(settings, _sharedLlm));
|
||||
|
||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||
var pluginHost = new PluginHost(settings, commandResolver);
|
||||
pluginHost.LoadAll();
|
||||
_pluginHost = pluginHost;
|
||||
|
||||
// ─── 런처 윈도우 ──────────────────────────────────────────────────────
|
||||
var vm = new LauncherViewModel(commandResolver, settings);
|
||||
_launcher = new LauncherWindow(vm)
|
||||
{
|
||||
OpenSettingsAction = OpenSettings
|
||||
};
|
||||
|
||||
// ─── 클립보드 히스토리 초기화 (메시지 펌프 시작 직후 — 런처 표시 불필요) ──
|
||||
Dispatcher.BeginInvoke(
|
||||
() => _clipboardHistory?.Initialize(),
|
||||
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||||
|
||||
// ─── ChatWindow 미리 생성 (앱 유휴 시점에 숨겨진 채로 초기화) ──────────
|
||||
// 이후 OpenAiChat() 시 창 생성 비용 없이 즉시 열림
|
||||
Dispatcher.BeginInvoke(
|
||||
() => PrewarmChatWindow(),
|
||||
System.Windows.Threading.DispatcherPriority.SystemIdle);
|
||||
|
||||
// ─── 인덱스 빌드 (백그라운드) + 완료 후 FileSystemWatcher 시작 ────────
|
||||
_ = indexService.BuildAsync().ContinueWith(_ => indexService.StartWatchers());
|
||||
|
||||
// ─── 글로벌 훅 + 스니펫 확장기 ───────────────────────────────────────
|
||||
_inputListener = new InputListener();
|
||||
_inputListener.HotkeyTriggered += OnHotkeyTriggered;
|
||||
_inputListener.CaptureHotkeyTriggered += OnCaptureHotkeyTriggered;
|
||||
_inputListener.HookFailed += OnHookFailed;
|
||||
|
||||
// 설정에 저장된 핫키로 초기화 (기본: Alt+Space)
|
||||
_inputListener.UpdateHotkey(settings.Settings.Hotkey);
|
||||
|
||||
// 글로벌 캡처 단축키 초기화
|
||||
_inputListener.UpdateCaptureHotkey(
|
||||
settings.Settings.ScreenCapture.GlobalHotkey,
|
||||
settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
|
||||
|
||||
var snippetExpander = new SnippetExpander(settings);
|
||||
_inputListener.KeyFilter = snippetExpander.HandleKey;
|
||||
|
||||
_inputListener.Start();
|
||||
|
||||
// ─── 시스템 트레이 ─────────────────────────────────────────────────────
|
||||
SetupTrayIcon(pluginHost, settings);
|
||||
|
||||
// ─── 파일 대화상자 감지 ──────────────────────────────────────────────
|
||||
_fileDialogWatcher = new FileDialogWatcher();
|
||||
_fileDialogWatcher.FileDialogOpened += (_, hwnd) =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (_launcher == null || _launcher.IsVisible) return;
|
||||
if (_settings?.Settings.Launcher.EnableFileDialogIntegration != true) return;
|
||||
WindowTracker.Capture();
|
||||
_launcher.Show();
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
|
||||
() => _launcher.SetInputText("cd "));
|
||||
});
|
||||
};
|
||||
_fileDialogWatcher.Start();
|
||||
|
||||
// 독 바 자동 표시
|
||||
if (settings.Settings.Launcher.DockBarAutoShow)
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, () => ToggleDockBar());
|
||||
|
||||
LogService.Info($"초기화 완료. {settings.Settings.Hotkey}로 실행하세요.");
|
||||
}
|
||||
|
||||
private void OnHotkeyTriggered(object? sender, EventArgs e)
|
||||
{
|
||||
// 런처가 열리기 전 현재 활성 창을 저장 (SnapHandler, ScreenCaptureHandler용)
|
||||
WindowTracker.Capture();
|
||||
|
||||
// 선택 텍스트 감지는 UI 스레드 밖에서 수행해야 SendInput이 정상 처리됨
|
||||
var launcherSettings = _settings?.Settings.Launcher;
|
||||
var enableTextAction = launcherSettings?.EnableTextAction == true;
|
||||
|
||||
// ── 런처 토글(닫기)은 텍스트 감지 없이 즉시 처리 ──
|
||||
bool isVisible = false;
|
||||
Dispatcher.Invoke(() => { isVisible = _launcher?.IsVisible == true; });
|
||||
if (isVisible)
|
||||
{
|
||||
Dispatcher.Invoke(() => _launcher?.Hide());
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 텍스트 감지가 비활성이면 즉시 런처 표시 ──
|
||||
if (!enableTextAction)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (_launcher == null) return;
|
||||
UsageStatisticsService.RecordLauncherOpen();
|
||||
_launcher.Show();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 텍스트 감지 활성: 런처를 먼저 열고, 텍스트 감지를 비동기로 처리 ──
|
||||
string? selectedText = TryGetSelectedText();
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (_launcher == null) return;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(selectedText))
|
||||
{
|
||||
var enabledCmds = launcherSettings?.TextActionCommands ?? new();
|
||||
|
||||
// 활성 명령이 1개뿐이면 팝업 없이 바로 실행
|
||||
if (enabledCmds.Count == 1)
|
||||
{
|
||||
var directAction = TextActionPopup.AvailableCommands
|
||||
.FirstOrDefault(c => c.Key == enabledCmds[0]);
|
||||
if (!string.IsNullOrEmpty(directAction.Key))
|
||||
{
|
||||
var actionResult = enabledCmds[0] switch
|
||||
{
|
||||
"translate" => TextActionPopup.ActionResult.Translate,
|
||||
"summarize" => TextActionPopup.ActionResult.Summarize,
|
||||
"grammar" => TextActionPopup.ActionResult.GrammarFix,
|
||||
"explain" => TextActionPopup.ActionResult.Explain,
|
||||
"rewrite" => TextActionPopup.ActionResult.Rewrite,
|
||||
_ => TextActionPopup.ActionResult.None,
|
||||
};
|
||||
if (actionResult != TextActionPopup.ActionResult.None)
|
||||
{
|
||||
ExecuteTextAction(actionResult, selectedText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 여러 개 → 팝업 표시
|
||||
var popup = new TextActionPopup(selectedText, enabledCmds);
|
||||
popup.Closed += (_, _) =>
|
||||
{
|
||||
switch (popup.SelectedAction)
|
||||
{
|
||||
case TextActionPopup.ActionResult.OpenLauncher:
|
||||
UsageStatisticsService.RecordLauncherOpen();
|
||||
_launcher.Show();
|
||||
break;
|
||||
case TextActionPopup.ActionResult.None:
|
||||
break; // Esc 또는 포커스 잃음
|
||||
default:
|
||||
// AI 명령 실행 → AX Agent 대화로 전달
|
||||
ExecuteTextAction(popup.SelectedAction, popup.SelectedText);
|
||||
break;
|
||||
}
|
||||
};
|
||||
popup.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
UsageStatisticsService.RecordLauncherOpen();
|
||||
_launcher.Show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>현재 선택된 텍스트를 가져옵니다 (Ctrl+C 시뮬레이션).</summary>
|
||||
/// <remarks>후크 스레드에서 호출됩니다 (UI 스레드 아님). 클립보드 접근은 STA 스레드가 필요합니다.</remarks>
|
||||
private string? TryGetSelectedText()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 클립보드는 STA 스레드에서만 접근 가능 → Dispatcher로 읽기
|
||||
string? prevText = null;
|
||||
bool hadText = false;
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
hadText = System.Windows.Clipboard.ContainsText();
|
||||
prevText = hadText ? System.Windows.Clipboard.GetText() : null;
|
||||
System.Windows.Clipboard.Clear();
|
||||
});
|
||||
|
||||
System.Threading.Thread.Sleep(30);
|
||||
|
||||
// Ctrl+C 시뮬레이션 (후크 스레드에서 → 대상 앱이 처리)
|
||||
var inputs = new[]
|
||||
{
|
||||
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x11 } } },
|
||||
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x43 } } },
|
||||
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x43, dwFlags = 0x0002 } } },
|
||||
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x11, dwFlags = 0x0002 } } },
|
||||
};
|
||||
NativeMethods.SendInput((uint)inputs.Length, inputs, System.Runtime.InteropServices.Marshal.SizeOf<NativeMethods.INPUT>());
|
||||
|
||||
// 클립보드 업데이트 대기 (앱마다 다름)
|
||||
System.Threading.Thread.Sleep(80);
|
||||
|
||||
// 클립보드에서 새 텍스트 확인 (STA 필요)
|
||||
string? selected = null;
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (System.Windows.Clipboard.ContainsText())
|
||||
{
|
||||
var copied = System.Windows.Clipboard.GetText();
|
||||
if (copied != prevText && !string.IsNullOrWhiteSpace(copied))
|
||||
selected = copied;
|
||||
}
|
||||
|
||||
// 클립보드 복원
|
||||
if (selected != null && prevText != null)
|
||||
System.Windows.Clipboard.SetText(prevText);
|
||||
else if (selected != null && !hadText)
|
||||
System.Windows.Clipboard.Clear();
|
||||
});
|
||||
|
||||
return selected;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>AI 텍스트 명령을 AX Agent 대화로 전달합니다.</summary>
|
||||
private void ExecuteTextAction(TextActionPopup.ActionResult action, string text)
|
||||
{
|
||||
var prompt = action switch
|
||||
{
|
||||
TextActionPopup.ActionResult.Translate => $"다음 텍스트를 번역해줘:\n\n{text}",
|
||||
TextActionPopup.ActionResult.Summarize => $"다음 텍스트를 요약해줘:\n\n{text}",
|
||||
TextActionPopup.ActionResult.GrammarFix => $"다음 텍스트의 문법을 교정해줘:\n\n{text}",
|
||||
TextActionPopup.ActionResult.Explain => $"다음 텍스트를 쉽게 설명해줘:\n\n{text}",
|
||||
TextActionPopup.ActionResult.Rewrite => $"다음 텍스트를 다시 작성해줘:\n\n{text}",
|
||||
_ => text
|
||||
};
|
||||
|
||||
// ! 프리픽스로 AX Agent에 전달
|
||||
_launcher?.Show();
|
||||
_launcher?.SetInputText($"! {prompt}");
|
||||
}
|
||||
|
||||
private static class NativeMethods
|
||||
{
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||
public struct INPUT { public uint type; public InputUnion u; }
|
||||
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit)]
|
||||
public struct InputUnion { [System.Runtime.InteropServices.FieldOffset(0)] public KEYBDINPUT ki; }
|
||||
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||
public struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
||||
}
|
||||
|
||||
private void OnCaptureHotkeyTriggered(object? sender, EventArgs e)
|
||||
{
|
||||
// 런처 없이 직접 캡처 실행
|
||||
WindowTracker.Capture();
|
||||
var mode = _settings?.Settings.ScreenCapture.GlobalHotkeyMode ?? "screen";
|
||||
Dispatcher.Invoke(async () =>
|
||||
{
|
||||
if (_captureHandler == null) return;
|
||||
try
|
||||
{
|
||||
await _captureHandler.CaptureDirectAsync(mode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"글로벌 캡처 실패: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnHookFailed(object? sender, EventArgs e)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_trayIcon?.ShowBalloonTip(3000, "AX Copilot",
|
||||
$"글로벌 단축키 등록에 실패했습니다.\n다른 앱과 {_settings?.Settings.Hotkey ?? "단축키"}가 충돌하고 있을 수 있습니다.",
|
||||
ToolTipIcon.Warning);
|
||||
});
|
||||
}
|
||||
|
||||
private void SetupTrayIcon(PluginHost pluginHost, SettingsService settings)
|
||||
{
|
||||
_trayIcon = new NotifyIcon
|
||||
{
|
||||
Text = "AX Copilot",
|
||||
Visible = true,
|
||||
Icon = LoadAppIcon()
|
||||
};
|
||||
|
||||
// ─── WPF 커스텀 트레이 메뉴 구성 ──────────────────────────────────
|
||||
_trayMenu = new Views.TrayMenuWindow();
|
||||
_trayMenu
|
||||
.AddItem("\uE7C5", "AX Commander 호출하기", () =>
|
||||
Dispatcher.Invoke(() => _launcher?.Show()))
|
||||
.AddItem("\uE8BD", "AX Agent 대화하기", () =>
|
||||
Dispatcher.Invoke(() => OpenAiChat()), out var aiTrayItem)
|
||||
.AddItem("\uE8A7", "독 바 표시", () =>
|
||||
Dispatcher.Invoke(() => ToggleDockBar()))
|
||||
.AddSeparator()
|
||||
.AddItem("\uE72C", "플러그인 재로드", () =>
|
||||
{
|
||||
pluginHost.Reload();
|
||||
_trayIcon!.ShowBalloonTip(2000, "AX Copilot", "플러그인이 재로드되었습니다.", ToolTipIcon.None);
|
||||
})
|
||||
.AddItem("\uE838", "로그 폴더 열기", () =>
|
||||
{
|
||||
var logDir = System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "logs");
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("explorer.exe", logDir)
|
||||
{ UseShellExecute = true });
|
||||
})
|
||||
.AddItem("\uE9D9", "사용 통계", () =>
|
||||
Dispatcher.Invoke(() => new StatisticsWindow().Show()))
|
||||
.AddItem("\uE736", "사용 가이드 문서보기", () =>
|
||||
{
|
||||
try { Dispatcher.Invoke(() => new Views.GuideViewerWindow().Show()); }
|
||||
catch (Exception ex) { LogService.Error($"사용 가이드 열기 실패: {ex.Message}"); }
|
||||
})
|
||||
.AddSeparator()
|
||||
.AddToggleItem("\uE82F", "Windows 시작 시 자동 실행", IsAutoStartEnabled(),
|
||||
isChecked => SetAutoStart(isChecked), out _, out _)
|
||||
.AddSeparator()
|
||||
.AddItem("\uE713", "설정", () =>
|
||||
Dispatcher.Invoke(OpenSettings))
|
||||
.AddItem("\uE946", "개발 정보", () =>
|
||||
Dispatcher.Invoke(() => new AboutWindow().Show()))
|
||||
.AddItem("\uE711", "종료", () =>
|
||||
{
|
||||
_inputListener?.Dispose();
|
||||
_trayIcon?.Dispose();
|
||||
Shutdown();
|
||||
});
|
||||
|
||||
// AI 기능 활성화 여부에 따라 메뉴 항목 가시성 동적 업데이트
|
||||
_trayMenu.Opening += () =>
|
||||
{
|
||||
aiTrayItem.Visibility = settings.Settings.AiEnabled
|
||||
? System.Windows.Visibility.Visible
|
||||
: System.Windows.Visibility.Collapsed;
|
||||
};
|
||||
|
||||
// 우클릭 → WPF 메뉴 표시, 좌클릭 → 런처 토글
|
||||
_trayIcon.MouseClick += (_, e) =>
|
||||
{
|
||||
if (e.Button == System.Windows.Forms.MouseButtons.Left)
|
||||
Dispatcher.Invoke(() => _launcher?.Show());
|
||||
else if (e.Button == System.Windows.Forms.MouseButtons.Right)
|
||||
Dispatcher.Invoke(() => _trayMenu?.ShowWithUpdate());
|
||||
};
|
||||
|
||||
// 타이머/알람 풍선 알림 서비스 연결
|
||||
NotificationService.Initialize((title, msg) =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
_trayIcon?.ShowBalloonTip(4000, title, msg, ToolTipIcon.None));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>ChatWindow 등 외부에서 설정 창을 여는 공개 메서드.</summary>
|
||||
public void OpenSettingsFromChat() => Dispatcher.Invoke(OpenSettings);
|
||||
|
||||
/// <summary>AX Agent 창 열기 (트레이 메뉴 등에서 호출).</summary>
|
||||
private Views.ChatWindow? _chatWindow;
|
||||
|
||||
/// <summary>
|
||||
/// ChatWindow를 백그라운드에서 미리 생성합니다 (앱 시작 후 저우선순위로 호출).
|
||||
/// 이후 OpenAiChat() 시 창 생성 비용 없이 즉시 Show/Activate만 수행합니다.
|
||||
/// </summary>
|
||||
internal void PrewarmChatWindow()
|
||||
{
|
||||
if (_chatWindow != null || _settings == null) return;
|
||||
_chatWindow = new Views.ChatWindow(_settings);
|
||||
}
|
||||
|
||||
private void OpenAiChat()
|
||||
{
|
||||
if (_settings == null) return;
|
||||
if (_chatWindow == null)
|
||||
{
|
||||
_chatWindow = new Views.ChatWindow(_settings);
|
||||
}
|
||||
_chatWindow.Show();
|
||||
_chatWindow.Activate();
|
||||
}
|
||||
|
||||
public void ToggleDockBar()
|
||||
{
|
||||
if (_dockBar != null && _dockBar.IsVisible)
|
||||
{
|
||||
_dockBar.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dockBar == null)
|
||||
{
|
||||
_dockBar = new DockBarWindow();
|
||||
_dockBar.OnQuickSearch = query =>
|
||||
{
|
||||
if (_launcher == null) return;
|
||||
_launcher.Show();
|
||||
_launcher.Activate(); // 독 바 뒤가 아닌 전면에 표시
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
|
||||
() => _launcher.SetInputText(query));
|
||||
};
|
||||
_dockBar.OnCapture = async () =>
|
||||
{
|
||||
WindowTracker.Capture();
|
||||
if (_captureHandler != null)
|
||||
await _captureHandler.CaptureDirectAsync("region");
|
||||
};
|
||||
_dockBar.OnOpenAgent = () =>
|
||||
{
|
||||
if (_launcher == null) return;
|
||||
_launcher.Show();
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
|
||||
() => _launcher.SetInputText("!"));
|
||||
};
|
||||
}
|
||||
var launcher = _settings?.Settings.Launcher;
|
||||
var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" };
|
||||
_dockBar.BuildFromSettings(dockItems);
|
||||
_dockBar.OnPositionChanged = (left, top) =>
|
||||
{
|
||||
if (_settings != null)
|
||||
{
|
||||
_settings.Settings.Launcher.DockBarLeft = left;
|
||||
_settings.Settings.Launcher.DockBarTop = top;
|
||||
_settings.Save();
|
||||
}
|
||||
};
|
||||
_dockBar.Show();
|
||||
_dockBar.ApplySettings(
|
||||
launcher?.DockBarOpacity ?? 0.92,
|
||||
launcher?.DockBarLeft ?? -1,
|
||||
launcher?.DockBarTop ?? -1,
|
||||
launcher?.DockBarRainbowGlow ?? false);
|
||||
}
|
||||
|
||||
/// <summary>독 바를 현재 설정으로 즉시 새로고침합니다.</summary>
|
||||
public void RefreshDockBar()
|
||||
{
|
||||
if (_dockBar == null || !_dockBar.IsVisible) return;
|
||||
var launcher = _settings?.Settings.Launcher;
|
||||
var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" };
|
||||
_dockBar.BuildFromSettings(dockItems);
|
||||
_dockBar.ApplySettings(
|
||||
launcher?.DockBarOpacity ?? 0.92,
|
||||
launcher?.DockBarLeft ?? -1,
|
||||
launcher?.DockBarTop ?? -1,
|
||||
launcher?.DockBarRainbowGlow ?? false);
|
||||
}
|
||||
|
||||
private void OpenSettings()
|
||||
{
|
||||
if (_settingsWindow != null && _settingsWindow.IsVisible)
|
||||
{
|
||||
_settingsWindow.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settings == null || _launcher == null) return;
|
||||
|
||||
var vm = new ViewModels.SettingsViewModel(_settings);
|
||||
|
||||
// 미리보기 콜백: 현재 편집 중인 색상(vm.ColorRows)으로 런처에 즉시 반영
|
||||
void PreviewCallback(string themeKey)
|
||||
{
|
||||
if (themeKey == "custom")
|
||||
{
|
||||
var tempColors = new AxCopilot.Models.CustomThemeColors();
|
||||
foreach (var row in vm.ColorRows)
|
||||
{
|
||||
var prop = typeof(AxCopilot.Models.CustomThemeColors).GetProperty(row.Property);
|
||||
prop?.SetValue(tempColors, row.Hex);
|
||||
}
|
||||
_launcher.ApplyTheme(themeKey, tempColors);
|
||||
}
|
||||
else
|
||||
{
|
||||
_launcher.ApplyTheme(themeKey, _settings.Settings.Launcher.CustomTheme);
|
||||
}
|
||||
}
|
||||
|
||||
// 취소/X 닫기 콜백: 파일에 저장된 원본 설정으로 복원
|
||||
void RevertCallback()
|
||||
{
|
||||
_launcher.ApplyTheme(
|
||||
_settings.Settings.Launcher.Theme ?? "system",
|
||||
_settings.Settings.Launcher.CustomTheme);
|
||||
}
|
||||
|
||||
_settingsWindow = new Views.SettingsWindow(vm, PreviewCallback, RevertCallback)
|
||||
{
|
||||
// 핫키 녹화 중 글로벌 핫키 일시 정지
|
||||
SuspendHotkeyCallback = suspend =>
|
||||
{
|
||||
if (_inputListener != null)
|
||||
_inputListener.SuspendHotkey = suspend;
|
||||
}
|
||||
};
|
||||
|
||||
// 저장 완료 시 InputListener 핫키 갱신 + 알림 타이머 재시작
|
||||
vm.SaveCompleted += (_, _) =>
|
||||
{
|
||||
if (_inputListener != null && _settings != null)
|
||||
{
|
||||
_inputListener.UpdateHotkey(_settings.Settings.Hotkey);
|
||||
_inputListener.UpdateCaptureHotkey(
|
||||
_settings.Settings.ScreenCapture.GlobalHotkey,
|
||||
_settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
|
||||
}
|
||||
_worktimeReminder?.RestartTimer();
|
||||
};
|
||||
|
||||
_settingsWindow.Show();
|
||||
}
|
||||
|
||||
// ─── 자동 시작 레지스트리 헬퍼 ──────────────────────────────────────────
|
||||
|
||||
private static System.Drawing.Icon LoadAppIcon()
|
||||
{
|
||||
// DPI 인식 아이콘 크기 (기본 16 → 고DPI에서 20/24/32)
|
||||
var iconSize = System.Windows.Forms.SystemInformation.SmallIconSize;
|
||||
|
||||
// 1) 파일 시스템에서 로드 (개발 환경)
|
||||
try
|
||||
{
|
||||
var exeDir = System.IO.Path.GetDirectoryName(Environment.ProcessPath)
|
||||
?? AppContext.BaseDirectory;
|
||||
var path = System.IO.Path.Combine(exeDir, "Assets", "icon.ico");
|
||||
if (System.IO.File.Exists(path))
|
||||
return new System.Drawing.Icon(path, iconSize);
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
// 2) 내장 리소스에서 로드 (PublishSingleFile 배포)
|
||||
try
|
||||
{
|
||||
var uri = new Uri("pack://application:,,,/Assets/icon.ico");
|
||||
var stream = System.Windows.Application.GetResourceStream(uri)?.Stream;
|
||||
if (stream != null)
|
||||
return new System.Drawing.Icon(stream, iconSize);
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
return System.Drawing.SystemIcons.Application;
|
||||
}
|
||||
|
||||
private const string AutoRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
private const string AutoRunName = "AxCopilot";
|
||||
|
||||
private static bool IsAutoStartEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(AutoRunKey, writable: false);
|
||||
return key?.GetValue(AutoRunName) != null;
|
||||
}
|
||||
catch (Exception) { return false; }
|
||||
}
|
||||
|
||||
private static void SetAutoStart(bool enable)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(AutoRunKey, writable: true);
|
||||
if (key == null) return;
|
||||
|
||||
if (enable)
|
||||
{
|
||||
var exePath = Environment.ProcessPath
|
||||
?? System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName
|
||||
?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(exePath))
|
||||
key.SetValue(AutoRunName, $"\"{exePath}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
key.DeleteValue(AutoRunName, throwOnMissingValue: false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"자동 시작 레지스트리 설정 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
_chatWindow?.ForceClose(); // 미리 생성된 ChatWindow 진짜 닫기
|
||||
_inputListener?.Dispose();
|
||||
_clipboardHistory?.Dispose();
|
||||
_indexService?.Dispose();
|
||||
_sessionTracking?.Dispose();
|
||||
_worktimeReminder?.Dispose();
|
||||
_trayIcon?.Dispose();
|
||||
try { _singleInstanceMutex?.ReleaseMutex(); } catch (Exception) { }
|
||||
_singleInstanceMutex?.Dispose();
|
||||
LogService.Info("=== AX Copilot 종료 ===");
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user