ChatWindow.xaml: - 서브 헤더 바(Row 1) 우측에 ModelHeaderChip 버튼 추가 (Segoe MDL2 브레인 아이콘 + ModelHeaderLabel TextBlock) - 서브 헤더 바 우측에 PermissionHeaderChip 버튼 추가 (잠금 아이콘 #4FC3F7 + PermissionHeaderLabel TextBlock) ChatWindow.ModelSelector.cs: - UpdateModelLabel(): ModelHeaderLabel 동기 갱신 코드 추가 ChatWindow.PermissionMenu.cs: - UpdatePermissionUI(): PermissionHeaderLabel 동기 갱신 코드 추가 - PermissionHeaderChip_Click() 신규: PlacementTarget을 헤더 칩으로 교체 후 기존 BtnPermission_Click 호출 ChatWindow.xaml.cs: - Loaded 핸들러에 UpdatePermissionUI() 초기 호출 추가 docs/NEXT_ROADMAP.md: - Phase 17-UI-B 완료 항목 추가 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
511 lines
21 KiB
C#
511 lines
21 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>Phase 17-UI-B: 헤더 바 권한 칩 클릭 — 권한 팝업을 칩 위치에 표시.</summary>
|
|
private void PermissionHeaderChip_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (PermissionPopup == null) return;
|
|
// 팝업 기준점을 헤더 칩으로 변경
|
|
PermissionPopup.PlacementTarget = PermissionHeaderChip;
|
|
PermissionPopup.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
|
|
BtnPermission_Click(sender, e);
|
|
}
|
|
|
|
private void UpdatePermissionUI()
|
|
{
|
|
if (PermissionLabel == null || PermissionIcon == null) return;
|
|
var perm = Llm.FilePermission;
|
|
PermissionLabel.Text = perm;
|
|
// Phase 17-UI-B: 헤더 칩 텍스트도 갱신
|
|
if (PermissionHeaderLabel != null) PermissionHeaderLabel.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();
|
|
}
|
|
}
|