[Phase 40] ChatWindow 2차 파셜 클래스 분할 (94.5% 감소)
4,767줄 ChatWindow.xaml.cs를 7개 파셜 파일로 추가 분할 메인 파일: 4,767줄 → 262줄 (94.5% 감소) 전체 ChatWindow 파셜 파일: 15개 - ChatWindow.Controls.cs (595줄): 사용자정보, 스크롤, 제목편집, 탭전환 - ChatWindow.WorkFolder.cs (359줄): 작업폴더, 폴더 설정 - ChatWindow.PermissionMenu.cs (498줄): 권한, 파일첨부, 사이드바 - ChatWindow.ConversationList.cs (747줄): 대화목록, 제목편집, 검색 - ChatWindow.Sending.cs (720줄): 전송, 편집모드, 타이머 - ChatWindow.HelpCommands.cs (157줄): /help 도움말 - ChatWindow.ResponseHandling.cs (1,494줄): 응답재생성, 스트리밍, 토스트 - 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
498
src/AxCopilot/Views/ChatWindow.PermissionMenu.cs
Normal file
498
src/AxCopilot/Views/ChatWindow.PermissionMenu.cs
Normal file
@@ -0,0 +1,498 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
// ─── 권한 메뉴 ─────────────────────────────────────────────────────────
|
||||
|
||||
private void BtnPermission_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PermissionPopup == null) return;
|
||||
PermissionItems.Children.Clear();
|
||||
|
||||
var levels = new (string Level, string Sym, string Desc, string Color)[] {
|
||||
("Ask", "\uE8D7", "매번 확인 — 파일 접근 시 사용자에게 묻습니다", "#4B5EFC"),
|
||||
("Auto", "\uE73E", "자동 허용 — 파일을 자동으로 읽고 씁니다", "#DD6B20"),
|
||||
("Deny", "\uE711", "접근 차단 — 파일 접근을 허용하지 않습니다", "#C50F1F"),
|
||||
};
|
||||
var current = Llm.FilePermission;
|
||||
foreach (var (level, sym, desc, color) in levels)
|
||||
{
|
||||
var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 라운드 코너 템플릿 (기본 Button 크롬 제거)
|
||||
var template = new ControlTemplate(typeof(Button));
|
||||
var bdFactory = new FrameworkElementFactory(typeof(Border));
|
||||
bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent);
|
||||
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
|
||||
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8));
|
||||
bdFactory.Name = "Bd";
|
||||
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left);
|
||||
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
bdFactory.AppendChild(cpFactory);
|
||||
template.VisualTree = bdFactory;
|
||||
// 호버 효과
|
||||
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
|
||||
hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty,
|
||||
new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd"));
|
||||
template.Triggers.Add(hoverTrigger);
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Template = template,
|
||||
BorderThickness = new Thickness(0),
|
||||
Cursor = Cursors.Hand,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Left,
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
};
|
||||
ApplyHoverScaleAnimation(btn, 1.02);
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
// 커스텀 체크 아이콘
|
||||
sp.Children.Add(CreateCheckIcon(isActive));
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = sym, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14,
|
||||
Foreground = BrushFromHex(color),
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
var textStack = new StackPanel();
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = level, FontSize = 13, FontWeight = FontWeights.Bold,
|
||||
Foreground = BrushFromHex(color),
|
||||
});
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = desc, FontSize = 11,
|
||||
Foreground = ThemeResourceHelper.Secondary(this),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxWidth = 220,
|
||||
});
|
||||
sp.Children.Add(textStack);
|
||||
btn.Content = sp;
|
||||
|
||||
var capturedLevel = level;
|
||||
btn.Click += (_, _) =>
|
||||
{
|
||||
Llm.FilePermission = capturedLevel;
|
||||
UpdatePermissionUI();
|
||||
SaveConversationSettings();
|
||||
PermissionPopup.IsOpen = false;
|
||||
};
|
||||
PermissionItems.Children.Add(btn);
|
||||
}
|
||||
PermissionPopup.IsOpen = true;
|
||||
}
|
||||
|
||||
private bool _autoWarningDismissed; // Auto 경고 배너 사용자가 닫았는지
|
||||
|
||||
private void BtnAutoWarningClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_autoWarningDismissed = true;
|
||||
if (AutoPermissionWarning != null)
|
||||
AutoPermissionWarning.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void UpdatePermissionUI()
|
||||
{
|
||||
if (PermissionLabel == null || PermissionIcon == null) return;
|
||||
var perm = Llm.FilePermission;
|
||||
PermissionLabel.Text = perm;
|
||||
PermissionIcon.Text = perm switch
|
||||
{
|
||||
"Auto" => "\uE73E",
|
||||
"Deny" => "\uE711",
|
||||
_ => "\uE8D7",
|
||||
};
|
||||
|
||||
// Auto 모드일 때 경고 색상 + 배너 표시
|
||||
if (perm == "Auto")
|
||||
{
|
||||
var warnColor = new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20));
|
||||
PermissionLabel.Foreground = warnColor;
|
||||
PermissionIcon.Foreground = warnColor;
|
||||
// Auto 전환 시 새 대화에서만 1회 필수 표시 (기존 대화에서 이미 Auto였으면 숨김)
|
||||
ChatConversation? convForWarn;
|
||||
lock (_convLock) convForWarn = _currentConversation;
|
||||
var isExisting = convForWarn != null && convForWarn.Messages.Count > 0 && convForWarn.Permission == "Auto";
|
||||
if (AutoPermissionWarning != null && !_autoWarningDismissed && !isExisting)
|
||||
AutoPermissionWarning.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
_autoWarningDismissed = false; // Auto가 아닌 모드로 전환하면 리셋
|
||||
var defaultFg = ThemeResourceHelper.Secondary(this);
|
||||
var iconFg = perm == "Deny" ? new SolidColorBrush(Color.FromRgb(0xC5, 0x0F, 0x1F))
|
||||
: new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); // Ask = 파란색
|
||||
PermissionLabel.Foreground = defaultFg;
|
||||
PermissionIcon.Foreground = iconFg;
|
||||
if (AutoPermissionWarning != null)
|
||||
AutoPermissionWarning.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
// ──── 데이터 활용 수준 메뉴 ────
|
||||
|
||||
private void BtnDataUsage_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (DataUsagePopup == null) return;
|
||||
DataUsageItems.Children.Clear();
|
||||
|
||||
var options = new (string Key, string Sym, string Label, string Desc, string Color)[]
|
||||
{
|
||||
("active", "\uE9F5", "적극 활용", "폴더 내 문서를 자동 탐색하여 보고서 작성에 적극 활용합니다", "#107C10"),
|
||||
("passive", "\uE8FD", "소극 활용", "사용자가 요청할 때만 폴더 데이터를 참조합니다", "#D97706"),
|
||||
("none", "\uE8D8", "활용하지 않음", "폴더 내 문서를 읽거나 참조하지 않습니다", "#9CA3AF"),
|
||||
};
|
||||
|
||||
foreach (var (key, sym, label, desc, color) in options)
|
||||
{
|
||||
var isActive = key.Equals(_folderDataUsage, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var template = new ControlTemplate(typeof(Button));
|
||||
var bdFactory = new FrameworkElementFactory(typeof(Border));
|
||||
bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent);
|
||||
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
|
||||
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8));
|
||||
bdFactory.Name = "Bd";
|
||||
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left);
|
||||
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
bdFactory.AppendChild(cpFactory);
|
||||
template.VisualTree = bdFactory;
|
||||
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
|
||||
hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty,
|
||||
new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd"));
|
||||
template.Triggers.Add(hoverTrigger);
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Template = template,
|
||||
BorderThickness = new Thickness(0),
|
||||
Cursor = Cursors.Hand,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Left,
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
};
|
||||
ApplyHoverScaleAnimation(btn, 1.02);
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
// 커스텀 체크 아이콘
|
||||
sp.Children.Add(CreateCheckIcon(isActive));
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = sym, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14,
|
||||
Foreground = BrushFromHex(color),
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
var textStack = new StackPanel();
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label, FontSize = 13, FontWeight = FontWeights.Bold,
|
||||
Foreground = BrushFromHex(color),
|
||||
});
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = desc, FontSize = 11,
|
||||
Foreground = ThemeResourceHelper.Secondary(this),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxWidth = 240,
|
||||
});
|
||||
sp.Children.Add(textStack);
|
||||
btn.Content = sp;
|
||||
|
||||
var capturedKey = key;
|
||||
btn.Click += (_, _) =>
|
||||
{
|
||||
_folderDataUsage = capturedKey;
|
||||
UpdateDataUsageUI();
|
||||
SaveConversationSettings();
|
||||
DataUsagePopup.IsOpen = false;
|
||||
};
|
||||
DataUsageItems.Children.Add(btn);
|
||||
}
|
||||
DataUsagePopup.IsOpen = true;
|
||||
}
|
||||
|
||||
private void UpdateDataUsageUI()
|
||||
{
|
||||
if (DataUsageLabel == null || DataUsageIcon == null) return;
|
||||
var (label, icon, color) = _folderDataUsage switch
|
||||
{
|
||||
"passive" => ("소극", "\uE8FD", "#D97706"),
|
||||
"none" => ("미사용", "\uE8D8", "#9CA3AF"),
|
||||
_ => ("적극", "\uE9F5", "#107C10"),
|
||||
};
|
||||
DataUsageLabel.Text = label;
|
||||
DataUsageIcon.Text = icon;
|
||||
DataUsageIcon.Foreground = BrushFromHex(color);
|
||||
}
|
||||
|
||||
/// <summary>Cowork/Code 탭 진입 시 설정의 기본 권한을 적용.</summary>
|
||||
private void ApplyTabDefaultPermission()
|
||||
{
|
||||
if (_activeTab == "Chat")
|
||||
{
|
||||
// Chat 탭: 경고 배너 숨기고 기본 Ask 모드로 복원
|
||||
Llm.FilePermission = "Ask";
|
||||
UpdatePermissionUI();
|
||||
return;
|
||||
}
|
||||
var defaultPerm = Llm.DefaultAgentPermission;
|
||||
if (!string.IsNullOrEmpty(defaultPerm))
|
||||
{
|
||||
Llm.FilePermission = defaultPerm;
|
||||
UpdatePermissionUI();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 파일 첨부 ─────────────────────────────────────────────────────────
|
||||
|
||||
private void BtnAttach_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Multiselect = true,
|
||||
Title = "첨부할 파일을 선택하세요",
|
||||
Filter = "모든 파일 (*.*)|*.*|텍스트 (*.txt;*.md;*.csv)|*.txt;*.md;*.csv|코드 (*.cs;*.py;*.js;*.ts)|*.cs;*.py;*.js;*.ts",
|
||||
};
|
||||
|
||||
// 작업 폴더가 있으면 초기 경로 설정
|
||||
var workFolder = GetCurrentWorkFolder();
|
||||
if (!string.IsNullOrEmpty(workFolder) && System.IO.Directory.Exists(workFolder))
|
||||
dlg.InitialDirectory = workFolder;
|
||||
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
|
||||
foreach (var file in dlg.FileNames)
|
||||
AddAttachedFile(file);
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"
|
||||
};
|
||||
|
||||
private void AddAttachedFile(string filePath)
|
||||
{
|
||||
if (_attachedFiles.Contains(filePath)) return;
|
||||
|
||||
// 파일 크기 제한 (10MB)
|
||||
try
|
||||
{
|
||||
var fi = new System.IO.FileInfo(filePath);
|
||||
if (fi.Length > 10 * 1024 * 1024)
|
||||
{
|
||||
CustomMessageBox.Show($"파일이 너무 큽니다 (10MB 초과):\n{fi.Name}", "첨부 제한", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 이미지 파일 → Vision API용 base64 변환
|
||||
var ext = fi.Extension.ToLowerInvariant();
|
||||
if (ImageExtensions.Contains(ext) && Llm.EnableImageInput)
|
||||
{
|
||||
var maxKb = Llm.MaxImageSizeKb;
|
||||
if (maxKb <= 0) maxKb = 5120;
|
||||
if (fi.Length > maxKb * 1024)
|
||||
{
|
||||
CustomMessageBox.Show($"이미지가 너무 큽니다 ({fi.Length / 1024}KB, 최대 {maxKb}KB).",
|
||||
"이미지 크기 초과", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = System.IO.File.ReadAllBytes(filePath);
|
||||
var mimeType = ext switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".gif" => "image/gif",
|
||||
".bmp" => "image/bmp",
|
||||
".webp" => "image/webp",
|
||||
_ => "image/png",
|
||||
};
|
||||
var attachment = new ImageAttachment
|
||||
{
|
||||
Base64 = Convert.ToBase64String(bytes),
|
||||
MimeType = mimeType,
|
||||
FileName = fi.Name,
|
||||
};
|
||||
|
||||
// 중복 확인
|
||||
if (_pendingImages.Any(i => i.FileName == attachment.FileName)) return;
|
||||
|
||||
_pendingImages.Add(attachment);
|
||||
AddImagePreview(attachment);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception) { return; }
|
||||
|
||||
_attachedFiles.Add(filePath);
|
||||
RefreshAttachedFilesUI();
|
||||
}
|
||||
|
||||
private void RemoveAttachedFile(string filePath)
|
||||
{
|
||||
_attachedFiles.Remove(filePath);
|
||||
RefreshAttachedFilesUI();
|
||||
}
|
||||
|
||||
private void RefreshAttachedFilesUI()
|
||||
{
|
||||
AttachedFilesPanel.Items.Clear();
|
||||
if (_attachedFiles.Count == 0)
|
||||
{
|
||||
AttachedFilesPanel.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
AttachedFilesPanel.Visibility = Visibility.Visible;
|
||||
var secondaryBrush = ThemeResourceHelper.Secondary(this);
|
||||
var hintBg = ThemeResourceHelper.Hint(this);
|
||||
|
||||
foreach (var file in _attachedFiles.ToList())
|
||||
{
|
||||
var fileName = System.IO.Path.GetFileName(file);
|
||||
var capturedFile = file;
|
||||
|
||||
var chip = new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Padding = new Thickness(8, 4, 4, 4),
|
||||
Margin = new Thickness(0, 0, 4, 4),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8A5", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10,
|
||||
Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = fileName, FontSize = 11, Foreground = secondaryBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, MaxWidth = 150, TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
ToolTip = file,
|
||||
});
|
||||
var removeBtn = new Button
|
||||
{
|
||||
Content = new TextBlock { Text = "\uE711", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 8, Foreground = secondaryBrush },
|
||||
Background = Brushes.Transparent, BorderThickness = new Thickness(0),
|
||||
Cursor = Cursors.Hand, Padding = new Thickness(4, 2, 4, 2), Margin = new Thickness(2, 0, 0, 0),
|
||||
};
|
||||
removeBtn.Click += (_, _) => RemoveAttachedFile(capturedFile);
|
||||
sp.Children.Add(removeBtn);
|
||||
chip.Child = sp;
|
||||
AttachedFilesPanel.Items.Add(chip);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>첨부 파일 내용을 시스템 메시지로 변환합니다.</summary>
|
||||
private string BuildFileContextPrompt()
|
||||
{
|
||||
if (_attachedFiles.Count == 0) return "";
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("\n[첨부 파일 컨텍스트]");
|
||||
|
||||
foreach (var file in _attachedFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ext = System.IO.Path.GetExtension(file).ToLowerInvariant();
|
||||
var isBinary = ext is ".exe" or ".dll" or ".zip" or ".7z" or ".rar" or ".tar" or ".gz"
|
||||
or ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".ico" or ".webp" or ".svg"
|
||||
or ".pdf" or ".docx" or ".xlsx" or ".pptx" or ".doc" or ".xls" or ".ppt"
|
||||
or ".mp3" or ".mp4" or ".avi" or ".mov" or ".mkv" or ".wav" or ".flac"
|
||||
or ".psd" or ".ai" or ".sketch" or ".fig"
|
||||
or ".msi" or ".iso" or ".img" or ".bin" or ".dat" or ".db" or ".sqlite";
|
||||
if (isBinary)
|
||||
{
|
||||
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (바이너리 파일, 내용 생략) ---");
|
||||
continue;
|
||||
}
|
||||
|
||||
var content = System.IO.File.ReadAllText(file);
|
||||
// 최대 8000자로 제한
|
||||
if (content.Length > 8000)
|
||||
content = content[..8000] + "\n... (이하 생략)";
|
||||
|
||||
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} ---");
|
||||
sb.AppendLine(content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (읽기 실패: {ex.Message}) ---");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void ResizeGrip_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
|
||||
{
|
||||
var newW = Width + e.HorizontalChange;
|
||||
var newH = Height + e.VerticalChange;
|
||||
if (newW >= MinWidth) Width = newW;
|
||||
if (newH >= MinHeight) Height = newH;
|
||||
}
|
||||
|
||||
// ─── 사이드바 토글 ───────────────────────────────────────────────────
|
||||
|
||||
private void BtnToggleSidebar_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_sidebarVisible = !_sidebarVisible;
|
||||
if (_sidebarVisible)
|
||||
{
|
||||
// 사이드바 열기, 아이콘 바 숨기기
|
||||
IconBarColumn.Width = new GridLength(0);
|
||||
IconBarPanel.Visibility = Visibility.Collapsed;
|
||||
SidebarPanel.Visibility = Visibility.Visible;
|
||||
ToggleSidebarIcon.Text = "\uE76B";
|
||||
AnimateSidebar(0, 270, () => SidebarColumn.MinWidth = 200);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 사이드바 닫기, 아이콘 바 표시
|
||||
SidebarColumn.MinWidth = 0;
|
||||
ToggleSidebarIcon.Text = "\uE76C";
|
||||
AnimateSidebar(270, 0, () =>
|
||||
{
|
||||
SidebarPanel.Visibility = Visibility.Collapsed;
|
||||
IconBarColumn.Width = new GridLength(52);
|
||||
IconBarPanel.Visibility = Visibility.Visible;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void AnimateSidebar(double from, double to, Action? onComplete = null)
|
||||
{
|
||||
var duration = 200.0;
|
||||
var start = DateTime.UtcNow;
|
||||
var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) };
|
||||
EventHandler tickHandler = null!;
|
||||
tickHandler = (_, _) =>
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - start).TotalMilliseconds;
|
||||
var t = Math.Min(elapsed / duration, 1.0);
|
||||
t = 1 - (1 - t) * (1 - t);
|
||||
SidebarColumn.Width = new GridLength(from + (to - from) * t);
|
||||
if (elapsed >= duration)
|
||||
{
|
||||
timer.Stop();
|
||||
timer.Tick -= tickHandler;
|
||||
SidebarColumn.Width = new GridLength(to);
|
||||
onComplete?.Invoke();
|
||||
}
|
||||
};
|
||||
timer.Tick += tickHandler;
|
||||
timer.Start();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user