창 위치 기억 (Feature 1):
- AppSettings.cs: LauncherSettings에 RememberPosition, LastLeft, LastTop 프로퍼티 추가
- SettingsViewModel.cs/.Properties.cs/.Methods.cs: RememberPosition 바인딩 프로퍼티 연동
- LauncherWindow.Animations.cs: CenterOnScreen() — RememberPosition ON 시 저장 좌표 복원
- LauncherWindow.Shell.cs: Window_Deactivated — 비활성화 시 현재 위치 비동기 저장
- SettingsWindow.xaml: 런처 탭 › "마지막 위치 기억" 토글 추가
파일 아이콘 표시 (Feature 2):
- Services/IconCacheService.cs (신규, 192줄): Shell32 SHGetFileInfo로 아이콘 추출,
%LOCALAPPDATA%\AxCopilot\IconCache\에 PNG 캐시, WarmUp()으로 앱 시작 시 미리 준비
- Core/CommandResolver.cs: 퍼지 검색 결과에 IconCacheService.GetIconPath() 연결
- Handlers/FileBrowserHandler.cs: 상위폴더·폴더·파일 항목에 IconCacheService 연결
- App.xaml.cs: SystemIdle 시점에 IconCacheService.WarmUp() 호출
퍼지 검색 랭킹 개선 (Feature 3):
- Services/UsageRankingService.cs 전면 개선: 기존 int 횟수 → UsageRecord{Count, LastUsedMs}
- GetScore() 반환형 int → double, 30일 반감기 지수 감쇠(decay=exp(-days/43.3)) 적용
- 구형 usage.json 자동 마이그레이션 (count만 있는 형식 → 신규 형식)
- GetTopItems() / SortByUsage() 점수 기준 정렬로 업데이트
미리보기 패널 (Feature 4):
- ViewModels/LauncherViewModel.cs: PreviewText, HasPreview 프로퍼티 + UpdatePreviewAsync()
클립보드 텍스트(최대 400자) 및 텍스트 파일(최초 6줄) 미리보기, 80ms 디바운스
- Views/LauncherWindow.xaml: RowDefinitions 7→8개, Row5에 PreviewPanel Border 삽입,
IndexStatusText Row5→6, WidgetBar Row6→7, ToastOverlay RowSpan 3→4
빌드: 경고 0, 오류 0
651 lines
34 KiB
C#
651 lines
34 KiB
C#
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 SchedulerService? _schedulerService;
|
|
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));
|
|
// Phase L3-5: 파일 태그 시스템
|
|
commandResolver.RegisterHandler(new TagHandler());
|
|
// Phase L3-8: 알림 센터
|
|
commandResolver.RegisterHandler(new NotifHandler());
|
|
// Phase L3-9: 뽀모도로 타이머
|
|
commandResolver.RegisterHandler(new PomoHandler());
|
|
|
|
// ─── Phase L4 핸들러 ──────────────────────────────────────────────────
|
|
// Phase L4-1: 인라인 파일 탐색기 (Prefix=null, 경로 패턴 감지)
|
|
commandResolver.RegisterHandler(new FileBrowserHandler());
|
|
|
|
// ─── Phase L5 핸들러 ──────────────────────────────────────────────────
|
|
// Phase L5-1: 전용 핫키 목록 관리 (prefix=hotkey)
|
|
commandResolver.RegisterHandler(new HotkeyHandler(settings));
|
|
// Phase L5-2: OCR 화면 텍스트 추출 (prefix=ocr)
|
|
commandResolver.RegisterHandler(new OcrHandler());
|
|
// Phase L5-4: 앱 세션 스냅 (prefix=session)
|
|
commandResolver.RegisterHandler(new SessionHandler(settings));
|
|
// Phase L5-5: 배치 파일 이름변경 (prefix=batchren)
|
|
commandResolver.RegisterHandler(new BatchRenameHandler());
|
|
// Phase L5-6: 자동화 스케줄러 (prefix=sched)
|
|
var schedulerService = new SchedulerService(settings);
|
|
schedulerService.Start();
|
|
_schedulerService = schedulerService;
|
|
commandResolver.RegisterHandler(new ScheduleHandler(settings));
|
|
// Phase L6-2: 런처 매크로 (prefix=macro)
|
|
commandResolver.RegisterHandler(new MacroHandler(settings));
|
|
// Phase L6-3: 컨텍스트 감지 자동완성 (prefix=ctx)
|
|
commandResolver.RegisterHandler(new ContextHandler());
|
|
|
|
// ─── Phase L7 핸들러 ──────────────────────────────────────────────────
|
|
// L7-1: Git 빠른 조회 (prefix=git)
|
|
commandResolver.RegisterHandler(new GitHandler());
|
|
// L7-2: 정규식 테스터 (prefix=re)
|
|
commandResolver.RegisterHandler(new RegexHandler());
|
|
// L7-3: 시간대 변환기 (prefix=tz)
|
|
commandResolver.RegisterHandler(new TimeZoneHandler());
|
|
// L7-4: 네트워크 진단 (prefix=net)
|
|
commandResolver.RegisterHandler(new NetDiagHandler());
|
|
|
|
// ─── Phase L8 핸들러 ──────────────────────────────────────────────────
|
|
// L8-1: 파일 해시 검증 (prefix=hash)
|
|
commandResolver.RegisterHandler(new FileHashHandler());
|
|
// L8-2: 아카이브 관리 (prefix=zip)
|
|
commandResolver.RegisterHandler(new ZipHandler());
|
|
// L8-3: 시스템 이벤트 로그 (prefix=evt)
|
|
commandResolver.RegisterHandler(new EventLogHandler());
|
|
// L8-4: SSH 퀵 커넥트 (prefix=ssh)
|
|
commandResolver.RegisterHandler(new SshHandler(settings));
|
|
|
|
// ─── Phase L9 핸들러 ──────────────────────────────────────────────────
|
|
// L9-1: 비밀번호 생성기 (prefix=pwd)
|
|
commandResolver.RegisterHandler(new PasswordGenHandler());
|
|
// L9-2: IP 서브넷 계산기 (prefix=subnet)
|
|
commandResolver.RegisterHandler(new SubnetHandler());
|
|
// L9-3: 시스템 정리 (prefix=clean)
|
|
commandResolver.RegisterHandler(new CleanHandler());
|
|
// L9-4: 진수 변환기 (prefix=base)
|
|
commandResolver.RegisterHandler(new BaseConvertHandler());
|
|
|
|
// ─── Phase L10 핸들러 ─────────────────────────────────────────────────
|
|
// L10-1: XML 포맷터·검증기·XPath (prefix=xml)
|
|
commandResolver.RegisterHandler(new XmlHandler());
|
|
// L10-2: UUID/GUID 생성기 (prefix=uuid)
|
|
commandResolver.RegisterHandler(new UuidHandler());
|
|
// L10-3: SSL 인증서 체커 (prefix=cert)
|
|
commandResolver.RegisterHandler(new CertHandler());
|
|
// L10-4: Lorem Ipsum 더미 텍스트 (prefix=lorem)
|
|
commandResolver.RegisterHandler(new LoremHandler());
|
|
|
|
// ─── Phase L11 핸들러 ─────────────────────────────────────────────────
|
|
// L11-1: CSV 뷰어·파서 (prefix=csv)
|
|
commandResolver.RegisterHandler(new CsvHandler());
|
|
// L11-2: JWT 토큰 디코더 (prefix=jwt)
|
|
commandResolver.RegisterHandler(new JwtHandler());
|
|
// L11-3: Cron 표현식 설명기 (prefix=cron)
|
|
commandResolver.RegisterHandler(new CronHandler());
|
|
// L11-4: 유니코드 문자 조회 (prefix=unicode)
|
|
commandResolver.RegisterHandler(new UnicodeHandler());
|
|
|
|
// ─── Phase L12 핸들러 ─────────────────────────────────────────────────
|
|
// L12-1: HTTP 요청 테스터 (prefix=http)
|
|
commandResolver.RegisterHandler(new HttpTesterHandler());
|
|
// L12-2: hosts 파일 관리 (prefix=hosts)
|
|
commandResolver.RegisterHandler(new HostsHandler());
|
|
// L12-3: 모스 부호 변환기 (prefix=morse)
|
|
commandResolver.RegisterHandler(new MorseHandler());
|
|
// L12-4: 시작 프로그램 조회 (prefix=startup)
|
|
commandResolver.RegisterHandler(new StartupHandler());
|
|
|
|
// ─── Phase L13 핸들러 ─────────────────────────────────────────────────
|
|
// L13-1: DNS 레코드 조회 (prefix=dns)
|
|
commandResolver.RegisterHandler(new DnsQueryHandler());
|
|
// L13-2: PATH 환경변수 뷰어 (prefix=path)
|
|
commandResolver.RegisterHandler(new PathHandler());
|
|
// L13-3: 드라이브 정보 (prefix=drive)
|
|
commandResolver.RegisterHandler(new DriveHandler());
|
|
// L13-4: 나이·D-day 계산기 (prefix=age)
|
|
commandResolver.RegisterHandler(new AgeHandler());
|
|
|
|
// ─── Phase L14 핸들러 ─────────────────────────────────────────────────
|
|
// L14-1: Wake-on-LAN (prefix=wol)
|
|
commandResolver.RegisterHandler(new WolHandler());
|
|
// L14-2: 레지스트리 조회 (prefix=reg)
|
|
commandResolver.RegisterHandler(new RegHandler());
|
|
// L14-3: 팁·할인·분할 계산기 (prefix=tip)
|
|
commandResolver.RegisterHandler(new TipHandler());
|
|
// L14-4: 시스템 폰트 목록 (prefix=font)
|
|
commandResolver.RegisterHandler(new FontHandler());
|
|
|
|
// ─── Phase L15 핸들러 ─────────────────────────────────────────────────
|
|
// L15-1: WSL 관리 (prefix=wsl)
|
|
commandResolver.RegisterHandler(new WslHandler());
|
|
// L15-2: 환율 변환기 (prefix=currency)
|
|
commandResolver.RegisterHandler(new CurrencyHandler());
|
|
// L15-3: BMI·건강 계산기 (prefix=bmi)
|
|
commandResolver.RegisterHandler(new BmiHandler());
|
|
// L15-4: Markdown 분석기 (prefix=md)
|
|
commandResolver.RegisterHandler(new MdHandler());
|
|
|
|
// ─── Phase L16 핸들러 ─────────────────────────────────────────────────
|
|
// L16-1: ping·tracert 실행기 (prefix=ping)
|
|
commandResolver.RegisterHandler(new PingHandler());
|
|
// L16-2: Docker 컨테이너·이미지 조회 (prefix=docker)
|
|
commandResolver.RegisterHandler(new DockerHandler());
|
|
// L16-3: 할 일 목록 (prefix=todo)
|
|
commandResolver.RegisterHandler(new TodoHandler());
|
|
// L16-4: 텍스트 → 표 변환기 (prefix=table)
|
|
commandResolver.RegisterHandler(new TableHandler());
|
|
|
|
// ─── Phase L17 핸들러 ─────────────────────────────────────────────────
|
|
// L17-1: 단위 변환기 (prefix=unit)
|
|
commandResolver.RegisterHandler(new UnitHandler());
|
|
// L17-2: 숫자 포맷·읽기 변환기 (prefix=num)
|
|
commandResolver.RegisterHandler(new NumHandler());
|
|
// L17-3: YAML 파서·포맷터 (prefix=yaml)
|
|
commandResolver.RegisterHandler(new YamlHandler());
|
|
// L17-4: .gitignore 생성기 (prefix=gitignore)
|
|
commandResolver.RegisterHandler(new GitignoreHandler());
|
|
|
|
// ─── Phase L18 핸들러 ─────────────────────────────────────────────────
|
|
// L18-1: SQL 포맷터·분석기 (prefix=sql)
|
|
commandResolver.RegisterHandler(new SqlHandler());
|
|
// L18-2: 텍스트 케이스 변환기 (prefix=text)
|
|
commandResolver.RegisterHandler(new TextCaseHandler());
|
|
// L18-3: 화면 비율·해상도 계산기 (prefix=aspect)
|
|
commandResolver.RegisterHandler(new AspectHandler());
|
|
// L18-4: IT·개발 약어 사전 (prefix=abbr)
|
|
commandResolver.RegisterHandler(new AbbrHandler());
|
|
// L19-1: 공학 계산기 (prefix=calc)
|
|
commandResolver.RegisterHandler(new CalcHandler());
|
|
// L19-2: 타이머·알람 (prefix=timer)
|
|
commandResolver.RegisterHandler(new TimerHandler());
|
|
// L19-3: IP 주소 유틸리티 (prefix=ip)
|
|
commandResolver.RegisterHandler(new IpInfoHandler());
|
|
// L19-4: npm/yarn/pnpm 명령어 생성기 (prefix=npm)
|
|
commandResolver.RegisterHandler(new NpmHandler());
|
|
// L20-1: 16진수·바이트 변환기 (prefix=hex)
|
|
commandResolver.RegisterHandler(new HexHandler());
|
|
// L20-2: 랜덤 생성기 (prefix=rand)
|
|
commandResolver.RegisterHandler(new RandHandler());
|
|
// L20-3: 문자열 조작 도구 (prefix=str)
|
|
commandResolver.RegisterHandler(new StrHandler());
|
|
// L20-4: Unix 파일 권한 계산기 (prefix=perm)
|
|
commandResolver.RegisterHandler(new PermHandler());
|
|
// L21-1: TOML 파서·분석기 (prefix=toml)
|
|
commandResolver.RegisterHandler(new TomlHandler());
|
|
// L21-2: 로그 파일 분석기 (prefix=log)
|
|
commandResolver.RegisterHandler(new LogHandler());
|
|
// L21-3: PowerShell 명령 생성기 (prefix=ps)
|
|
commandResolver.RegisterHandler(new PsHandler());
|
|
// L21-4: 키보드 단축키 참조 사전 (prefix=key)
|
|
commandResolver.RegisterHandler(new KeyHandler());
|
|
|
|
// L22-1: 프로세스 상세 조회·정리 (prefix=proc)
|
|
commandResolver.RegisterHandler(new ProcHandler());
|
|
// L22-2: Excel 함수 레퍼런스 (prefix=xl)
|
|
commandResolver.RegisterHandler(new XlHandler());
|
|
// L22-3: Python pip 명령 생성기 (prefix=pip)
|
|
commandResolver.RegisterHandler(new PipHandler());
|
|
// L22-4: 업무 양식·문서 구조 템플릿 (prefix=form)
|
|
commandResolver.RegisterHandler(new FormHandler());
|
|
|
|
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
|
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);
|
|
|
|
// ─── 파일 아이콘 캐시 워밍업 (앱 유휴 시점에 자주 쓰는 확장자 미리 추출) ──
|
|
Dispatcher.BeginInvoke(
|
|
() => AxCopilot.Services.IconCacheService.WarmUp(),
|
|
System.Windows.Threading.DispatcherPriority.SystemIdle);
|
|
|
|
// ─── 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.CustomHotkeyTriggered += OnCustomHotkeyTriggered;
|
|
_inputListener.HookFailed += OnHookFailed;
|
|
|
|
// 설정에 저장된 핫키로 초기화 (기본: Alt+Space)
|
|
_inputListener.UpdateHotkey(settings.Settings.Hotkey);
|
|
|
|
// 글로벌 캡처 단축키 초기화
|
|
_inputListener.UpdateCaptureHotkey(
|
|
settings.Settings.ScreenCapture.GlobalHotkey,
|
|
settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
|
|
|
|
// 항목별 전용 핫키 초기화
|
|
_inputListener.UpdateCustomHotkeys(settings.Settings.CustomHotkeys);
|
|
|
|
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 OnCustomHotkeyTriggered(object? sender, Core.CustomHotkeyEventArgs e)
|
|
{
|
|
// 전용 핫키 발동: 런처를 열지 않고 대상을 직접 실행
|
|
Handlers.HotkeyHandler.ExecuteHotkeyTarget(e.Target, e.Type);
|
|
}
|
|
|
|
private void OnHookFailed(object? sender, EventArgs e)
|
|
{
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
_trayIcon?.ShowBalloonTip(3000, "AX Copilot",
|
|
$"글로벌 단축키 등록에 실패했습니다.\n다른 앱과 {_settings?.Settings.Hotkey ?? "단축키"}가 충돌하고 있을 수 있습니다.",
|
|
ToolTipIcon.Warning);
|
|
});
|
|
}
|
|
}
|