AX Commander 비교본 런처 기능 대량 이식
변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
This commit is contained in:
546
src/AxCopilot/Views/BatchRenameWindow.xaml
Normal file
546
src/AxCopilot/Views/BatchRenameWindow.xaml
Normal file
@@ -0,0 +1,546 @@
|
||||
<Window x:Class="AxCopilot.Views.BatchRenameWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Commander — 배치 파일 이름변경"
|
||||
Width="780" Height="580"
|
||||
MinWidth="560" MinHeight="420"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
ShowInTaskbar="False"
|
||||
AllowDrop="True">
|
||||
|
||||
<Window.Resources>
|
||||
<!-- 원본 열 텍스트 스타일 -->
|
||||
<Style x:Key="OrigColStyle" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="12,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryText}"/>
|
||||
</Style>
|
||||
<!-- 새 이름 열 텍스트 스타일 -->
|
||||
<Style x:Key="NewColStyle" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="12,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryText}"/>
|
||||
</Style>
|
||||
<!-- 상태 열 텍스트 스타일 -->
|
||||
<Style x:Key="StatusColStyle" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="8,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<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"/>
|
||||
<TextBlock x:Name="FileCountBadge" Text=""
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0,0,0"/>
|
||||
</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.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 패턴 입력 행 -->
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0" Text="패턴"
|
||||
FontSize="12" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<TextBox Grid.Column="1" x:Name="PatternBox"
|
||||
FontFamily="Cascadia Code, Consolas, Courier New"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Padding="8,5"
|
||||
VerticalContentAlignment="Center"
|
||||
Text="{}{n}_{name}"
|
||||
TextChanged="PatternBox_TextChanged"/>
|
||||
|
||||
<!-- 변수 힌트 버튼 -->
|
||||
<Border Grid.Column="2" x:Name="BtnHint"
|
||||
Margin="6,0,0,0"
|
||||
CornerRadius="4" Padding="10,5" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnHint_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="11"
|
||||
Foreground="{DynamicResource AccentColor}"/>
|
||||
</Border>
|
||||
|
||||
<!-- 힌트 팝업 -->
|
||||
<Popup x:Name="HintPopup"
|
||||
Grid.Column="2"
|
||||
PlacementTarget="{x:Reference BtnHint}"
|
||||
Placement="Bottom"
|
||||
AllowsTransparency="True"
|
||||
StaysOpen="False">
|
||||
<Border Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
CornerRadius="8" Padding="14,10"
|
||||
MinWidth="280">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.3" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<StackPanel>
|
||||
<TextBlock Text="사용 가능한 변수"
|
||||
FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Margin="0,0,0,8"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="22"/>
|
||||
</Grid.RowDefinitions>
|
||||
<!-- 변수 목록 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{}{name}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="원본 파일명 (확장자 제외)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="{}{n}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="순번 (1, 2, 3 …)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="{}{n:3}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="세 자리 순번 (001, 002 …)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="{}{date}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Text="오늘 날짜 (yyyy-MM-dd)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="{}{date:yyyyMMdd}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Text="날짜 (형식 직접 지정)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Text="{}{ext}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" Text="원본 확장자 (점 제외)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<Border Grid.Row="6" Grid.ColumnSpan="2"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0" Margin="0,2"/>
|
||||
|
||||
<TextBlock Grid.Row="7" Grid.Column="0" Text="/old/new/"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="#F59E0B" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="7" Grid.Column="1" Text="정규식 모드: 패턴 치환"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
|
||||
<!-- 옵션 행 -->
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0" Text="모드"
|
||||
FontSize="12" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- 모드 세그먼트 -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border x:Name="BtnModeVar"
|
||||
CornerRadius="4,0,0,4" Padding="12,4"
|
||||
Cursor="Hand" MouseLeftButtonUp="BtnModeVar_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentColor}"/>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock x:Name="BtnModeVarText" Text="변수 패턴"
|
||||
FontSize="11" Foreground="White"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnModeRegex"
|
||||
CornerRadius="0,4,4,0" Padding="12,4"
|
||||
Cursor="Hand" MouseLeftButtonUp="BtnModeRegex_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 x:Name="BtnModeRegexText" Text="정규식"
|
||||
FontSize="11" Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="시작 번호"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,6,0"/>
|
||||
<TextBox x:Name="StartNumberBox"
|
||||
Width="52" Text="1"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Padding="6,3"
|
||||
VerticalContentAlignment="Center"
|
||||
TextChanged="StartNumberBox_TextChanged"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 확장자 유지 토글 -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="확장자 유지"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"/>
|
||||
<Border x:Name="ExtToggle"
|
||||
Width="36" Height="20" CornerRadius="10"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
Cursor="Hand" MouseLeftButtonUp="ExtToggle_Click">
|
||||
<Border x:Name="ExtThumb"
|
||||
Width="16" Height="16" CornerRadius="8"
|
||||
Background="White"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,1,0"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 미리보기 그리드 ──────────────────────────────────────── -->
|
||||
<Grid Grid.Row="2">
|
||||
|
||||
<!-- 드롭 힌트 오버레이 -->
|
||||
<Border x:Name="DropHintOverlay"
|
||||
Background="#18FFFFFF"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="40"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="파일을 여기에 끌어다 놓으세요"
|
||||
FontSize="14" FontWeight="Medium"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,10,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 빈 상태 안내 -->
|
||||
<StackPanel x:Name="EmptyState"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="Visible">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="44"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" Opacity="0.3"/>
|
||||
<TextBlock Text="파일을 추가하세요"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,12,0,4" Opacity="0.6"/>
|
||||
<TextBlock Text="하단 버튼 또는 드래그 앤 드롭으로 파일이나 폴더를 추가합니다"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" Opacity="0.4"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- DataGrid 미리보기 -->
|
||||
<DataGrid x:Name="PreviewGrid"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Extended"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HeadersVisibility="Column"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeRows="False"
|
||||
CanUserSortColumns="False"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
RowBackground="Transparent"
|
||||
AlternatingRowBackground="#07FFFFFF"
|
||||
BorderThickness="0"
|
||||
FontSize="12"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Visibility="Collapsed">
|
||||
<DataGrid.Resources>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="{DynamicResource ItemBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryText}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Padding" Value="12,6"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
</Style>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="MinHeight" Value="30"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#14FFFFFF"/>
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding HasConflict}" Value="True">
|
||||
<Setter Property="Background" Value="#18EF5350"/>
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#20AccentColor"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
</Style>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="원본 파일명"
|
||||
Binding="{Binding OriginalName}"
|
||||
Width="*"
|
||||
ElementStyle="{StaticResource OrigColStyle}"/>
|
||||
<DataGridTextColumn Header="새 파일명"
|
||||
Binding="{Binding NewName}"
|
||||
Width="*"
|
||||
ElementStyle="{StaticResource NewColStyle}"/>
|
||||
<DataGridTextColumn Header="상태"
|
||||
Binding="{Binding StatusText}"
|
||||
Width="72"
|
||||
ElementStyle="{StaticResource StatusColStyle}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── 하단 버튼 바 ─────────────────────────────────────────── -->
|
||||
<Border Grid.Row="3" CornerRadius="0,0,12,12"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Padding="12,8">
|
||||
<Grid>
|
||||
<!-- 좌측: 파일 조작 버튼 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
|
||||
<!-- 폴더 추가 -->
|
||||
<Border CornerRadius="4" Padding="10,5" Cursor="Hand" Margin="0,0,6,0"
|
||||
MouseLeftButtonUp="BtnAddFolder_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="#5C9BD6" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="폴더 추가" FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 파일 추가 -->
|
||||
<Border CornerRadius="4" Padding="10,5" Cursor="Hand" Margin="0,0,6,0"
|
||||
MouseLeftButtonUp="BtnAddFiles_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,5,0"/>
|
||||
<TextBlock Text="파일 추가" FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 선택 제거 -->
|
||||
<Border CornerRadius="4" Padding="10,5" Cursor="Hand" Margin="0,0,6,0"
|
||||
MouseLeftButtonUp="BtnRemoveSelected_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="#EF5350" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="선택 제거" FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 전체 제거 -->
|
||||
<Border CornerRadius="4" Padding="10,5" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClearAll_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="{DynamicResource SecondaryText}" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="전체 제거" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 우측: 충돌 수 + 적용 버튼 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="ConflictLabel" Text=""
|
||||
FontSize="11"
|
||||
Foreground="#EF5350"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,14,0"/>
|
||||
|
||||
<Border x:Name="BtnApply"
|
||||
CornerRadius="4" Padding="16,6" Cursor="Hand"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
MouseLeftButtonUp="BtnApply_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>
|
||||
426
src/AxCopilot/Views/BatchRenameWindow.xaml.cs
Normal file
426
src/AxCopilot/Views/BatchRenameWindow.xaml.cs
Normal file
@@ -0,0 +1,426 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// L5-5: 배치 파일 이름변경 창.
|
||||
/// 여러 파일을 변수 패턴 또는 정규식으로 미리보기 후 일괄 적용합니다.
|
||||
/// </summary>
|
||||
public partial class BatchRenameWindow : Window
|
||||
{
|
||||
private readonly ObservableCollection<RenameEntry> _entries = new();
|
||||
private bool _keepExt = true;
|
||||
private bool _regexMode = false;
|
||||
private int _startNumber = 1;
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
public BatchRenameWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
PreviewGrid.ItemsSource = _entries;
|
||||
|
||||
// 항목 변경 → 카운트 배지 + 빈 상태 갱신
|
||||
_entries.CollectionChanged += (_, _) => RefreshUI();
|
||||
|
||||
// 드래그 앤 드롭
|
||||
Drop += Window_Drop;
|
||||
DragOver += Window_DragOver;
|
||||
DragLeave += (_, _) => DropHintOverlay.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// ─── 외부에서 파일 목록 주입 ─────────────────────────────────────────
|
||||
public void AddFiles(IEnumerable<string> paths)
|
||||
{
|
||||
foreach (var p in paths)
|
||||
{
|
||||
if (!File.Exists(p)) continue;
|
||||
if (_entries.Any(e => string.Equals(e.OriginalPath, p, StringComparison.OrdinalIgnoreCase))) continue;
|
||||
_entries.Add(new RenameEntry
|
||||
{
|
||||
OriginalPath = p,
|
||||
OriginalName = Path.GetFileName(p)
|
||||
});
|
||||
}
|
||||
UpdatePreviews();
|
||||
}
|
||||
|
||||
// ─── 패턴 엔진 ───────────────────────────────────────────────────────
|
||||
private static string ApplyPattern(
|
||||
string pattern,
|
||||
string originalName,
|
||||
string ext,
|
||||
int index,
|
||||
int startNum,
|
||||
bool keepExt,
|
||||
bool regexMode)
|
||||
{
|
||||
if (regexMode)
|
||||
{
|
||||
// /regex/replacement/ 형식
|
||||
var m = Regex.Match(pattern, @"^/(.+)/([^/]*)/?$");
|
||||
if (m.Success)
|
||||
{
|
||||
var rxPat = m.Groups[1].Value;
|
||||
var repl = m.Groups[2].Value;
|
||||
try
|
||||
{
|
||||
var result = Regex.Replace(originalName, rxPat, repl);
|
||||
if (keepExt && !string.IsNullOrEmpty(ext) &&
|
||||
!result.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
|
||||
result += ext;
|
||||
return result;
|
||||
}
|
||||
catch { return "⚠ 정규식 오류"; }
|
||||
}
|
||||
// 패턴 불완전 → 원본 그대로
|
||||
return originalName + ext;
|
||||
}
|
||||
|
||||
// 변수 모드
|
||||
var n = index + startNum;
|
||||
var newName = pattern;
|
||||
|
||||
// {n:자릿수} — 자릿수 패딩
|
||||
newName = Regex.Replace(newName, @"\{n:(\d+)\}", rm =>
|
||||
{
|
||||
if (int.TryParse(rm.Groups[1].Value, out var digits))
|
||||
return n.ToString($"D{digits}");
|
||||
return n.ToString();
|
||||
});
|
||||
|
||||
// {date:format}
|
||||
newName = Regex.Replace(newName, @"\{date:([^}]+)\}", rm =>
|
||||
{
|
||||
try { return DateTime.Today.ToString(rm.Groups[1].Value); }
|
||||
catch { return DateTime.Today.ToString("yyyy-MM-dd"); }
|
||||
});
|
||||
|
||||
newName = newName
|
||||
.Replace("{n}", n.ToString())
|
||||
.Replace("{name}", originalName)
|
||||
.Replace("{orig}", originalName)
|
||||
.Replace("{date}", DateTime.Today.ToString("yyyy-MM-dd"))
|
||||
.Replace("{ext}", ext.TrimStart('.'));
|
||||
|
||||
// 확장자 유지
|
||||
if (keepExt && !string.IsNullOrEmpty(ext))
|
||||
{
|
||||
if (!Path.HasExtension(newName))
|
||||
newName += ext;
|
||||
}
|
||||
|
||||
return newName;
|
||||
}
|
||||
|
||||
private void UpdatePreviews()
|
||||
{
|
||||
var pattern = PatternBox?.Text ?? "{n}_{name}";
|
||||
var startNum = _startNumber;
|
||||
var keepExt = _keepExt;
|
||||
var regexMode = _regexMode;
|
||||
|
||||
// 새 이름 계산
|
||||
for (int i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
var entry = _entries[i];
|
||||
var ext = Path.GetExtension(entry.OriginalPath);
|
||||
var nameNoExt = Path.GetFileNameWithoutExtension(entry.OriginalPath);
|
||||
entry.NewName = ApplyPattern(pattern, nameNoExt, ext, i, startNum, keepExt, regexMode);
|
||||
}
|
||||
|
||||
// 충돌 감지 (같은 폴더 내 같은 새 이름 OR 기존 파일)
|
||||
var grouped = _entries
|
||||
.GroupBy(e => Path.Combine(
|
||||
Path.GetDirectoryName(e.OriginalPath) ?? "",
|
||||
e.NewName),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var conflictPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var g in grouped)
|
||||
{
|
||||
if (g.Count() > 1)
|
||||
foreach (var e in g) conflictPaths.Add(e.OriginalPath);
|
||||
}
|
||||
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
var destPath = Path.Combine(
|
||||
Path.GetDirectoryName(entry.OriginalPath) ?? "",
|
||||
entry.NewName);
|
||||
|
||||
// 충돌: 동명 중복 OR 목적지 파일이 이미 존재(원본 제외)
|
||||
var nameConflict = conflictPaths.Contains(entry.OriginalPath);
|
||||
var destExists = File.Exists(destPath) &&
|
||||
!string.Equals(destPath, entry.OriginalPath, StringComparison.OrdinalIgnoreCase);
|
||||
entry.HasConflict = nameConflict || destExists;
|
||||
}
|
||||
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
private void RefreshUI()
|
||||
{
|
||||
var count = _entries.Count;
|
||||
var conflicts = _entries.Count(e => e.HasConflict);
|
||||
|
||||
FileCountBadge.Text = count > 0 ? $"— {count}개 파일" : "";
|
||||
EmptyState.Visibility = count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
PreviewGrid.Visibility = count == 0 ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
ConflictLabel.Text = conflicts > 0 ? $"⚠ 충돌 {conflicts}개" : "";
|
||||
}
|
||||
|
||||
// ─── 타이틀바 ────────────────────────────────────────────────────────
|
||||
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 PatternBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
=> UpdatePreviews();
|
||||
|
||||
private void StartNumberBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
if (int.TryParse(StartNumberBox.Text, out var n) && n >= 0)
|
||||
_startNumber = n;
|
||||
UpdatePreviews();
|
||||
}
|
||||
|
||||
// ─── 모드 선택 ───────────────────────────────────────────────────────
|
||||
private void BtnModeVar_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_regexMode = false;
|
||||
SetModeUI();
|
||||
UpdatePreviews();
|
||||
}
|
||||
|
||||
private void BtnModeRegex_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_regexMode = true;
|
||||
SetModeUI();
|
||||
if (string.IsNullOrWhiteSpace(PatternBox.Text) || !PatternBox.Text.StartsWith('/'))
|
||||
PatternBox.Text = "/(old)/(new)/";
|
||||
UpdatePreviews();
|
||||
}
|
||||
|
||||
private void SetModeUI()
|
||||
{
|
||||
var accent = TryFindResource("AccentColor") as System.Windows.Media.Brush
|
||||
?? System.Windows.Media.Brushes.DodgerBlue;
|
||||
var sec = TryFindResource("SecondaryText") as System.Windows.Media.Brush
|
||||
?? System.Windows.Media.Brushes.Gray;
|
||||
var dimBg = new System.Windows.Media.SolidColorBrush(
|
||||
System.Windows.Media.Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
|
||||
if (_regexMode)
|
||||
{
|
||||
BtnModeVar.Background = dimBg;
|
||||
BtnModeVarText.Foreground = sec;
|
||||
BtnModeRegex.Background = accent;
|
||||
BtnModeRegexText.Foreground = System.Windows.Media.Brushes.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
BtnModeVar.Background = accent;
|
||||
BtnModeVarText.Foreground = System.Windows.Media.Brushes.White;
|
||||
BtnModeRegex.Background = dimBg;
|
||||
BtnModeRegexText.Foreground = sec;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 확장자 유지 토글 ────────────────────────────────────────────────
|
||||
private void ExtToggle_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_keepExt = !_keepExt;
|
||||
|
||||
var accent = TryFindResource("AccentColor") as System.Windows.Media.Brush
|
||||
?? System.Windows.Media.Brushes.DodgerBlue;
|
||||
var gray = new System.Windows.Media.SolidColorBrush(
|
||||
System.Windows.Media.Color.FromArgb(0x40, 0xFF, 0xFF, 0xFF));
|
||||
|
||||
ExtToggle.Background = _keepExt ? accent : gray;
|
||||
ExtThumb.HorizontalAlignment = _keepExt ? HorizontalAlignment.Right : HorizontalAlignment.Left;
|
||||
ExtThumb.Margin = _keepExt ? new Thickness(0, 0, 1, 0) : new Thickness(1, 0, 0, 0);
|
||||
|
||||
UpdatePreviews();
|
||||
}
|
||||
|
||||
// ─── 힌트 팝업 ───────────────────────────────────────────────────────
|
||||
private void BtnHint_Click(object sender, MouseButtonEventArgs e)
|
||||
=> HintPopup.IsOpen = !HintPopup.IsOpen;
|
||||
|
||||
// ─── 파일/폴더 추가 ──────────────────────────────────────────────────
|
||||
private void BtnAddFolder_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
using var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||||
{
|
||||
Description = "파일이 있는 폴더를 선택하세요",
|
||||
ShowNewFolderButton = false
|
||||
};
|
||||
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||
|
||||
var files = Directory.GetFiles(dlg.SelectedPath);
|
||||
Array.Sort(files);
|
||||
AddFiles(files);
|
||||
}
|
||||
|
||||
private void BtnAddFiles_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
using var dlg = new System.Windows.Forms.OpenFileDialog
|
||||
{
|
||||
Title = "이름변경할 파일 선택",
|
||||
Multiselect = true,
|
||||
Filter = "모든 파일 (*.*)|*.*"
|
||||
};
|
||||
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||
|
||||
var files = dlg.FileNames.OrderBy(f => f).ToArray();
|
||||
AddFiles(files);
|
||||
}
|
||||
|
||||
private void BtnRemoveSelected_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var selected = PreviewGrid.SelectedItems.OfType<RenameEntry>().ToList();
|
||||
foreach (var item in selected) _entries.Remove(item);
|
||||
UpdatePreviews();
|
||||
}
|
||||
|
||||
private void BtnClearAll_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_entries.Clear();
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
// ─── 적용 ────────────────────────────────────────────────────────────
|
||||
private void BtnApply_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var toRename = _entries
|
||||
.Where(en => en.NewName != en.OriginalName && !en.HasConflict)
|
||||
.ToList();
|
||||
|
||||
if (toRename.Count == 0)
|
||||
{
|
||||
NotificationService.Notify("배치 이름변경", "변경할 파일이 없습니다. 패턴을 확인하거나 충돌을 해소하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
int ok = 0, fail = 0;
|
||||
foreach (var entry in toRename)
|
||||
{
|
||||
try
|
||||
{
|
||||
var destPath = Path.Combine(
|
||||
Path.GetDirectoryName(entry.OriginalPath) ?? "",
|
||||
entry.NewName);
|
||||
|
||||
File.Move(entry.OriginalPath, destPath);
|
||||
|
||||
// 성공 후 엔트리 갱신 (새 경로로 업데이트)
|
||||
entry.OriginalPath = destPath;
|
||||
entry.OriginalName = entry.NewName;
|
||||
ok++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"배치 이름변경 실패: {entry.OriginalPath} → {entry.NewName} — {ex.Message}");
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
// 미리보기 갱신 (적용 후 남은 항목)
|
||||
UpdatePreviews();
|
||||
|
||||
var msg = fail > 0
|
||||
? $"{ok}개 이름변경 완료, {fail}개 실패"
|
||||
: $"{ok}개 파일 이름변경 완료";
|
||||
NotificationService.Notify("AX Copilot", msg);
|
||||
LogService.Info($"배치 이름변경: {msg}");
|
||||
}
|
||||
|
||||
// ─── 드래그 앤 드롭 ──────────────────────────────────────────────────
|
||||
private void Window_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
DropHintOverlay.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Effects = DragDropEffects.None;
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Window_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
DropHintOverlay.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
||||
|
||||
var dropped = (string[])e.Data.GetData(DataFormats.FileDrop);
|
||||
var files = new List<string>();
|
||||
|
||||
foreach (var p in dropped)
|
||||
{
|
||||
if (File.Exists(p))
|
||||
{
|
||||
files.Add(p);
|
||||
}
|
||||
else if (Directory.Exists(p))
|
||||
{
|
||||
files.AddRange(Directory.GetFiles(p).OrderBy(f => f));
|
||||
}
|
||||
}
|
||||
|
||||
AddFiles(files);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── RenameEntry 모델 ────────────────────────────────────────────────────────
|
||||
public class RenameEntry : INotifyPropertyChanged
|
||||
{
|
||||
public string OriginalPath { get; set; } = "";
|
||||
|
||||
private string _originalName = "";
|
||||
public string OriginalName
|
||||
{
|
||||
get => _originalName;
|
||||
set { _originalName = value; OnPropertyChanged(); OnPropertyChanged(nameof(StatusText)); }
|
||||
}
|
||||
|
||||
private string _newName = "";
|
||||
public string NewName
|
||||
{
|
||||
get => _newName;
|
||||
set { _newName = value; OnPropertyChanged(); OnPropertyChanged(nameof(StatusText)); }
|
||||
}
|
||||
|
||||
private bool _hasConflict;
|
||||
public bool HasConflict
|
||||
{
|
||||
get => _hasConflict;
|
||||
set { _hasConflict = value; OnPropertyChanged(); OnPropertyChanged(nameof(StatusText)); }
|
||||
}
|
||||
|
||||
public string StatusText =>
|
||||
HasConflict
|
||||
? "⚠ 충돌"
|
||||
: OriginalName == NewName
|
||||
? "─"
|
||||
: "✓";
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
179
src/AxCopilot/Views/MacroEditorWindow.xaml
Normal file
179
src/AxCopilot/Views/MacroEditorWindow.xaml
Normal file
@@ -0,0 +1,179 @@
|
||||
<Window x:Class="AxCopilot.Views.MacroEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Commander — 매크로 편집"
|
||||
Width="560" Height="520"
|
||||
MinWidth="460" MinHeight="420"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
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>
|
||||
|
||||
<!-- ─── 이름/설명 ─────────────────────────────────────────────── -->
|
||||
<StackPanel Grid.Row="1" Margin="18,14,18,8">
|
||||
<TextBlock Text="매크로 이름" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="NameBox"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,6" Margin="0,0,0,10"/>
|
||||
|
||||
<TextBlock Text="설명 (선택)" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="DescBox"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5" Margin="0,0,0,10"/>
|
||||
|
||||
<!-- 열 헤더 -->
|
||||
<Border Height="1" Background="{DynamicResource BorderColor}" Margin="0,0,0,6"/>
|
||||
<Grid Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="80"/>
|
||||
<ColumnDefinition Width="70"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="유형" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
<TextBlock Grid.Column="1" Text="대상 (경로/URL/명령)" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="4,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Text="표시 이름" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="4,0,0,0"/>
|
||||
<TextBlock Grid.Column="3" Text="딜레이(ms)" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="4,0,0,0"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ─── 단계 목록 ─────────────────────────────────────────────── -->
|
||||
<ScrollViewer Grid.Row="2" Margin="18,0,18,0"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="StepsPanel"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ─── 하단 버튼 바 ─────────────────────────────────────────── -->
|
||||
<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 CornerRadius="4" Padding="12,5" Cursor="Hand"
|
||||
HorizontalAlignment="Left"
|
||||
MouseLeftButtonUp="BtnAddStep_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="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="단계 추가" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</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 CornerRadius="4" Padding="16,5" Cursor="Hand"
|
||||
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>
|
||||
320
src/AxCopilot/Views/MacroEditorWindow.xaml.cs
Normal file
320
src/AxCopilot/Views/MacroEditorWindow.xaml.cs
Normal file
@@ -0,0 +1,320 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class MacroEditorWindow : Window
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
private readonly MacroEntry? _editing;
|
||||
|
||||
// 유형 목록
|
||||
private static readonly string[] StepTypes = { "app", "url", "folder", "notification", "cmd" };
|
||||
private static readonly string[] StepTypeLabels = { "앱", "URL", "폴더", "알림", "PowerShell" };
|
||||
|
||||
// 각 행의 컨트롤 참조
|
||||
private readonly List<StepRowUi> _rows = new();
|
||||
|
||||
// 공유 타입 팝업
|
||||
private readonly Popup _typePopup = new()
|
||||
{
|
||||
StaysOpen = false,
|
||||
AllowsTransparency = true,
|
||||
Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom
|
||||
};
|
||||
private StepRowUi? _typeTargetRow;
|
||||
|
||||
public MacroEditorWindow(MacroEntry? entry, SettingsService settings)
|
||||
{
|
||||
InitializeComponent();
|
||||
_settings = settings;
|
||||
_editing = entry;
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
BuildTypePopup();
|
||||
|
||||
if (_editing != null)
|
||||
{
|
||||
NameBox.Text = _editing.Name;
|
||||
DescBox.Text = _editing.Description;
|
||||
foreach (var step in _editing.Steps)
|
||||
AddRow(step);
|
||||
}
|
||||
|
||||
if (_rows.Count == 0)
|
||||
AddRow(null); // 기본 빈 행
|
||||
}
|
||||
|
||||
// ─── 팝업 빌드 ──────────────────────────────────────────────────────────
|
||||
private void BuildTypePopup()
|
||||
{
|
||||
var bg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var hover = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Gray;
|
||||
|
||||
var panel = new StackPanel { Background = bg, MinWidth = 100 };
|
||||
|
||||
for (int i = 0; i < StepTypes.Length; i++)
|
||||
{
|
||||
var idx = i;
|
||||
var label = StepTypeLabels[i];
|
||||
|
||||
var item = new Border
|
||||
{
|
||||
Padding = new Thickness(12, 6, 12, 6),
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
Tag = idx
|
||||
};
|
||||
item.MouseEnter += (_, _) => item.Background = hover;
|
||||
item.MouseLeave += (_, _) => item.Background = Brushes.Transparent;
|
||||
item.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
if (_typeTargetRow != null)
|
||||
SetRowType(_typeTargetRow, idx);
|
||||
_typePopup.IsOpen = false;
|
||||
};
|
||||
item.Child = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 12,
|
||||
Foreground = fg
|
||||
};
|
||||
panel.Children.Add(item);
|
||||
}
|
||||
|
||||
var outerBorder = new Border
|
||||
{
|
||||
Background = bg,
|
||||
BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Child = panel,
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
BlurRadius = 12,
|
||||
ShadowDepth = 3,
|
||||
Opacity = 0.3,
|
||||
Color = Colors.Black,
|
||||
Direction = 270
|
||||
}
|
||||
};
|
||||
|
||||
_typePopup.Child = outerBorder;
|
||||
}
|
||||
|
||||
// ─── 행 추가 ────────────────────────────────────────────────────────────
|
||||
private void AddRow(MacroStep? step)
|
||||
{
|
||||
var bg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var primFg = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
|
||||
var row = new StepRowUi();
|
||||
|
||||
var grid = new Grid { Margin = new Thickness(0, 0, 0, 4) };
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(90) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(70) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
|
||||
|
||||
// Col 0: 유형 버튼
|
||||
var typeLbl = new TextBlock
|
||||
{
|
||||
FontSize = 11,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = primFg,
|
||||
Text = StepTypeLabels[0]
|
||||
};
|
||||
var typeBtn = new Border
|
||||
{
|
||||
Background = bg,
|
||||
BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(6, 4, 6, 4),
|
||||
Cursor = Cursors.Hand,
|
||||
Child = typeLbl,
|
||||
Margin = new Thickness(0, 0, 4, 0)
|
||||
};
|
||||
typeBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
_typeTargetRow = row;
|
||||
_typePopup.PlacementTarget = typeBtn;
|
||||
_typePopup.IsOpen = true;
|
||||
};
|
||||
row.TypeButton = typeBtn;
|
||||
row.TypeLabel = typeLbl;
|
||||
Grid.SetColumn(typeBtn, 0);
|
||||
|
||||
// Col 1: 대상
|
||||
var targetBox = new TextBox
|
||||
{
|
||||
FontSize = 11,
|
||||
Foreground = primFg,
|
||||
Background = (Brush)TryFindResource("LauncherBackground")! ?? Brushes.Black,
|
||||
BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(6, 4, 6, 4),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
Text = step?.Target ?? ""
|
||||
};
|
||||
row.TargetBox = targetBox;
|
||||
Grid.SetColumn(targetBox, 1);
|
||||
|
||||
// Col 2: 표시 이름
|
||||
var labelBox = new TextBox
|
||||
{
|
||||
FontSize = 11,
|
||||
Foreground = secFg,
|
||||
Background = (Brush)TryFindResource("LauncherBackground")! ?? Brushes.Black,
|
||||
BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(6, 4, 6, 4),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
Text = step?.Label ?? ""
|
||||
};
|
||||
row.LabelBox = labelBox;
|
||||
Grid.SetColumn(labelBox, 2);
|
||||
|
||||
// Col 3: 딜레이
|
||||
var delayBox = new TextBox
|
||||
{
|
||||
FontSize = 11,
|
||||
Foreground = primFg,
|
||||
Background = (Brush)TryFindResource("LauncherBackground")! ?? Brushes.Black,
|
||||
BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(6, 4, 6, 4),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
Text = (step?.DelayMs ?? 500).ToString()
|
||||
};
|
||||
row.DelayBox = delayBox;
|
||||
Grid.SetColumn(delayBox, 3);
|
||||
|
||||
// Col 4: 삭제
|
||||
var delBtn = new Border
|
||||
{
|
||||
Width = 24,
|
||||
Height = 24,
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "\uE711",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11,
|
||||
Foreground = secFg,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
}
|
||||
};
|
||||
delBtn.MouseEnter += (_, _) => delBtn.Background = new SolidColorBrush(Color.FromArgb(0x30, 0xC0, 0x50, 0x50));
|
||||
delBtn.MouseLeave += (_, _) => delBtn.Background = Brushes.Transparent;
|
||||
delBtn.MouseLeftButtonUp += (_, _) => RemoveRow(row);
|
||||
Grid.SetColumn(delBtn, 4);
|
||||
|
||||
grid.Children.Add(typeBtn);
|
||||
grid.Children.Add(targetBox);
|
||||
grid.Children.Add(labelBox);
|
||||
grid.Children.Add(delayBox);
|
||||
grid.Children.Add(delBtn);
|
||||
|
||||
row.Grid = grid;
|
||||
|
||||
// 유형 초기화
|
||||
int typeIdx = step != null ? Array.IndexOf(StepTypes, step.Type.ToLowerInvariant()) : 0;
|
||||
if (typeIdx < 0) typeIdx = 0;
|
||||
SetRowType(row, typeIdx);
|
||||
|
||||
_rows.Add(row);
|
||||
StepsPanel.Children.Add(grid);
|
||||
}
|
||||
|
||||
private void SetRowType(StepRowUi row, int typeIdx)
|
||||
{
|
||||
row.TypeIndex = typeIdx;
|
||||
row.TypeLabel.Text = StepTypeLabels[typeIdx];
|
||||
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
row.TypeLabel.Foreground = accent;
|
||||
}
|
||||
|
||||
private void RemoveRow(StepRowUi row)
|
||||
{
|
||||
StepsPanel.Children.Remove(row.Grid);
|
||||
_rows.Remove(row);
|
||||
}
|
||||
|
||||
// ─── 버튼 이벤트 ─────────────────────────────────────────────────────────
|
||||
private void BtnAddStep_Click(object sender, MouseButtonEventArgs e) => AddRow(null);
|
||||
|
||||
private void BtnSave_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var name = NameBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
MessageBox.Show("매크로 이름을 입력하세요.", "저장 오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var steps = _rows
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.TargetBox.Text))
|
||||
.Select(r => new MacroStep
|
||||
{
|
||||
Type = StepTypes[r.TypeIndex],
|
||||
Target = r.TargetBox.Text.Trim(),
|
||||
Label = r.LabelBox.Text.Trim(),
|
||||
DelayMs = int.TryParse(r.DelayBox.Text, out var d) ? Math.Max(0, d) : 500
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var entry = _editing ?? new MacroEntry();
|
||||
entry.Name = name;
|
||||
entry.Description = DescBox.Text.Trim();
|
||||
entry.Steps = steps;
|
||||
|
||||
if (_editing == null)
|
||||
_settings.Settings.Macros.Add(entry);
|
||||
|
||||
_settings.Save();
|
||||
NotificationService.Notify("AX Copilot", $"매크로 '{entry.Name}' 저장됨");
|
||||
Close();
|
||||
}
|
||||
|
||||
private void BtnCancel_Click(object sender, MouseButtonEventArgs e) => Close();
|
||||
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close();
|
||||
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed) DragMove();
|
||||
}
|
||||
|
||||
// ─── 내부 행 참조 클래스 ──────────────────────────────────────────────────
|
||||
private class StepRowUi
|
||||
{
|
||||
public Grid Grid = null!;
|
||||
public Border TypeButton = null!;
|
||||
public TextBlock TypeLabel = null!;
|
||||
public TextBox TargetBox = null!;
|
||||
public TextBox LabelBox = null!;
|
||||
public TextBox DelayBox = null!;
|
||||
public int TypeIndex;
|
||||
}
|
||||
}
|
||||
343
src/AxCopilot/Views/ScheduleEditorWindow.xaml
Normal file
343
src/AxCopilot/Views/ScheduleEditorWindow.xaml
Normal file
@@ -0,0 +1,343 @@
|
||||
<Window x:Class="AxCopilot.Views.ScheduleEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Commander — 스케줄 편집"
|
||||
Width="520" Height="480"
|
||||
MinWidth="440" MinHeight="400"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
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="*"/> <!-- 콘텐츠 -->
|
||||
<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>
|
||||
|
||||
<!-- ─── 콘텐츠 ───────────────────────────────────────────────── -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="18,14,18,10">
|
||||
|
||||
<!-- 스케줄 이름 -->
|
||||
<TextBlock Text="스케줄 이름" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="NameBox"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,6" Margin="0,0,0,14"/>
|
||||
|
||||
<!-- ── 트리거 유형 ── -->
|
||||
<TextBlock Text="실행 주기" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<Border x:Name="BtnDaily" CornerRadius="4,0,0,4" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="TriggerType_Click" Tag="daily">
|
||||
<TextBlock x:Name="TxtDaily" Text="매일" FontSize="12"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnWeekdays" CornerRadius="0" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="TriggerType_Click" Tag="weekdays">
|
||||
<TextBlock x:Name="TxtWeekdays" Text="주중(월~금)" FontSize="12"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnWeekly" CornerRadius="0" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="TriggerType_Click" Tag="weekly">
|
||||
<TextBlock x:Name="TxtWeekly" Text="매주" FontSize="12"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnOnce" CornerRadius="0,4,4,0" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="TriggerType_Click" Tag="once">
|
||||
<TextBlock x:Name="TxtOnce" Text="한번" FontSize="12"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 실행 시각 -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="실행 시각 (HH:mm)"
|
||||
FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" Width="130"/>
|
||||
<TextBox Grid.Column="1" x:Name="TimeBox"
|
||||
Text="09:00"
|
||||
FontFamily="Cascadia Code, Consolas"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5" MaxWidth="100" HorizontalAlignment="Left"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 요일 선택 (weekly일 때만 표시) -->
|
||||
<StackPanel x:Name="WeekDaysPanel" Visibility="Collapsed" Margin="0,0,0,12">
|
||||
<TextBlock Text="요일 선택" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="BtnSun" Tag="0" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="일" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnMon" Tag="1" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="월" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnTue" Tag="2" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="화" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnWed" Tag="3" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="수" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnThu" Tag="4" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="목" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnFri" Tag="5" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="금" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnSat" Tag="6" CornerRadius="4" Width="38" Height="32" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="토" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 날짜 선택 (once일 때만 표시) -->
|
||||
<Grid x:Name="DatePanel" Visibility="Collapsed" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="실행 날짜 (yyyy-MM-dd)"
|
||||
FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" Width="130"/>
|
||||
<TextBox Grid.Column="1" x:Name="DateBox"
|
||||
FontFamily="Cascadia Code, Consolas"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5" MaxWidth="140" HorizontalAlignment="Left"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 구분선 -->
|
||||
<Border Height="1" Background="{DynamicResource BorderColor}" Margin="0,4,0,14"/>
|
||||
|
||||
<!-- ── 액션 유형 ── -->
|
||||
<TextBlock Text="실행 액션" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<Border x:Name="BtnActionApp" CornerRadius="4,0,0,4" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="ActionType_Click" Tag="app">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock x:Name="TxtActionApp" Text="앱 실행" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border x:Name="BtnActionNotif" CornerRadius="0,4,4,0" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="ActionType_Click" Tag="notification">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock x:Name="TxtActionNotif" Text="알림 표시" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 앱 경로 (app 모드) -->
|
||||
<StackPanel x:Name="AppPathPanel" Visibility="Visible">
|
||||
<TextBlock Text="앱 경로" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0" x:Name="AppPathBox"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5" Margin="0,0,6,0"/>
|
||||
<Border Grid.Column="1" CornerRadius="4" Padding="10,5" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnBrowseApp_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="11"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<TextBlock Text="실행 인자 (선택)" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="AppArgsBox"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 알림 메시지 (notification 모드) -->
|
||||
<StackPanel x:Name="NotifPanel" Visibility="Collapsed">
|
||||
<TextBlock Text="알림 메시지" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="NotifMsgBox"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,6"
|
||||
AcceptsReturn="False"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── 조건 (L6-4) ── -->
|
||||
<Border Height="1" Background="{DynamicResource BorderColor}" Margin="0,14,0,14"/>
|
||||
<TextBlock Text="실행 조건 (선택)" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
|
||||
<Grid Margin="0,0,0,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="프로세스 이름"
|
||||
FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" Width="100"/>
|
||||
<TextBox Grid.Column="1" x:Name="ConditionProcessBox"
|
||||
FontFamily="Cascadia Code, Consolas"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5" MaxWidth="180" HorizontalAlignment="Left"/>
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<Border x:Name="BtnCondRun" CornerRadius="4,0,0,4" Padding="12,4" Cursor="Hand"
|
||||
MouseLeftButtonUp="ConditionMode_Click" Tag="run">
|
||||
<TextBlock x:Name="TxtCondRun" Text="실행 중일 때" FontSize="11"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnCondNotRun" CornerRadius="0,4,4,0" Padding="12,4" Cursor="Hand"
|
||||
MouseLeftButtonUp="ConditionMode_Click" Tag="notrun">
|
||||
<TextBlock x:Name="TxtCondNotRun" Text="실행 중 아닐 때" FontSize="11"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock Text="비어 있으면 조건 없이 항상 실행합니다 · 예: chrome, code, slack"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Margin="0,4,0,0"/>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ─── 하단 버튼 바 ─────────────────────────────────────────── -->
|
||||
<Border Grid.Row="2" CornerRadius="0,0,12,12"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Padding="12,8">
|
||||
<Grid>
|
||||
<!-- 활성화 토글 -->
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="활성화" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<Border x:Name="EnabledToggle"
|
||||
Width="36" Height="20" CornerRadius="10"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
Cursor="Hand" MouseLeftButtonUp="EnabledToggle_Click">
|
||||
<Border x:Name="EnabledThumb"
|
||||
Width="16" Height="16" CornerRadius="8"
|
||||
Background="White"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,1,0"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 취소·저장 -->
|
||||
<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 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>
|
||||
338
src/AxCopilot/Views/ScheduleEditorWindow.xaml.cs
Normal file
338
src/AxCopilot/Views/ScheduleEditorWindow.xaml.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ScheduleEditorWindow : Window
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
private readonly ScheduleEntry? _editing; // null = 새 스케줄
|
||||
|
||||
private string _triggerType = "daily";
|
||||
private string _actionType = "app";
|
||||
private bool _enabled = true;
|
||||
private bool _conditionMustRun = true;
|
||||
|
||||
// 요일 버튼 → Border 참조
|
||||
private Border[] _dayBtns = null!;
|
||||
|
||||
public ScheduleEditorWindow(ScheduleEntry? entry, SettingsService settings)
|
||||
{
|
||||
InitializeComponent();
|
||||
_settings = settings;
|
||||
_editing = entry;
|
||||
|
||||
_dayBtns = new[] { BtnSun, BtnMon, BtnTue, BtnWed, BtnThu, BtnFri, BtnSat };
|
||||
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
// ─── 초기화 ─────────────────────────────────────────────────────────────
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 다크 테마 색상
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x25, 0x26, 0x37));
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var border = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2E, 0x2F, 0x4A));
|
||||
|
||||
// 요일 버튼 기본 색
|
||||
foreach (var b in _dayBtns)
|
||||
{
|
||||
b.Background = dimBg;
|
||||
b.BorderBrush = border;
|
||||
b.BorderThickness = new Thickness(1);
|
||||
if (b.Child is TextBlock tb)
|
||||
tb.Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
}
|
||||
|
||||
if (_editing != null)
|
||||
LoadFromEntry(_editing);
|
||||
else
|
||||
SetTriggerUi("daily");
|
||||
|
||||
SetActionUi(_actionType);
|
||||
UpdateToggleUi(_enabled);
|
||||
SetConditionModeUi(_conditionMustRun);
|
||||
}
|
||||
|
||||
private void LoadFromEntry(ScheduleEntry e)
|
||||
{
|
||||
NameBox.Text = e.Name;
|
||||
TimeBox.Text = e.TriggerTime;
|
||||
_enabled = e.Enabled;
|
||||
_triggerType = e.TriggerType;
|
||||
_actionType = e.ActionType;
|
||||
|
||||
if (e.TriggerDate != null)
|
||||
DateBox.Text = e.TriggerDate;
|
||||
|
||||
SetTriggerUi(e.TriggerType);
|
||||
|
||||
// 요일 복원
|
||||
foreach (var b in _dayBtns)
|
||||
{
|
||||
if (int.TryParse(b.Tag?.ToString(), out var day) && e.WeekDays.Contains(day))
|
||||
SetDaySelected(b, true);
|
||||
}
|
||||
|
||||
if (e.ActionType == "app")
|
||||
{
|
||||
AppPathBox.Text = e.ActionTarget;
|
||||
AppArgsBox.Text = e.ActionArgs ?? "";
|
||||
}
|
||||
else
|
||||
{
|
||||
NotifMsgBox.Text = e.ActionTarget;
|
||||
}
|
||||
|
||||
// 조건 복원
|
||||
ConditionProcessBox.Text = e.ConditionProcess ?? "";
|
||||
_conditionMustRun = e.ConditionProcessMustRun;
|
||||
SetConditionModeUi(_conditionMustRun);
|
||||
}
|
||||
|
||||
// ─── 트리거 유형 ─────────────────────────────────────────────────────────
|
||||
private void TriggerType_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is string tag)
|
||||
SetTriggerUi(tag);
|
||||
}
|
||||
|
||||
private void SetTriggerUi(string type)
|
||||
{
|
||||
_triggerType = type;
|
||||
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var white = Brushes.White;
|
||||
|
||||
// 버튼 배경·텍스트 색 초기화
|
||||
void SetBtn(Border btn, TextBlock txt, bool active)
|
||||
{
|
||||
btn.Background = active ? accent : dimBg;
|
||||
txt.Foreground = active ? white : secFg;
|
||||
}
|
||||
|
||||
SetBtn(BtnDaily, TxtDaily, type == "daily");
|
||||
SetBtn(BtnWeekdays, TxtWeekdays, type == "weekdays");
|
||||
SetBtn(BtnWeekly, TxtWeekly, type == "weekly");
|
||||
SetBtn(BtnOnce, TxtOnce, type == "once");
|
||||
|
||||
// 요일 패널 / 날짜 패널 표시
|
||||
WeekDaysPanel.Visibility = type == "weekly" ? Visibility.Visible : Visibility.Collapsed;
|
||||
DatePanel.Visibility = type == "once" ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// once 기본값
|
||||
if (type == "once" && string.IsNullOrWhiteSpace(DateBox.Text))
|
||||
DateBox.Text = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
// ─── 요일 선택 ──────────────────────────────────────────────────────────
|
||||
private void WeekDay_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not Border btn) return;
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
bool current = btn.Background == accent;
|
||||
SetDaySelected(btn, !current);
|
||||
}
|
||||
|
||||
private void SetDaySelected(Border btn, bool selected)
|
||||
{
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
btn.Background = selected ? accent : dimBg;
|
||||
if (btn.Child is TextBlock tb)
|
||||
tb.Foreground = selected ? Brushes.White : secFg;
|
||||
}
|
||||
|
||||
private List<int> GetSelectedDays()
|
||||
{
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var list = new List<int>();
|
||||
foreach (var b in _dayBtns)
|
||||
{
|
||||
if (b.Background == accent && int.TryParse(b.Tag?.ToString(), out var day))
|
||||
list.Add(day);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ─── 액션 유형 ──────────────────────────────────────────────────────────
|
||||
private void ActionType_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is string tag)
|
||||
SetActionUi(tag);
|
||||
}
|
||||
|
||||
private void SetActionUi(string type)
|
||||
{
|
||||
_actionType = type;
|
||||
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var white = Brushes.White;
|
||||
|
||||
bool isApp = type == "app";
|
||||
|
||||
BtnActionApp.Background = isApp ? accent : dimBg;
|
||||
BtnActionNotif.Background = !isApp ? accent : dimBg;
|
||||
|
||||
TxtActionApp.Foreground = isApp ? white : secFg;
|
||||
TxtActionNotif.Foreground = !isApp ? white : secFg;
|
||||
|
||||
// 아이콘 TextBlock은 StackPanel의 첫 번째 자식
|
||||
if (BtnActionApp.Child is StackPanel spApp && spApp.Children.Count > 0)
|
||||
((TextBlock)spApp.Children[0]).Foreground = isApp ? white : secFg;
|
||||
if (BtnActionNotif.Child is StackPanel spNotif && spNotif.Children.Count > 0)
|
||||
((TextBlock)spNotif.Children[0]).Foreground = !isApp ? white : secFg;
|
||||
|
||||
AppPathPanel.Visibility = isApp ? Visibility.Visible : Visibility.Collapsed;
|
||||
NotifPanel.Visibility = !isApp ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// ─── 앱 찾아보기 ─────────────────────────────────────────────────────────
|
||||
private void BtnBrowseApp_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
Title = "실행 파일 선택",
|
||||
Filter = "실행 파일|*.exe;*.bat;*.cmd;*.lnk;*.ps1|모든 파일|*.*"
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
AppPathBox.Text = dlg.FileName;
|
||||
}
|
||||
|
||||
// ─── 조건 모드 (L6-4) ───────────────────────────────────────────────────
|
||||
private void ConditionMode_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is string tag)
|
||||
SetConditionModeUi(tag == "run");
|
||||
}
|
||||
|
||||
private void SetConditionModeUi(bool mustRun)
|
||||
{
|
||||
_conditionMustRun = mustRun;
|
||||
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
BtnCondRun.Background = mustRun ? accent : dimBg;
|
||||
BtnCondNotRun.Background = !mustRun ? accent : dimBg;
|
||||
TxtCondRun.Foreground = mustRun ? Brushes.White : secFg;
|
||||
TxtCondNotRun.Foreground = !mustRun ? Brushes.White : secFg;
|
||||
}
|
||||
|
||||
// ─── 활성화 토글 ─────────────────────────────────────────────────────────
|
||||
private void EnabledToggle_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_enabled = !_enabled;
|
||||
UpdateToggleUi(_enabled);
|
||||
}
|
||||
|
||||
private void UpdateToggleUi(bool enabled)
|
||||
{
|
||||
var accent = TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var off = new SolidColorBrush(Color.FromRgb(0x3A, 0x3B, 0x5A));
|
||||
|
||||
EnabledToggle.Background = enabled ? accent : off;
|
||||
|
||||
// 썸 위치 애니메이션
|
||||
var da = new DoubleAnimation(
|
||||
enabled ? 1.0 : -1.0, // 실제 HorizontalAlignment·Margin으로 처리
|
||||
TimeSpan.FromMilliseconds(150));
|
||||
|
||||
EnabledThumb.HorizontalAlignment = enabled ? HorizontalAlignment.Right : HorizontalAlignment.Left;
|
||||
EnabledThumb.Margin = enabled ? new Thickness(0, 0, 2, 0) : new Thickness(2, 0, 0, 0);
|
||||
}
|
||||
|
||||
// ─── 저장 ────────────────────────────────────────────────────────────────
|
||||
private void BtnSave_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var name = NameBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
MessageBox.Show("스케줄 이름을 입력하세요.", "저장 오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var timeStr = TimeBox.Text.Trim();
|
||||
if (!TimeSpan.TryParseExact(timeStr, new[] { @"hh\:mm", @"h\:mm" },
|
||||
System.Globalization.CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
MessageBox.Show("실행 시각을 HH:mm 형식으로 입력하세요. (예: 09:00)",
|
||||
"저장 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_triggerType == "once")
|
||||
{
|
||||
var dateStr = DateBox.Text.Trim();
|
||||
if (!DateTime.TryParse(dateStr, out _))
|
||||
{
|
||||
MessageBox.Show("실행 날짜를 yyyy-MM-dd 형식으로 입력하세요.",
|
||||
"저장 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_actionType == "app" && string.IsNullOrWhiteSpace(AppPathBox.Text))
|
||||
{
|
||||
MessageBox.Show("실행할 앱 경로를 입력하세요.", "저장 오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 항목 편집 or 신규 생성
|
||||
var entry = _editing ?? new ScheduleEntry();
|
||||
|
||||
entry.Name = name;
|
||||
entry.Enabled = _enabled;
|
||||
entry.TriggerType = _triggerType;
|
||||
entry.TriggerTime = timeStr;
|
||||
entry.WeekDays = _triggerType == "weekly" ? GetSelectedDays() : new List<int>();
|
||||
entry.TriggerDate = _triggerType == "once" ? DateBox.Text.Trim() : null;
|
||||
entry.ActionType = _actionType;
|
||||
entry.ActionTarget = _actionType == "app"
|
||||
? AppPathBox.Text.Trim()
|
||||
: NotifMsgBox.Text.Trim();
|
||||
entry.ActionArgs = _actionType == "app" ? AppArgsBox.Text.Trim() : "";
|
||||
|
||||
// 조건 저장 (L6-4)
|
||||
entry.ConditionProcess = ConditionProcessBox.Text.Trim();
|
||||
entry.ConditionProcessMustRun = _conditionMustRun;
|
||||
|
||||
var schedules = _settings.Settings.Schedules;
|
||||
|
||||
if (_editing == null)
|
||||
schedules.Add(entry);
|
||||
// 편집 모드: 이미 리스트 내 참조이므로 별도 추가 불필요
|
||||
|
||||
_settings.Save();
|
||||
NotificationService.Notify("AX Copilot", $"스케줄 '{entry.Name}' 저장됨");
|
||||
Close();
|
||||
}
|
||||
|
||||
// ─── 윈도우 컨트롤 ──────────────────────────────────────────────────────
|
||||
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();
|
||||
}
|
||||
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