[Phase L5-3] QuickLook 인라인 편집 기능 구현
QuickLookWindow.xaml: - 타이틀바 우측에 ✏ 편집 버튼(BtnEdit) 추가 (텍스트 파일일 때만 Visible) - 닫기 버튼과 StackPanel으로 묶음, 호버 시 #28FFFFFF 배경 - TextEditBox (TextBox) 추가 — TextScrollViewer와 동일 위치(Grid.Row=1) - Cascadia Code 12px, Border 없음, AcceptsReturn/Tab, 기본 Collapsed - TextChanged → TextEditBox_TextChanged 이벤트 연결 QuickLookWindow.xaml.cs: - 편집 상태 필드: _currentFilePath, _currentFileExt, _isEditMode, _isModified, _suppressTextChanged - LoadPreview() 수정: - _currentFilePath/_currentFileExt 저장 - TextExts 파일일 때만 BtnEdit.Visibility=Visible - BtnEdit_Click / BtnEdit_Click → ToggleEditMode() - TextEditBox_TextChanged → _isModified=true, 파일명에 "● " 마커 추가 - OnKeyDown 확장: - 편집 모드: Ctrl+S(저장), Ctrl+E(토글), Esc(미저장 확인) - 미리보기 모드: Ctrl+E(편집 모드 진입) - BtnClose_Click: 미저장 확인 후 닫기 - EnterEditMode(): 파일 전체 읽기(300줄 제한 없음), TextEditBox 표시, 아이콘→👁, 상태바 힌트 - ExitEditMode(): TextScrollViewer 복원, 아이콘→✏, ReloadPreview() 호출 - SaveEditedFile(): File.WriteAllText(UTF8), ● 마커 제거, 상태바 갱신, 완료 알림 - ReloadPreview(): 저장 후 PreviewText 새로고침 (편집 내용 반영) HelpDetailWindow.Shortcuts.cs: - "기타 창" 카테고리에 "Ctrl+E (QuickLook)" 및 "Ctrl+S (QuickLook 편집 중)" 단축키 추가 docs/LAUNCHER_ROADMAP.md: - L5-3 ✅ 완료 표시 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -131,7 +131,7 @@
|
||||
|---|------|------|----------|
|
||||
| L5-1 | **항목별 전용 핫키** ✅ | 앱·URL·폴더에 `Ctrl+Alt+숫자` 등 글로벌 단축키 직접 할당. `hotkey` 프리픽스로 관리. `HotkeyAssignment` 모델 + `InputListener` 확장 + 설정창 "전용 핫키" 탭 | 높음 |
|
||||
| L5-2 | **OCR 화면 텍스트 추출** ✅ | `ocr` 프리픽스 + F4 글로벌 단축키. RegionSelectWindow 재사용, Windows.Media.Ocr 로컬 엔진. 결과 → 클립보드 복사 + 런처 입력창 자동 채움 | 높음 |
|
||||
| L5-3 | **QuickLook 인라인 편집** | F3 미리보기에서 텍스트·마크다운 파일 직접 편집 + Ctrl+S 저장. 변경 감지(수정 표시 `●`), Esc 취소 | 중간 |
|
||||
| L5-3 | **QuickLook 인라인 편집** ✅ | F3 미리보기 → Ctrl+E 편집 모드 토글. 텍스트/코드 전체 읽기(300줄 제한 없음). Ctrl+S 저장, ● 수정 마커, Esc 취소 확인, 저장 후 미리보기 새로고침 | 중간 |
|
||||
| L5-4 | **앱 세션 스냅** | 여러 앱을 지정 레이아웃으로 한번에 열기. `snap 세션이름` → 등록된 앱 목록을 각 레이아웃에 배치 | 중간 |
|
||||
| L5-5 | **배치 파일 이름 변경** | 다중 선택 후 `rename {패턴}` → 넘버링·날짜·정규식 치환 미리보기 → 일괄 적용 | 중간 |
|
||||
| L5-6 | **자동화 스케줄러** | `sched` 프리픽스로 시간·앱 기반 트리거 등록. "매일 09:00 = 크롬 열기", "캐치 앱 실행 시 = 알림" | 낮음 |
|
||||
|
||||
@@ -72,6 +72,16 @@ public partial class HelpDetailWindow
|
||||
"화면 영역 텍스트 추출 (OCR)",
|
||||
"런처를 닫고 화면 드래그 영역 선택 모드를 즉시 실행합니다. 선택한 영역의 텍스트를 자동으로 인식해 클립보드에 복사하고 런처 입력창에 채웁니다.",
|
||||
"\uE8D2", "#0F766E"));
|
||||
|
||||
// ── QuickLook 편집 ─────────────────────────────────────────────────
|
||||
items.Add(MakeShortcut("기타 창", "Ctrl + E (QuickLook)",
|
||||
"QuickLook 편집 모드 전환",
|
||||
"F3 미리보기 창에서 텍스트/코드 파일을 직접 편집할 수 있습니다. 편집 모드에서 Ctrl+S로 저장, Esc로 취소합니다. 이미지·PDF·Office 파일은 편집 불가입니다.",
|
||||
"\uE70F", "#6B2C91"));
|
||||
items.Add(MakeShortcut("기타 창", "Ctrl + S (QuickLook 편집 중)",
|
||||
"편집 내용 즉시 저장",
|
||||
"QuickLook 편집 모드에서 현재 편집 내용을 파일에 저장합니다. 저장 완료 시 알림이 표시됩니다.",
|
||||
"\uE74E", "#059669"));
|
||||
items.Add(MakeShortcut("런처 기능", "F1",
|
||||
"도움말 창 열기",
|
||||
"이 화면을 직접 엽니다. 'help' 를 입력하는 것과 동일합니다.",
|
||||
|
||||
@@ -44,9 +44,34 @@
|
||||
MaxWidth="270"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 닫기 버튼 -->
|
||||
<Border HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
<!-- 우측 버튼 그룹 -->
|
||||
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<!-- 편집 버튼 (텍스트 파일일 때만 표시) -->
|
||||
<Border x:Name="BtnEdit"
|
||||
CornerRadius="4" Padding="8,4" Cursor="Hand"
|
||||
Visibility="Collapsed" Margin="0,0,2,0"
|
||||
ToolTip="편집 모드 전환 (Ctrl+E)"
|
||||
MouseLeftButtonUp="BtnEdit_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock x:Name="BtnEditIcon"
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
|
||||
<!-- 닫기 버튼 -->
|
||||
<Border CornerRadius="4" Padding="8,4" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClose_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
@@ -61,6 +86,7 @@
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -113,6 +139,23 @@
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 인라인 편집 TextBox (편집 모드에서만 표시) -->
|
||||
<TextBox x:Name="TextEditBox"
|
||||
FontFamily="Cascadia Code, Consolas, Courier New"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderThickness="0"
|
||||
Padding="14,12"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
TextWrapping="NoWrap"
|
||||
SpellCheck.IsEnabled="False"
|
||||
Visibility="Collapsed"
|
||||
TextChanged="TextEditBox_TextChanged"/>
|
||||
|
||||
<!-- PDF 미리보기 (텍스트 추출) -->
|
||||
<ScrollViewer x:Name="PdfScrollViewer"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
|
||||
@@ -54,6 +54,14 @@ public partial class QuickLookWindow : Window
|
||||
private static readonly HashSet<string> ExcelExts = new(StringComparer.OrdinalIgnoreCase)
|
||||
{ ".xlsx", ".xls" };
|
||||
|
||||
// ─── 편집 상태 ────────────────────────────────────────────────────────────
|
||||
|
||||
private string _currentFilePath = ""; // 현재 표시 중인 파일 경로
|
||||
private string _currentFileExt = ""; // 현재 파일 확장자
|
||||
private bool _isEditMode; // 편집 모드 활성 여부
|
||||
private bool _isModified; // 미저장 변경 있음
|
||||
private bool _suppressTextChanged; // TextChanged 이벤트 억제 플래그
|
||||
|
||||
// ─── 생성 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public QuickLookWindow(string path, Window owner)
|
||||
@@ -68,6 +76,50 @@ public partial class QuickLookWindow : Window
|
||||
|
||||
private void OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
// ─ 편집 모드 단축키 ─────────────────────────────────────────
|
||||
if (_isEditMode)
|
||||
{
|
||||
// Ctrl+S → 저장
|
||||
if (e.Key == Key.S && Keyboard.Modifiers == ModifierKeys.Control)
|
||||
{
|
||||
SaveEditedFile();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
// Ctrl+E → 편집 모드 토글
|
||||
if (e.Key == Key.E && Keyboard.Modifiers == ModifierKeys.Control)
|
||||
{
|
||||
ToggleEditMode();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
// Esc → 편집 모드 종료 (미저장 확인)
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
if (_isModified)
|
||||
{
|
||||
var result = CustomMessageBox.Show(
|
||||
"저장하지 않은 변경 내용이 있습니다.\n편집을 취소하고 미리보기로 돌아갈까요?",
|
||||
"AX Copilot — 편집 취소",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) { e.Handled = true; return; }
|
||||
}
|
||||
ExitEditMode();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
return; // 편집 모드에서 F3 등은 무시 (TextBox가 처리)
|
||||
}
|
||||
|
||||
// ─ 미리보기 모드 단축키 ─────────────────────────────────────
|
||||
if (e.Key == Key.E && Keyboard.Modifiers == ModifierKeys.Control)
|
||||
{
|
||||
ToggleEditMode();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key is Key.Escape or Key.F3)
|
||||
{
|
||||
Close();
|
||||
@@ -80,7 +132,162 @@ public partial class QuickLookWindow : Window
|
||||
if (e.LeftButton == MouseButtonState.Pressed) DragMove();
|
||||
}
|
||||
|
||||
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close();
|
||||
private void BtnClose_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (_isEditMode && _isModified)
|
||||
{
|
||||
var result = CustomMessageBox.Show(
|
||||
"저장하지 않은 변경 내용이 있습니다.\n저장하지 않고 닫을까요?",
|
||||
"AX Copilot — 저장 확인",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
// ─── 편집 버튼 클릭 ─────────────────────────────────────────────────────
|
||||
|
||||
private void BtnEdit_Click(object sender, MouseButtonEventArgs e) => ToggleEditMode();
|
||||
|
||||
// ─── 편집 텍스트 변경 감지 ───────────────────────────────────────────────
|
||||
|
||||
private void TextEditBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
if (_suppressTextChanged) return;
|
||||
if (!_isModified)
|
||||
{
|
||||
_isModified = true;
|
||||
// 타이틀에 ● 수정 마커 추가
|
||||
if (!FileNameText.Text.StartsWith("● "))
|
||||
FileNameText.Text = "● " + FileNameText.Text;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 편집 모드 진입/종료 ─────────────────────────────────────────────────
|
||||
|
||||
private void ToggleEditMode()
|
||||
{
|
||||
if (!_isEditMode)
|
||||
EnterEditMode();
|
||||
else
|
||||
{
|
||||
if (_isModified)
|
||||
{
|
||||
var result = CustomMessageBox.Show(
|
||||
"저장하지 않은 변경 내용이 있습니다.\n저장하고 미리보기로 돌아갈까요?",
|
||||
"AX Copilot — 편집 모드 종료",
|
||||
MessageBoxButton.YesNoCancel,
|
||||
MessageBoxImage.Question);
|
||||
if (result == MessageBoxResult.Cancel) return;
|
||||
if (result == MessageBoxResult.Yes) SaveEditedFile();
|
||||
}
|
||||
ExitEditMode();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnterEditMode()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_currentFilePath) || !TextExts.Contains(_currentFileExt)) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 파일 전체 내용 읽기 (미리보기의 300줄 제한 없이)
|
||||
string fullContent;
|
||||
try { fullContent = File.ReadAllText(_currentFilePath, Encoding.UTF8); }
|
||||
catch { fullContent = File.ReadAllText(_currentFilePath); }
|
||||
|
||||
_suppressTextChanged = true;
|
||||
TextEditBox.Text = fullContent;
|
||||
_suppressTextChanged = false;
|
||||
|
||||
// UI 전환: 미리보기 → 편집
|
||||
TextScrollViewer.Visibility = Visibility.Collapsed;
|
||||
TextEditBox.Visibility = Visibility.Visible;
|
||||
TextEditBox.Focus();
|
||||
|
||||
// 편집 버튼 아이콘 → 👁 (보기 모드로 전환 의미)
|
||||
BtnEditIcon.Text = "\uE890"; // Eye / View icon
|
||||
BtnEdit.ToolTip = "미리보기 모드로 전환 (Ctrl+E)";
|
||||
BtnEditIcon.Foreground = TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(75, 94, 252));
|
||||
|
||||
_isEditMode = true;
|
||||
_isModified = false;
|
||||
|
||||
// 상태 힌트를 하단 FooterMeta에 추가
|
||||
FooterMeta.Text = FooterMeta.Text + " · ✏ 편집 모드 (Ctrl+S 저장 · Esc 취소)";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Warn($"편집 모드 진입 실패: {ex.Message}");
|
||||
Services.NotificationService.Notify("AX Copilot", $"파일을 열 수 없습니다: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitEditMode()
|
||||
{
|
||||
TextEditBox.Visibility = Visibility.Collapsed;
|
||||
TextScrollViewer.Visibility = Visibility.Visible;
|
||||
|
||||
// 편집 버튼 아이콘 복원 → ✏
|
||||
BtnEditIcon.Text = "\uE70F";
|
||||
BtnEdit.ToolTip = "편집 모드 전환 (Ctrl+E)";
|
||||
BtnEditIcon.Foreground = TryFindResource("SecondaryText") as Brush
|
||||
?? Brushes.Gray;
|
||||
|
||||
_isEditMode = false;
|
||||
_isModified = false;
|
||||
|
||||
// 미리보기 다시 로드 (편집된 내용 반영)
|
||||
ReloadPreview();
|
||||
}
|
||||
|
||||
private void SaveEditedFile()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_currentFilePath)) return;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(_currentFilePath, TextEditBox.Text, Encoding.UTF8);
|
||||
_isModified = false;
|
||||
// ● 마커 제거
|
||||
if (FileNameText.Text.StartsWith("● "))
|
||||
FileNameText.Text = FileNameText.Text[2..];
|
||||
|
||||
// 하단 상태 갱신
|
||||
var info = new FileInfo(_currentFilePath);
|
||||
FooterMeta.Text = $"{FormatSize(info.Length)} · {info.LastWriteTime:yyyy-MM-dd HH:mm} · ✏ 편집 모드 (Ctrl+S 저장 · Esc 취소)";
|
||||
|
||||
Services.NotificationService.Notify("저장 완료", $"{Path.GetFileName(_currentFilePath)} 저장됨");
|
||||
Services.LogService.Info($"인라인 편집 저장: {_currentFilePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Error($"파일 저장 실패: {ex.Message}");
|
||||
CustomMessageBox.Show(
|
||||
$"파일을 저장하지 못했습니다.\n{ex.Message}",
|
||||
"저장 오류",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadPreview()
|
||||
{
|
||||
// 편집 후 미리보기 새로고침 (변경된 내용 반영)
|
||||
if (string.IsNullOrEmpty(_currentFilePath) || !File.Exists(_currentFilePath)) return;
|
||||
|
||||
// 파일명 ● 마커 제거
|
||||
if (FileNameText.Text.StartsWith("● "))
|
||||
FileNameText.Text = FileNameText.Text[2..];
|
||||
|
||||
// 상태 바 복원
|
||||
var info = new FileInfo(_currentFilePath);
|
||||
FooterMeta.Text = $"{FormatSize(info.Length)} · {info.LastWriteTime:yyyy-MM-dd HH:mm}";
|
||||
|
||||
// 텍스트 뷰 갱신
|
||||
LoadTextPreview(_currentFilePath, _currentFileExt);
|
||||
}
|
||||
|
||||
// ─── 미리보기 로드 ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -105,22 +312,44 @@ public partial class QuickLookWindow : Window
|
||||
var ext = Path.GetExtension(path);
|
||||
var info = new FileInfo(path);
|
||||
|
||||
// 편집 상태 저장
|
||||
_currentFilePath = path;
|
||||
_currentFileExt = ext;
|
||||
|
||||
FooterPath.Text = path;
|
||||
FooterMeta.Text = $"{FormatSize(info.Length)} · {info.LastWriteTime:yyyy-MM-dd HH:mm}";
|
||||
|
||||
if (ImageExts.Contains(ext))
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Collapsed;
|
||||
LoadImagePreview(path, info);
|
||||
}
|
||||
else if (PdfExts.Contains(ext))
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Collapsed;
|
||||
LoadPdfPreview(path, info);
|
||||
}
|
||||
else if (WordExts.Contains(ext))
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Collapsed;
|
||||
LoadWordPreview(path, info);
|
||||
}
|
||||
else if (ExcelExts.Contains(ext))
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Collapsed;
|
||||
LoadExcelPreview(path, info);
|
||||
}
|
||||
else if (TextExts.Contains(ext))
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Visible; // ✏ 편집 버튼 표시
|
||||
LoadTextPreview(path, ext);
|
||||
}
|
||||
else
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Collapsed;
|
||||
LoadFileInfo(path, ext, info);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowInfo("\uE783", $"미리보기 오류", ex.Message);
|
||||
|
||||
Reference in New Issue
Block a user