using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Services; namespace AxCopilot.Views; /// /// L5-4: 앱 세션 편집기. /// 세션 이름, 앱 목록(경로 + 라벨 + 스냅 위치)을 편집하여 저장합니다. /// public partial class SessionEditorWindow : Window { private readonly SettingsService _settings; private readonly AppSession? _original; // 편집 모드 원본 (새 세션이면 null) private readonly List _rows = new(); // 스냅 팝업 대상 행 private AppRowUi? _snapTargetRow; // 사용 가능한 스냅 위치 목록 (키 → 표시명) private static readonly (string Key, string Label)[] SnapOptions = [ ("full", "전체화면"), ("left", "왼쪽 절반"), ("right", "오른쪽 절반"), ("tl", "좌상단 1/4"), ("tr", "우상단 1/4"), ("bl", "좌하단 1/4"), ("br", "우하단 1/4"), ("center", "중앙 80%"), ("third-l", "좌측 1/3"), ("third-c", "중앙 1/3"), ("third-r", "우측 1/3"), ("two3-l", "좌측 2/3"), ("two3-r", "우측 2/3"), ("none", "스냅 없음"), ]; // ────────────────────────────────────────────────────────────────────── /// /// 세션 편집기를 엽니다. /// /// 편집할 기존 세션. null이면 새로 만들기 모드. /// 설정 서비스. public SessionEditorWindow(AppSession? session, SettingsService settings) { InitializeComponent(); _settings = settings; _original = session; BuildSnapPopup(); LoadSession(session); } /// 새 세션 모드일 때 기본 이름을 설정합니다. public string InitialName { set { if (_original == null) NameBox.Text = value; } } // ─── 초기화 ─────────────────────────────────────────────────────────── private void LoadSession(AppSession? session) { if (session == null) { NameBox.Text = "새 세션"; DescBox.Text = ""; } else { NameBox.Text = session.Name; DescBox.Text = session.Description; foreach (var app in session.Apps) AddRow(app.Path, app.Label, app.SnapPosition, app.Arguments, app.DelayMs); } RefreshEmptyState(); } private void BuildSnapPopup() { var panel = new StackPanel { Margin = new Thickness(0) }; foreach (var (key, label) in SnapOptions) { var keyCapture = key; var border = new Border { CornerRadius = new CornerRadius(4), Padding = new Thickness(10, 5, 10, 5), Cursor = Cursors.Hand }; border.MouseEnter += (_, _) => border.Background = new SolidColorBrush(Color.FromArgb(0x28, 0xFF, 0xFF, 0xFF)); border.MouseLeave += (_, _) => border.Background = Brushes.Transparent; var stack = new StackPanel { Orientation = Orientation.Horizontal }; stack.Children.Add(new TextBlock { Text = keyCapture, FontFamily = new FontFamily("Cascadia Code, Consolas"), FontSize = 11, Foreground = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue, VerticalAlignment = VerticalAlignment.Center, MinWidth = 68, }); stack.Children.Add(new TextBlock { Text = label, FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, }); border.Child = stack; border.MouseLeftButtonUp += (_, _) => { if (_snapTargetRow != null) { _snapTargetRow.SnapPosition = keyCapture; _snapTargetRow.UpdateSnapLabel(); } SnapPickerPopup.IsOpen = false; }; panel.Children.Add(border); } SnapOptionsList.Content = panel; } // ─── 앱 행 추가 ─────────────────────────────────────────────────────── private void AddRow(string path = "", string label = "", string snap = "full", string args = "", int delayMs = 0) { var row = new AppRowUi(path, label, snap, args, delayMs); _rows.Add(row); var rowGrid = BuildRowGrid(row); AppListPanel.Children.Add(rowGrid); RefreshEmptyState(); } private Grid BuildRowGrid(AppRowUi row) { var grid = new Grid { Margin = new Thickness(14, 2, 4, 2) }; grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(100) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(108) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) }); // 경로 TextBox var pathBox = new TextBox { Text = row.Path, FontSize = 11, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, BorderThickness = new Thickness(1), Padding = new Thickness(6, 4, 6, 4), VerticalContentAlignment = VerticalAlignment.Center, ToolTip = "앱 실행 파일 경로", Margin = new Thickness(0, 0, 4, 0), }; pathBox.TextChanged += (_, _) => row.Path = pathBox.Text; Grid.SetColumn(pathBox, 0); // 라벨 TextBox var labelBox = new TextBox { Text = row.Label, FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, BorderThickness = new Thickness(1), Padding = new Thickness(6, 4, 6, 4), VerticalContentAlignment = VerticalAlignment.Center, ToolTip = "표시 이름 (선택)", Margin = new Thickness(0, 0, 4, 0), }; labelBox.TextChanged += (_, _) => row.Label = labelBox.Text; Grid.SetColumn(labelBox, 1); // 스냅 선택 Border (클릭 시 팝업) var snapBtn = new Border { CornerRadius = new CornerRadius(4), BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, BorderThickness = new Thickness(1), Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand, Margin = new Thickness(0, 0, 4, 0), ToolTip = "스냅 위치 선택", }; snapBtn.MouseEnter += (_, _) => snapBtn.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); snapBtn.MouseLeave += (_, _) => snapBtn.Background = Brushes.Transparent; var snapLabel = new TextBlock { FontFamily = new FontFamily("Cascadia Code, Consolas"), FontSize = 10, Foreground = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue, VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis, }; snapBtn.Child = snapLabel; // AppRowUi가 라벨 TextBlock을 참조할 수 있도록 저장 row.SnapLabelRef = snapLabel; row.SnapButtonRef = snapBtn; row.UpdateSnapLabel(); snapBtn.MouseLeftButtonUp += (sender, e) => { _snapTargetRow = row; SnapPickerPopup.PlacementTarget = (FrameworkElement)sender; SnapPickerPopup.IsOpen = true; e.Handled = true; }; Grid.SetColumn(snapBtn, 2); // 삭제 버튼 var delBtn = new Border { Width = 24, Height = 24, CornerRadius = new CornerRadius(4), Cursor = Cursors.Hand, ToolTip = "이 앱 제거", VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center, }; delBtn.MouseEnter += (_, _) => delBtn.Background = new SolidColorBrush(Color.FromArgb(0x30, 0xEF, 0x53, 0x50)); delBtn.MouseLeave += (_, _) => delBtn.Background = Brushes.Transparent; var delIcon = new TextBlock { Text = "\uE74D", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50)), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; delBtn.Child = delIcon; delBtn.MouseLeftButtonUp += (_, _) => { _rows.Remove(row); AppListPanel.Children.Remove(grid); RefreshEmptyState(); }; Grid.SetColumn(delBtn, 3); grid.Children.Add(pathBox); grid.Children.Add(labelBox); grid.Children.Add(snapBtn); grid.Children.Add(delBtn); return grid; } private void RefreshEmptyState() { EmptyState.Visibility = _rows.Count == 0 ? Visibility.Visible : Visibility.Collapsed; } // ─── 이벤트 핸들러 ──────────────────────────────────────────────────── private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) DragMove(); } private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close(); private void BtnCancel_Click(object sender, MouseButtonEventArgs e) => Close(); private void BtnAddApp_Click(object sender, MouseButtonEventArgs e) { using var dlg = new System.Windows.Forms.OpenFileDialog { Title = "앱 실행 파일 선택", Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*", }; if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; var label = Path.GetFileNameWithoutExtension(dlg.FileName); AddRow(dlg.FileName, label, "full"); } private void BtnSave_Click(object sender, MouseButtonEventArgs e) { var name = NameBox.Text.Trim(); if (string.IsNullOrWhiteSpace(name)) { NotificationService.Notify("세션 편집기", "세션 이름을 입력하세요."); NameBox.Focus(); return; } // 빈 경로 행 필터링 var validApps = _rows .Where(r => !string.IsNullOrWhiteSpace(r.Path)) .Select(r => new SessionApp { Path = r.Path.Trim(), Arguments = r.Arguments.Trim(), Label = r.Label.Trim(), SnapPosition = r.SnapPosition, DelayMs = r.DelayMs, }).ToList(); var session = new AppSession { Name = name, Description = DescBox.Text.Trim(), Apps = validApps, CreatedAt = _original?.CreatedAt ?? DateTime.Now, }; // 기존 세션 교체 또는 신규 추가 if (_original != null) { var idx = _settings.Settings.AppSessions.IndexOf(_original); if (idx >= 0) _settings.Settings.AppSessions[idx] = session; else _settings.Settings.AppSessions.Add(session); } else { // 동일 이름 세션이 있으면 교체 var existing = _settings.Settings.AppSessions .FirstOrDefault(s => s.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (existing != null) _settings.Settings.AppSessions.Remove(existing); _settings.Settings.AppSessions.Add(session); } _settings.Save(); NotificationService.Notify("AX Copilot", $"세션 '{name}' 저장됨 ({validApps.Count}개 앱)"); LogService.Info($"세션 저장: {name} ({validApps.Count}개 앱)"); Close(); } } // ─── 앱 행 UI 모델 ──────────────────────────────────────────────────────────── internal class AppRowUi { public string Path { get; set; } public string Label { get; set; } public string SnapPosition { get; set; } public string Arguments { get; set; } public int DelayMs { get; set; } // UI 참조 (라벨 갱신용) internal System.Windows.Controls.TextBlock? SnapLabelRef { get; set; } internal System.Windows.Controls.Border? SnapButtonRef { get; set; } public AppRowUi(string path, string label, string snap, string args, int delayMs) { Path = path; Label = label; SnapPosition = snap; Arguments = args; DelayMs = delayMs; } public void UpdateSnapLabel() { if (SnapLabelRef != null) SnapLabelRef.Text = SnapPosition; } }