[Phase L5-4] 앱 세션 스냅 (session) 구현
AppSettings.Models.cs (수정):
- AppSession 클래스 신규: Name/Description/Apps/CreatedAt
- SessionApp 클래스 신규: Path/Arguments/Label/SnapPosition/DelayMs
- 스냅 위치 14종: full/left/right/tl~br/center/third-l/c/r/two3-l/r/none
AppSettings.cs (수정):
- [JsonPropertyName("appSessions")] AppSessions 리스트 추가
SessionHandler.cs (신규, ~160줄):
- prefix="session", GetItemsAsync: 목록+필터, new/edit/del 서브커맨드
- ExecuteAsync: 편집기 열기 또는 LaunchSessionAsync 실행
- LaunchSessionAsync: Process.Start → 창 핸들 대기(최대 6초 폴링) → ApplySnapToWindow
- ApplySnapToWindow: P/Invoke(SetWindowPos, ShowWindow, MonitorFromWindow, GetMonitorInfo) + 14종 스냅 좌표 계산
SessionEditorWindow.xaml (신규, ~200줄):
- 타이틀바 + 세션명·설명 TextBox + 앱 목록 영역 + 하단 버튼 바
- SnapPickerPopup: PlacementTarget 코드에서 동적 지정, ContentControl에 StackPanel 14개 옵션
SessionEditorWindow.xaml.cs (신규, ~240줄):
- AppRowUi 내부 모델: Path/Label/SnapPosition/Args/DelayMs + SnapLabelRef 갱신
- BuildRowGrid(): 경로TextBox + 라벨TextBox + 스냅Border(팝업) + 삭제Border
- BtnAddApp: OpenFileDialog → 자동 라벨(파일명)
- BtnSave: 유효 행 필터 → AppSession 구성 → 기존 교체 또는 신규 추가 → Save()
App.xaml.cs (수정):
- Phase L5-4 섹션에 SessionHandler 등록
docs/LAUNCHER_ROADMAP.md (수정):
- L5-4 항목 ✅ 완료 표시 + 구현 상세 기록
빌드: 경고 0, 오류 0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -132,7 +132,7 @@
|
||||
| L5-1 | **항목별 전용 핫키** ✅ | 앱·URL·폴더에 `Ctrl+Alt+숫자` 등 글로벌 단축키 직접 할당. `hotkey` 프리픽스로 관리. `HotkeyAssignment` 모델 + `InputListener` 확장 + 설정창 "전용 핫키" 탭 | 높음 |
|
||||
| L5-2 | **OCR 화면 텍스트 추출** ✅ | `ocr` 프리픽스 + F4 글로벌 단축키. RegionSelectWindow 재사용, Windows.Media.Ocr 로컬 엔진. 결과 → 클립보드 복사 + 런처 입력창 자동 채움 | 높음 |
|
||||
| L5-3 | **QuickLook 인라인 편집** ✅ | F3 미리보기 → Ctrl+E 편집 모드 토글. 텍스트/코드 전체 읽기(300줄 제한 없음). Ctrl+S 저장, ● 수정 마커, Esc 취소 확인, 저장 후 미리보기 새로고침 | 중간 |
|
||||
| L5-4 | **앱 세션 스냅** | 여러 앱을 지정 레이아웃으로 한번에 열기. `snap 세션이름` → 등록된 앱 목록을 각 레이아웃에 배치 | 중간 |
|
||||
| L5-4 | **앱 세션 스냅** ✅ | `session` 프리픽스. `AppSession/SessionApp` 모델 추가 + `AppSettings.AppSessions` 저장. `SessionHandler`: 목록·실행·new/edit/del 서브커맨드. `SessionEditorWindow`: 세션 이름·설명·앱 행(경로+라벨+스냅 팝업 14종+삭제) 인라인 편집. 실행 시 Process.Start → 창 핸들 대기(6초) → ApplySnapToWindow(P/Invoke SetWindowPos+ShowWindow) | 중간 |
|
||||
| L5-5 | **배치 파일 이름 변경** ✅ | `batchren` 프리픽스로 BatchRenameWindow 오픈. 변수 패턴(`{name}`, `{n:3}`, `{date:format}`, `{ext}`) + 정규식 모드(`/old/new/`). 드래그 앤 드롭·폴더/파일 추가, DataGrid 실시간 미리보기, 충돌 감지(배경 붉은 강조), 확장자 유지 토글, 시작 번호 지정, 적용 후 엔트리 갱신 | 중간 |
|
||||
| L5-6 | **자동화 스케줄러** | `sched` 프리픽스로 시간·앱 기반 트리거 등록. "매일 09:00 = 크롬 열기", "캐치 앱 실행 시 = 알림" | 낮음 |
|
||||
|
||||
|
||||
@@ -182,6 +182,8 @@ public partial class App : System.Windows.Application
|
||||
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());
|
||||
|
||||
|
||||
301
src/AxCopilot/Handlers/SessionHandler.cs
Normal file
301
src/AxCopilot/Handlers/SessionHandler.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L5-4: 앱 세션 스냅 핸들러. "session" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: session → 저장된 세션 목록
|
||||
/// session 개발환경 → 해당 세션 실행 (앱 + 스냅 레이아웃 적용)
|
||||
/// session new 이름 → 새 세션 편집기 열기
|
||||
/// session edit 이름 → 기존 세션 편집기 열기
|
||||
/// session del 이름 → 세션 삭제
|
||||
///
|
||||
/// 세션 = [앱 경로 + 스냅 위치] 목록. 한 번에 모든 앱을 지정 레이아웃으로 실행합니다.
|
||||
/// </summary>
|
||||
public class SessionHandler : IActionHandler
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
|
||||
public SessionHandler(SettingsService settings) { _settings = settings; }
|
||||
|
||||
public string? Prefix => "session";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"AppSession",
|
||||
"앱 세션 스냅 — session",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
// ─── 항목 목록 ──────────────────────────────────────────────────────────
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var parts = q.Split(' ', 2, StringSplitOptions.TrimEntries);
|
||||
var cmd = parts.Length > 0 ? parts[0].ToLowerInvariant() : "";
|
||||
|
||||
// "new [이름]" — 새 세션 만들기
|
||||
if (cmd == "new")
|
||||
{
|
||||
var name = parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]) ? parts[1] : "새 세션";
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
|
||||
{
|
||||
new LauncherItem($"'{name}' 세션 만들기",
|
||||
"편집기에서 앱 목록과 스냅 레이아웃을 설정합니다",
|
||||
null, $"__new__{name}", Symbol: "\uE710")
|
||||
});
|
||||
}
|
||||
|
||||
// "edit 이름" — 기존 세션 편집
|
||||
if (cmd == "edit" && parts.Length > 1)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
|
||||
{
|
||||
new LauncherItem($"'{parts[1]}' 세션 편집",
|
||||
"편집기 열기",
|
||||
null, $"__edit__{parts[1]}", Symbol: "\uE70F")
|
||||
});
|
||||
}
|
||||
|
||||
// "del 이름" or "delete 이름" — 세션 삭제
|
||||
if ((cmd == "del" || cmd == "delete") && parts.Length > 1)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
|
||||
{
|
||||
new LauncherItem($"'{parts[1]}' 세션 삭제",
|
||||
"Enter로 삭제 확인 (되돌릴 수 없습니다)",
|
||||
null, $"__del__{parts[1]}", Symbol: Symbols.Delete)
|
||||
});
|
||||
}
|
||||
|
||||
// 세션 목록 (필터 적용)
|
||||
var sessions = _settings.Settings.AppSessions;
|
||||
var filter = q.ToLowerInvariant();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
foreach (var s in sessions)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filter) &&
|
||||
!s.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) &&
|
||||
!s.Description.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var appNames = string.Join(", ",
|
||||
s.Apps.Take(3).Select(a =>
|
||||
string.IsNullOrEmpty(a.Label)
|
||||
? Path.GetFileNameWithoutExtension(a.Path)
|
||||
: a.Label));
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
s.Name,
|
||||
$"{s.Apps.Count}개 앱 · {appNames}",
|
||||
null, s,
|
||||
Symbol: "\uE8A1")); // 창 레이아웃 아이콘
|
||||
}
|
||||
|
||||
if (items.Count == 0 && string.IsNullOrEmpty(filter))
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
"저장된 세션 없음",
|
||||
"'session new 이름'으로 앱 세션을 만드세요",
|
||||
null, null,
|
||||
Symbol: Symbols.Info));
|
||||
}
|
||||
|
||||
// 새 세션 만들기 (항상 표시)
|
||||
items.Add(new LauncherItem(
|
||||
"새 세션 만들기",
|
||||
"session new [이름] · 앱 목록 + 스냅 레이아웃 지정",
|
||||
null, "__new__새 세션",
|
||||
Symbol: "\uE710"));
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// ─── 실행 ─────────────────────────────────────────────────────────────
|
||||
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
if (item.Data is string s)
|
||||
{
|
||||
if (s.StartsWith("__new__"))
|
||||
{
|
||||
var name = s["__new__".Length..];
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var win = new Views.SessionEditorWindow(null, _settings);
|
||||
win.InitialName = name;
|
||||
win.Show();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (s.StartsWith("__edit__"))
|
||||
{
|
||||
var name = s["__edit__".Length..];
|
||||
var session = _settings.Settings.AppSessions
|
||||
.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var win = new Views.SessionEditorWindow(session, _settings);
|
||||
win.Show();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (s.StartsWith("__del__"))
|
||||
{
|
||||
var name = s["__del__".Length..];
|
||||
var session = _settings.Settings.AppSessions
|
||||
.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
if (session != null)
|
||||
{
|
||||
_settings.Settings.AppSessions.Remove(session);
|
||||
_settings.Save();
|
||||
NotificationService.Notify("AX Copilot", $"세션 '{name}' 삭제됨");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Data is Models.AppSession appSession)
|
||||
await LaunchSessionAsync(appSession, ct);
|
||||
}
|
||||
|
||||
// ─── 세션 실행 로직 ───────────────────────────────────────────────────
|
||||
private static async Task LaunchSessionAsync(Models.AppSession session, CancellationToken ct)
|
||||
{
|
||||
NotificationService.Notify("AX Copilot", $"'{session.Name}' 세션 시작...");
|
||||
LogService.Info($"세션 실행 시작: {session.Name} ({session.Apps.Count}개 앱)");
|
||||
|
||||
int launched = 0, failed = 0;
|
||||
|
||||
foreach (var app in session.Apps)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (app.DelayMs > 0)
|
||||
await Task.Delay(app.DelayMs, ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(app.Path)) continue;
|
||||
|
||||
System.Diagnostics.Process? proc = null;
|
||||
try
|
||||
{
|
||||
proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = app.Path,
|
||||
Arguments = app.Arguments ?? "",
|
||||
UseShellExecute = true
|
||||
});
|
||||
launched++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"세션 앱 실행 실패: {app.Path} — {ex.Message}");
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 스냅 위치 없거나 none → 그냥 실행
|
||||
if (proc == null || string.IsNullOrEmpty(app.SnapPosition) || app.SnapPosition == "none")
|
||||
continue;
|
||||
|
||||
// 창이 나타날 때까지 대기 (최대 6초)
|
||||
var hWnd = IntPtr.Zero;
|
||||
var deadline = DateTime.UtcNow.AddSeconds(6);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
try { proc.Refresh(); } catch { break; }
|
||||
hWnd = proc.MainWindowHandle;
|
||||
if (hWnd != IntPtr.Zero) break;
|
||||
await Task.Delay(200, ct);
|
||||
}
|
||||
|
||||
if (hWnd == IntPtr.Zero)
|
||||
{
|
||||
LogService.Warn($"세션: 창 핸들 획득 실패 ({app.Path})");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 창이 완전히 렌더링될 시간 허용
|
||||
await Task.Delay(250, ct);
|
||||
ApplySnapToWindow(hWnd, app.SnapPosition);
|
||||
}
|
||||
|
||||
var msg = failed > 0
|
||||
? $"'{session.Name}' 실행 완료 ({launched}개 성공, {failed}개 실패)"
|
||||
: $"'{session.Name}' 실행 완료 ({launched}개 앱)";
|
||||
NotificationService.Notify("AX Copilot", msg);
|
||||
LogService.Info($"세션 실행 완료: {msg}");
|
||||
}
|
||||
|
||||
// ─── 스냅 적용 (SnapHandler와 동일한 좌표 계산) ──────────────────────
|
||||
private static void ApplySnapToWindow(IntPtr hwnd, string snapKey)
|
||||
{
|
||||
if (snapKey == "full")
|
||||
{
|
||||
ShowWindow(hwnd, SW_MAXIMIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
var hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
var mi = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() };
|
||||
if (!GetMonitorInfo(hMonitor, ref mi)) return;
|
||||
|
||||
var w = mi.rcWork;
|
||||
int mw = w.right - w.left;
|
||||
int mh = w.bottom - w.top;
|
||||
int mx = w.left;
|
||||
int my = w.top;
|
||||
|
||||
var (x, y, cw, ch) = snapKey switch
|
||||
{
|
||||
"left" => (mx, my, mw / 2, mh),
|
||||
"right" => (mx + mw / 2, my, mw / 2, mh),
|
||||
"top" => (mx, my, mw, mh / 2),
|
||||
"bottom" => (mx, my + mh / 2, mw, mh / 2),
|
||||
"tl" => (mx, my, mw / 2, mh / 2),
|
||||
"tr" => (mx + mw / 2, my, mw / 2, mh / 2),
|
||||
"bl" => (mx, my + mh / 2, mw / 2, mh / 2),
|
||||
"br" => (mx + mw / 2, my + mh / 2, mw / 2, mh / 2),
|
||||
"third-l" => (mx, my, mw / 3, mh),
|
||||
"third-c" => (mx + mw / 3, my, mw / 3, mh),
|
||||
"third-r" => (mx + mw * 2 / 3,my, mw / 3, mh),
|
||||
"two3-l" => (mx, my, mw * 2 / 3, mh),
|
||||
"two3-r" => (mx + mw / 3, my, mw * 2 / 3, mh),
|
||||
"center" => (mx + mw / 10, my + mh / 10, mw * 8 / 10, mh * 8 / 10),
|
||||
_ => (mx, my, mw, mh)
|
||||
};
|
||||
|
||||
ShowWindow(hwnd, SW_RESTORE);
|
||||
SetWindowPos(hwnd, IntPtr.Zero, x, y, cw, ch, SWP_SHOWWINDOW | SWP_NOZORDER);
|
||||
}
|
||||
|
||||
// ─── P/Invoke ─────────────────────────────────────────────────────────
|
||||
private const uint SWP_SHOWWINDOW = 0x0040;
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
|
||||
private const int SW_RESTORE = 9;
|
||||
private const int SW_MAXIMIZE = 3;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RECT { public int left, top, right, bottom; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MONITORINFO
|
||||
{
|
||||
public int cbSize;
|
||||
public RECT rcMonitor, rcWork;
|
||||
public uint dwFlags;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
||||
[DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
[DllImport("user32.dll")] private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
|
||||
[DllImport("user32.dll")] private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
|
||||
}
|
||||
@@ -262,6 +262,54 @@ public class HotkeyAssignment
|
||||
public string Type { get; set; } = "app";
|
||||
}
|
||||
|
||||
// ─── 앱 세션 스냅 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 앱 세션: 여러 앱을 지정 레이아웃으로 한번에 실행하는 세트.
|
||||
/// session 핸들러로 저장/로드/실행합니다.
|
||||
/// </summary>
|
||||
public class AppSession
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("apps")]
|
||||
public List<SessionApp> Apps { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 세션 내 개별 앱 항목. 실행 경로·인자·스냅 위치를 지정합니다.
|
||||
/// </summary>
|
||||
public class SessionApp
|
||||
{
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("args")]
|
||||
public string Arguments { get; set; } = "";
|
||||
|
||||
/// <summary>표시 이름. 비어 있으면 파일명을 사용합니다.</summary>
|
||||
[JsonPropertyName("label")]
|
||||
public string Label { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 스냅 위치. left / right / tl / tr / bl / br / full / center /
|
||||
/// third-l / third-c / third-r / two3-l / two3-r / none
|
||||
/// </summary>
|
||||
[JsonPropertyName("snap")]
|
||||
public string SnapPosition { get; set; } = "full";
|
||||
|
||||
/// <summary>이 앱을 실행하기 전 대기 시간(ms). 기본값 0.</summary>
|
||||
[JsonPropertyName("delayMs")]
|
||||
public int DelayMs { get; set; } = 0;
|
||||
}
|
||||
|
||||
// ─── 잠금 해제 알림 설정 ───────────────────────────────────────────────────────
|
||||
|
||||
public class ReminderSettings
|
||||
|
||||
@@ -112,6 +112,12 @@ public class AppSettings
|
||||
[JsonPropertyName("customHotkeys")]
|
||||
public List<HotkeyAssignment> CustomHotkeys { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 앱 세션 목록. session 핸들러로 여러 앱을 지정 레이아웃에 한번에 실행합니다.
|
||||
/// </summary>
|
||||
[JsonPropertyName("appSessions")]
|
||||
public List<AppSession> AppSessions { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("llm")]
|
||||
public LlmSettings Llm { get; set; } = new();
|
||||
}
|
||||
|
||||
251
src/AxCopilot/Views/SessionEditorWindow.xaml
Normal file
251
src/AxCopilot/Views/SessionEditorWindow.xaml
Normal file
@@ -0,0 +1,251 @@
|
||||
<Window x:Class="AxCopilot.Views.SessionEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Commander — 세션 편집"
|
||||
Width="640" Height="540"
|
||||
MinWidth="500" MinHeight="400"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Margin="6">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="22" ShadowDepth="4" Opacity="0.32" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/> <!-- 타이틀바 -->
|
||||
<RowDefinition Height="Auto"/> <!-- 세션 정보 -->
|
||||
<RowDefinition Height="*"/> <!-- 앱 목록 -->
|
||||
<RowDefinition Height="Auto"/> <!-- 하단 버튼 -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ─── 타이틀바 ─────────────────────────────────────────────── -->
|
||||
<Border Grid.Row="0" CornerRadius="12,12,0,0"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseDown">
|
||||
<Grid Margin="14,0,8,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="15"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center" Margin="0,1,10,0"/>
|
||||
<TextBlock Text="세션 편집"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Border HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
CornerRadius="4" Padding="8,4" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClose_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#40C05050"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 세션 이름·설명 ───────────────────────────────────────── -->
|
||||
<Border Grid.Row="1"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,1"
|
||||
Padding="14,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Text="세션 이름" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,3"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="2"
|
||||
Text="설명 (선택)" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,3"/>
|
||||
|
||||
<TextBox Grid.Row="2" Grid.Column="0"
|
||||
x:Name="NameBox"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Padding="8,5"
|
||||
VerticalContentAlignment="Center"/>
|
||||
|
||||
<TextBox Grid.Row="2" Grid.Column="2"
|
||||
x:Name="DescBox"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Padding="8,5"
|
||||
VerticalContentAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 앱 목록 ──────────────────────────────────────────────── -->
|
||||
<Grid Grid.Row="2">
|
||||
<!-- 컬럼 헤더 -->
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="30"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,0,0,1"
|
||||
Padding="14,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/> <!-- 경로 -->
|
||||
<ColumnDefinition Width="100"/> <!-- 라벨 -->
|
||||
<ColumnDefinition Width="108"/> <!-- 스냅 -->
|
||||
<ColumnDefinition Width="30"/> <!-- 삭제 -->
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="앱 경로" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="라벨" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Text="스냅 위치" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 앱 행 목록 -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="AppListPanel" Margin="0,0,0,4">
|
||||
<!-- 행은 코드에서 동적으로 생성 -->
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 빈 상태 -->
|
||||
<StackPanel x:Name="EmptyState"
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="Visible">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="40"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" Opacity="0.3"/>
|
||||
<TextBlock Text="앱을 추가하세요"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,10,0,0" Opacity="0.5"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── 스냅 선택 팝업 (공유, 코드에서 위치 지정) ──────────────
|
||||
PlacementTarget은 코드에서 동적으로 설정합니다. -->
|
||||
<Popup x:Name="SnapPickerPopup"
|
||||
Placement="Bottom"
|
||||
AllowsTransparency="True"
|
||||
StaysOpen="False">
|
||||
<Border Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
CornerRadius="8" Padding="8,6"
|
||||
MinWidth="160">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.3" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<ContentControl x:Name="SnapOptionsList"/>
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
<!-- ─── 하단 버튼 바 ─────────────────────────────────────────── -->
|
||||
<Border Grid.Row="3" CornerRadius="0,0,12,12"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Padding="12,8">
|
||||
<Grid>
|
||||
<!-- 좌측: 앱 추가 버튼 -->
|
||||
<Border HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
CornerRadius="4" Padding="12,5" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnAddApp_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="#5CB85C" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="앱 추가" FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 우측: 취소 + 저장 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Border CornerRadius="4" Padding="14,5" Cursor="Hand" Margin="0,0,8,0"
|
||||
MouseLeftButtonUp="BtnCancel_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="취소" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BtnSave"
|
||||
CornerRadius="4" Padding="16,5" Cursor="Hand"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
MouseLeftButtonUp="BtnSave_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentColor}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="0.85"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="White" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="저장" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="White"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
386
src/AxCopilot/Views/SessionEditorWindow.xaml.cs
Normal file
386
src/AxCopilot/Views/SessionEditorWindow.xaml.cs
Normal file
@@ -0,0 +1,386 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// L5-4: 앱 세션 편집기.
|
||||
/// 세션 이름, 앱 목록(경로 + 라벨 + 스냅 위치)을 편집하여 저장합니다.
|
||||
/// </summary>
|
||||
public partial class SessionEditorWindow : Window
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
private readonly AppSession? _original; // 편집 모드 원본 (새 세션이면 null)
|
||||
private readonly List<AppRowUi> _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", "스냅 없음"),
|
||||
];
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 세션 편집기를 엽니다.
|
||||
/// </summary>
|
||||
/// <param name="session">편집할 기존 세션. null이면 새로 만들기 모드.</param>
|
||||
/// <param name="settings">설정 서비스.</param>
|
||||
public SessionEditorWindow(AppSession? session, SettingsService settings)
|
||||
{
|
||||
InitializeComponent();
|
||||
_settings = settings;
|
||||
_original = session;
|
||||
|
||||
BuildSnapPopup();
|
||||
LoadSession(session);
|
||||
}
|
||||
|
||||
/// <summary>새 세션 모드일 때 기본 이름을 설정합니다.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user