Some checks failed
Release Gate / gate (push) Has been cancelled
- 계획 승인 기본 흐름을 transcript inline 우선 구조로 더 정리하고, 계획 버튼은 저장된 계획을 여는 상세 보기 성격으로 분리했습니다. - OperationalStatusPresentationState를 확장해 runtime badge, compact strip, quick strip의 문구·강조색·노출 여부를 한 번에 계산하도록 통합했습니다. - ChatWindow 상태선/quick strip/status token 로직을 StatusPresentation partial로 분리해 메인 창 코드의 직접 분기와 렌더 책임을 줄였습니다. - 문서 이력(README, DEVELOPMENT)을 2026-04-06 01:37 KST 기준으로 갱신했습니다. - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
1059 lines
47 KiB
C#
1059 lines
47 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using System.Windows.Media.Effects;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
/// <summary>
|
|
/// 에이전트 실행 계획을 상세히 보여주는 별도 창.
|
|
/// - 항목 기본 접힘 / 클릭으로 펼침
|
|
/// - 모두 열기 / 모두 닫기 툴바
|
|
/// - 사방 가장자리 드래그 리사이즈
|
|
/// - 항목 드래그로 순서 변경
|
|
/// </summary>
|
|
internal sealed class PlanViewerWindow : Window
|
|
{
|
|
// ── Win32 Resize ──
|
|
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
|
private const int WM_NCHITTEST = 0x0084;
|
|
private const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13,
|
|
HTTOPRIGHT = 14, HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17;
|
|
private const int ResizeGrip = 12; // 사방 12px 영역에서 리사이즈 가능
|
|
|
|
private const string DragDataFormat = "PlanStepIndex";
|
|
|
|
private readonly StackPanel _stepsPanel;
|
|
private readonly ScrollViewer _scrollViewer;
|
|
private readonly StackPanel _btnPanel;
|
|
private readonly Border _statusBar;
|
|
private readonly TextBlock _statusText;
|
|
private readonly TextBlock _progressText;
|
|
|
|
// 펼침 상태 관리
|
|
private readonly HashSet<int> _expandedSteps = new();
|
|
|
|
// 드래그 상태
|
|
private int _dragSourceIndex = -1;
|
|
private Point _dragStartPoint;
|
|
|
|
private TaskCompletionSource<string?>? _tcs;
|
|
private string _planText = "";
|
|
private List<string> _steps = new();
|
|
private int _currentStep = -1;
|
|
private bool _isExecuting;
|
|
private readonly string _uiExpressionLevel;
|
|
|
|
public PlanViewerWindow(Window? owner = null)
|
|
{
|
|
if (owner != null)
|
|
{
|
|
Owner = owner;
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
|
Resources.MergedDictionaries.Add(owner.Resources);
|
|
}
|
|
|
|
_uiExpressionLevel = ResolveUiExpressionLevel();
|
|
|
|
Width = 640;
|
|
Height = 520;
|
|
MinWidth = 480;
|
|
MinHeight = 360;
|
|
WindowStartupLocation = owner == null
|
|
? WindowStartupLocation.CenterScreen
|
|
: WindowStartupLocation.CenterOwner;
|
|
WindowStyle = WindowStyle.None;
|
|
AllowsTransparency = true;
|
|
Background = Brushes.Transparent;
|
|
ResizeMode = ResizeMode.CanResize; // WndProc로 직접 처리
|
|
ShowInTaskbar = false;
|
|
|
|
var bgBrush = TryFindResource("LauncherBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var accentBrush = TryFindResource("AccentColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
|
|
var root = new Border
|
|
{
|
|
Background = bgBrush,
|
|
CornerRadius = new CornerRadius(14),
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Effect = new DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.35, Color = Colors.Black },
|
|
};
|
|
|
|
var mainGrid = new Grid();
|
|
mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 0: 타이틀 바
|
|
mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 1: 상태 바
|
|
mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 2: 툴바 (모두 열기/닫기)
|
|
mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); // 3: 단계 목록
|
|
mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 4: 하단 버튼
|
|
|
|
// ── 타이틀 바 ──
|
|
var titleBar = new Grid { Background = Brushes.Transparent, Margin = new Thickness(20, 14, 12, 0) };
|
|
titleBar.MouseLeftButtonDown += TitleBar_MouseLeftButtonDown;
|
|
|
|
var titleSp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
titleSp.Children.Add(new TextBlock
|
|
{
|
|
Text = "\uE9D2", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 10, 0),
|
|
});
|
|
titleSp.Children.Add(new TextBlock
|
|
{
|
|
Text = "실행 계획", FontSize = 16, FontWeight = FontWeights.Bold,
|
|
Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
titleBar.Children.Add(titleSp);
|
|
|
|
var closeBtn = new Border
|
|
{
|
|
Width = 32, Height = 32, CornerRadius = new CornerRadius(8),
|
|
Background = Brushes.Transparent, Cursor = Cursors.Hand,
|
|
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center,
|
|
Child = new TextBlock
|
|
{
|
|
Text = "\uE8BB", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 12, Foreground = secondaryText,
|
|
HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
|
|
},
|
|
};
|
|
closeBtn.MouseEnter += (s, _) => ((Border)s).Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
|
closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
|
closeBtn.MouseLeftButtonUp += (_, _) => Hide();
|
|
titleBar.Children.Add(closeBtn);
|
|
Grid.SetRow(titleBar, 0);
|
|
mainGrid.Children.Add(titleBar);
|
|
|
|
// ── 상태 바 (진행률) ──
|
|
_statusBar = new Border
|
|
{
|
|
Visibility = Visibility.Collapsed,
|
|
Margin = new Thickness(20, 8, 20, 0),
|
|
Padding = new Thickness(12, 6, 12, 6),
|
|
CornerRadius = new CornerRadius(8),
|
|
Background = new SolidColorBrush(Color.FromArgb(0x15,
|
|
((SolidColorBrush)accentBrush).Color.R,
|
|
((SolidColorBrush)accentBrush).Color.G,
|
|
((SolidColorBrush)accentBrush).Color.B)),
|
|
};
|
|
var statusGrid = new Grid();
|
|
_statusText = new TextBlock { Text = "실행 중...", FontSize = 12, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center };
|
|
statusGrid.Children.Add(_statusText);
|
|
_progressText = new TextBlock
|
|
{
|
|
Text = "", FontSize = 12, Foreground = secondaryText,
|
|
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center,
|
|
};
|
|
statusGrid.Children.Add(_progressText);
|
|
_statusBar.Child = statusGrid;
|
|
Grid.SetRow(_statusBar, 1);
|
|
mainGrid.Children.Add(_statusBar);
|
|
|
|
// ── 툴바: 모두 열기 / 모두 닫기 ──
|
|
var hoverBgTb = TryFindResource("ItemHoverBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
|
var toolBar = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
Margin = new Thickness(20, 6, 20, 0),
|
|
};
|
|
if (_uiExpressionLevel == "simple")
|
|
toolBar.Visibility = Visibility.Collapsed;
|
|
|
|
var expandAllBtn = MakeToolbarButton("\uE70D", "모두 열기", secondaryText, hoverBgTb);
|
|
expandAllBtn.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
for (int i = 0; i < _steps.Count; i++) _expandedSteps.Add(i);
|
|
RenderSteps();
|
|
};
|
|
toolBar.Children.Add(expandAllBtn);
|
|
|
|
var collapseAllBtn = MakeToolbarButton("\uE70E", "모두 닫기", secondaryText, hoverBgTb);
|
|
collapseAllBtn.MouseLeftButtonUp += (_, _) => { _expandedSteps.Clear(); RenderSteps(); };
|
|
toolBar.Children.Add(collapseAllBtn);
|
|
|
|
Grid.SetRow(toolBar, 2);
|
|
mainGrid.Children.Add(toolBar);
|
|
|
|
// ── 컨텐츠: 단계 목록 ──
|
|
_scrollViewer = new ScrollViewer
|
|
{
|
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
|
Margin = new Thickness(16, 6, 16, 0),
|
|
Padding = new Thickness(4),
|
|
};
|
|
_stepsPanel = new StackPanel();
|
|
_scrollViewer.Content = _stepsPanel;
|
|
Grid.SetRow(_scrollViewer, 3);
|
|
mainGrid.Children.Add(_scrollViewer);
|
|
|
|
// ── 하단 버튼 패널 ──
|
|
_btnPanel = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
Margin = new Thickness(20, 12, 20, 16),
|
|
};
|
|
Grid.SetRow(_btnPanel, 4);
|
|
mainGrid.Children.Add(_btnPanel);
|
|
|
|
root.Child = mainGrid;
|
|
Content = root;
|
|
|
|
// Win32 Resize 훅
|
|
SourceInitialized += (_, _) =>
|
|
{
|
|
var hwnd = new WindowInteropHelper(this).Handle;
|
|
var src = HwndSource.FromHwnd(hwnd);
|
|
src?.AddHook(WndProc);
|
|
};
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════
|
|
// 툴바 버튼 팩토리
|
|
// ════════════════════════════════════════════════════════════
|
|
|
|
private static Border MakeToolbarButton(string icon, string label, Brush fg, Brush hoverBg)
|
|
{
|
|
var btn = new Border
|
|
{
|
|
CornerRadius = new CornerRadius(6),
|
|
Padding = new Thickness(8, 3, 8, 3),
|
|
Margin = new Thickness(0, 0, 4, 0),
|
|
Background = Brushes.Transparent,
|
|
Cursor = Cursors.Hand,
|
|
};
|
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 9, Foreground = fg,
|
|
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0),
|
|
});
|
|
sp.Children.Add(new TextBlock { Text = label, FontSize = 11.5, Foreground = fg });
|
|
btn.Child = sp;
|
|
btn.MouseEnter += (s, _) => ((Border)s).Background = hoverBg;
|
|
btn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
|
return btn;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════
|
|
// 창 이동 / 리사이즈
|
|
// ════════════════════════════════════════════════════════════
|
|
|
|
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (e.ClickCount == 2) return;
|
|
try { DragMove(); } catch { }
|
|
}
|
|
|
|
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
|
{
|
|
if (msg == WM_NCHITTEST)
|
|
{
|
|
var pt = PointFromScreen(new Point(
|
|
(short)(lParam.ToInt32() & 0xFFFF),
|
|
(short)(lParam.ToInt32() >> 16)));
|
|
var w = ActualWidth;
|
|
var h = ActualHeight;
|
|
|
|
int hit = 0;
|
|
if (pt.X < ResizeGrip && pt.Y < ResizeGrip) hit = HTTOPLEFT;
|
|
else if (pt.X > w - ResizeGrip && pt.Y < ResizeGrip) hit = HTTOPRIGHT;
|
|
else if (pt.X < ResizeGrip && pt.Y > h - ResizeGrip) hit = HTBOTTOMLEFT;
|
|
else if (pt.X > w - ResizeGrip && pt.Y > h - ResizeGrip) hit = HTBOTTOMRIGHT;
|
|
else if (pt.X < ResizeGrip) hit = HTLEFT;
|
|
else if (pt.X > w - ResizeGrip) hit = HTRIGHT;
|
|
else if (pt.Y < ResizeGrip) hit = HTTOP;
|
|
else if (pt.Y > h - ResizeGrip) hit = HTBOTTOM;
|
|
|
|
if (hit != 0) { handled = true; return (IntPtr)hit; }
|
|
}
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════
|
|
// 공개 API
|
|
// ════════════════════════════════════════════════════════════
|
|
|
|
public void LoadPlan(string planText, List<string> steps, TaskCompletionSource<string?> tcs)
|
|
{
|
|
_planText = planText;
|
|
_steps = steps;
|
|
_tcs = tcs;
|
|
_currentStep = -1;
|
|
_isExecuting = false;
|
|
_expandedSteps.Clear(); // 새 계획 표시 시 모두 접힌 상태로 시작
|
|
if (_uiExpressionLevel == "rich")
|
|
{
|
|
for (int i = 0; i < _steps.Count; i++)
|
|
_expandedSteps.Add(i);
|
|
}
|
|
|
|
RenderSteps();
|
|
BuildApprovalButtons();
|
|
_statusBar.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
public void LoadPlanPreview(string planText, List<string> steps)
|
|
{
|
|
_planText = planText;
|
|
_steps = steps;
|
|
_tcs = null;
|
|
_currentStep = -1;
|
|
_isExecuting = false;
|
|
_expandedSteps.Clear();
|
|
if (_uiExpressionLevel == "rich")
|
|
{
|
|
for (int i = 0; i < _steps.Count; i++)
|
|
_expandedSteps.Add(i);
|
|
}
|
|
|
|
RenderSteps();
|
|
BuildCloseButton();
|
|
_statusBar.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
public Task<string?> ShowPlanAsync(string planText, List<string> steps, TaskCompletionSource<string?> tcs)
|
|
{
|
|
LoadPlan(planText, steps, tcs);
|
|
Show();
|
|
Activate();
|
|
return tcs.Task;
|
|
}
|
|
|
|
public void SwitchToExecutionMode()
|
|
{
|
|
_isExecuting = true;
|
|
_statusBar.Visibility = Visibility.Visible;
|
|
_statusText.Text = "▶ 계획 실행 중...";
|
|
_progressText.Text = $"0 / {_steps.Count}";
|
|
BuildExecutionButtons();
|
|
}
|
|
|
|
public void UpdateCurrentStep(int stepIndex)
|
|
{
|
|
if (stepIndex < 0 || stepIndex >= _steps.Count) return;
|
|
_currentStep = stepIndex;
|
|
_progressText.Text = $"{stepIndex + 1} / {_steps.Count}";
|
|
_expandedSteps.Add(stepIndex); // 현재 실행 중인 단계는 자동 펼침
|
|
RenderSteps();
|
|
}
|
|
|
|
public void MarkComplete()
|
|
{
|
|
_isExecuting = false;
|
|
_statusText.Text = "✓ 계획 실행 완료";
|
|
_statusText.Foreground = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81));
|
|
_progressText.Text = $"{_steps.Count} / {_steps.Count}";
|
|
_currentStep = _steps.Count;
|
|
RenderSteps();
|
|
BuildCloseButton();
|
|
}
|
|
|
|
public string PlanText => _planText;
|
|
public List<string> Steps => _steps;
|
|
|
|
public string? BuildApprovedDecisionPayload(string prefix)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(prefix))
|
|
return null;
|
|
|
|
var normalized = _steps
|
|
.Select(step => step?.Trim() ?? "")
|
|
.Where(step => !string.IsNullOrWhiteSpace(step))
|
|
.ToList();
|
|
if (normalized.Count == 0)
|
|
return null;
|
|
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine(prefix.Trim());
|
|
foreach (var step in normalized)
|
|
sb.AppendLine(step);
|
|
return sb.ToString().TrimEnd();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════
|
|
// 단계 목록 렌더링
|
|
// ════════════════════════════════════════════════════════════
|
|
|
|
private void RenderSteps()
|
|
{
|
|
_stepsPanel.Children.Clear();
|
|
|
|
var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
|
var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
|
|
|
var canEdit = !_isExecuting && _currentStep < 0; // 승인 대기 중에만 편집/순서변경 가능
|
|
|
|
var summaryText = _uiExpressionLevel switch
|
|
{
|
|
"simple" => _isExecuting
|
|
? "진행률을 간단히 표시합니다."
|
|
: $"{_steps.Count}단계 계획입니다. 핵심 단계만 확인하고 승인하세요.",
|
|
"rich" => _isExecuting
|
|
? "현재 단계를 기준으로 진행률을 표시합니다. 필요 시 단계를 펼쳐 세부 내용을 확인할 수 있습니다."
|
|
: $"총 {_steps.Count}단계입니다. 단계별 내용을 열어 우선순위/의존성을 검토한 뒤 승인 또는 수정 요청을 선택하세요.",
|
|
_ => _isExecuting
|
|
? "현재 단계를 기준으로 진행률을 표시합니다."
|
|
: $"총 {_steps.Count}단계입니다. 단계를 펼쳐 검토한 후 승인 또는 수정 요청을 선택하세요.",
|
|
};
|
|
|
|
_stepsPanel.Children.Add(new Border
|
|
{
|
|
Background = new SolidColorBrush(Color.FromArgb(0x12,
|
|
((SolidColorBrush)accentBrush).Color.R,
|
|
((SolidColorBrush)accentBrush).Color.G,
|
|
((SolidColorBrush)accentBrush).Color.B)),
|
|
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40,
|
|
((SolidColorBrush)accentBrush).Color.R,
|
|
((SolidColorBrush)accentBrush).Color.G,
|
|
((SolidColorBrush)accentBrush).Color.B)),
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(10),
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
Margin = new Thickness(0, 0, 0, 8),
|
|
Child = new StackPanel
|
|
{
|
|
Children =
|
|
{
|
|
new TextBlock
|
|
{
|
|
Text = _isExecuting ? "계획 실행 중" : "계획 승인 대기",
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = accentBrush,
|
|
},
|
|
new TextBlock
|
|
{
|
|
Text = summaryText,
|
|
FontSize = 11.5,
|
|
Foreground = secondaryText,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Margin = new Thickness(0, 3, 0, 0),
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
for (int i = 0; i < _steps.Count; i++)
|
|
{
|
|
var step = _steps[i];
|
|
var capturedIdx = i;
|
|
var isComplete = i < _currentStep;
|
|
var isCurrent = i == _currentStep;
|
|
var isPending = i > _currentStep;
|
|
var isExpanded = _expandedSteps.Contains(i);
|
|
|
|
// ─ 카드 Border ─
|
|
var card = new Border
|
|
{
|
|
CornerRadius = new CornerRadius(10),
|
|
Padding = new Thickness(10, 7, 10, 7),
|
|
Margin = new Thickness(0, 0, 0, 5),
|
|
Background = isCurrent
|
|
? new SolidColorBrush(Color.FromArgb(0x18,
|
|
((SolidColorBrush)accentBrush).Color.R,
|
|
((SolidColorBrush)accentBrush).Color.G,
|
|
((SolidColorBrush)accentBrush).Color.B))
|
|
: itemBg,
|
|
BorderBrush = isCurrent ? accentBrush : Brushes.Transparent,
|
|
BorderThickness = new Thickness(isCurrent ? 1.5 : 0),
|
|
AllowDrop = canEdit,
|
|
};
|
|
|
|
// 열기/닫기 토글 — 텍스트 또는 배경 클릭
|
|
card.Cursor = Cursors.Hand;
|
|
card.MouseLeftButtonUp += (s, e) =>
|
|
{
|
|
// 드래그 직후 클릭이 발생하는 경우 무시
|
|
if (e.OriginalSource is Border src && src.Tag?.ToString() == "DragHandle") return;
|
|
if (_expandedSteps.Contains(capturedIdx)) _expandedSteps.Remove(capturedIdx);
|
|
else _expandedSteps.Add(capturedIdx);
|
|
RenderSteps();
|
|
};
|
|
|
|
// ─ 카드 Grid: [drag?][badge][*text][chevron][edit?] ─
|
|
var cardGrid = new Grid();
|
|
int badgeCol = canEdit ? 1 : 0;
|
|
int textCol = canEdit ? 2 : 1;
|
|
int chevCol = canEdit ? 3 : 2;
|
|
int editCol = canEdit ? 4 : -1;
|
|
|
|
if (canEdit)
|
|
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // drag
|
|
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // badge
|
|
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // text
|
|
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // chevron
|
|
if (canEdit)
|
|
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // edit btns
|
|
|
|
// ── 드래그 핸들 (편집 모드 전용) ──
|
|
if (canEdit)
|
|
{
|
|
var dimColor = Color.FromArgb(0x55, 0x80, 0x80, 0x80);
|
|
var dimBrush = new SolidColorBrush(dimColor);
|
|
var dragHandle = new Border
|
|
{
|
|
Tag = "DragHandle",
|
|
Width = 20, Cursor = Cursors.SizeAll,
|
|
Background = Brushes.Transparent,
|
|
Margin = new Thickness(0, 0, 6, 0),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Child = new TextBlock
|
|
{
|
|
Text = "\uE8FD", // Sort/Lines 아이콘 (드래그 핸들)
|
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 11,
|
|
Foreground = dimBrush,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
},
|
|
};
|
|
dragHandle.MouseEnter += (s, _) =>
|
|
((TextBlock)((Border)s).Child).Foreground = secondaryText;
|
|
dragHandle.MouseLeave += (s, _) =>
|
|
((TextBlock)((Border)s).Child).Foreground = dimBrush;
|
|
|
|
// 드래그 시작 — 마우스 눌림 위치 기록
|
|
dragHandle.PreviewMouseLeftButtonDown += (s, e) =>
|
|
{
|
|
_dragSourceIndex = capturedIdx;
|
|
_dragStartPoint = e.GetPosition(_stepsPanel);
|
|
e.Handled = true; // 카드 클릭(expand) 이벤트 방지
|
|
};
|
|
// 충분히 움직이면 DragDrop 시작
|
|
dragHandle.PreviewMouseMove += (s, e) =>
|
|
{
|
|
if (_dragSourceIndex < 0 || e.LeftButton != MouseButtonState.Pressed) return;
|
|
var cur = e.GetPosition(_stepsPanel);
|
|
if (Math.Abs(cur.X - _dragStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance ||
|
|
Math.Abs(cur.Y - _dragStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
|
|
{
|
|
int idx = _dragSourceIndex;
|
|
_dragSourceIndex = -1;
|
|
DragDrop.DoDragDrop((DependencyObject)s,
|
|
new DataObject(DragDataFormat, idx), DragDropEffects.Move);
|
|
// DoDragDrop 완료 후 비주얼 정리
|
|
Dispatcher.InvokeAsync(RenderSteps);
|
|
}
|
|
};
|
|
dragHandle.PreviewMouseLeftButtonUp += (_, _) => _dragSourceIndex = -1;
|
|
|
|
Grid.SetColumn(dragHandle, 0);
|
|
cardGrid.Children.Add(dragHandle);
|
|
|
|
// ── 카드 Drop 이벤트 ──
|
|
card.DragOver += (s, e) =>
|
|
{
|
|
if (!e.Data.GetDataPresent(DragDataFormat)) return;
|
|
int src = (int)e.Data.GetData(DragDataFormat);
|
|
if (src != capturedIdx)
|
|
{
|
|
((Border)s).BorderBrush = accentBrush;
|
|
((Border)s).BorderThickness = new Thickness(1.5);
|
|
e.Effects = DragDropEffects.Move;
|
|
}
|
|
else e.Effects = DragDropEffects.None;
|
|
e.Handled = true;
|
|
};
|
|
card.DragLeave += (s, _) =>
|
|
{
|
|
bool isCurr = _currentStep == capturedIdx;
|
|
((Border)s).BorderBrush = isCurr ? accentBrush : Brushes.Transparent;
|
|
((Border)s).BorderThickness = new Thickness(isCurr ? 1.5 : 0);
|
|
};
|
|
card.Drop += (s, e) =>
|
|
{
|
|
if (!e.Data.GetDataPresent(DragDataFormat)) { e.Handled = true; return; }
|
|
int srcIdx = (int)e.Data.GetData(DragDataFormat);
|
|
int dstIdx = capturedIdx;
|
|
if (srcIdx != dstIdx && srcIdx >= 0 && srcIdx < _steps.Count)
|
|
{
|
|
var item = _steps[srcIdx];
|
|
_steps.RemoveAt(srcIdx);
|
|
// srcIdx < dstIdx 이면 제거 후 인덱스가 1 감소
|
|
int insertAt = srcIdx < dstIdx ? dstIdx - 1 : dstIdx;
|
|
_steps.Insert(insertAt, item);
|
|
_expandedSteps.Clear();
|
|
RenderSteps();
|
|
}
|
|
e.Handled = true;
|
|
};
|
|
}
|
|
|
|
// ── 상태 배지 ──
|
|
UIElement badge;
|
|
if (isComplete)
|
|
{
|
|
badge = new TextBlock
|
|
{
|
|
Text = "\uE73E", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 13, Foreground = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81)),
|
|
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
|
Width = 20, TextAlignment = TextAlignment.Center,
|
|
};
|
|
}
|
|
else if (isCurrent)
|
|
{
|
|
badge = new TextBlock
|
|
{
|
|
Text = "\uE768", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 13, Foreground = accentBrush,
|
|
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
|
Width = 20, TextAlignment = TextAlignment.Center,
|
|
};
|
|
}
|
|
else
|
|
{
|
|
badge = new Border
|
|
{
|
|
Width = 22, Height = 22, CornerRadius = new CornerRadius(11),
|
|
Background = new SolidColorBrush(Color.FromArgb(0x25, 0x80, 0x80, 0x80)),
|
|
Margin = new Thickness(0, 0, 10, 0), VerticalAlignment = VerticalAlignment.Center,
|
|
Child = new TextBlock
|
|
{
|
|
Text = $"{i + 1}", FontSize = 11, Foreground = secondaryText,
|
|
FontWeight = FontWeights.SemiBold,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
},
|
|
};
|
|
}
|
|
Grid.SetColumn(badge, badgeCol);
|
|
cardGrid.Children.Add(badge);
|
|
|
|
// ── 단계 텍스트 ──
|
|
var textBlock = new TextBlock
|
|
{
|
|
Text = step,
|
|
FontSize = 13,
|
|
Foreground = isComplete ? secondaryText : primaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Opacity = isPending && _isExecuting ? 0.6 : 1.0,
|
|
TextDecorations = isComplete ? TextDecorations.Strikethrough : null,
|
|
Margin = new Thickness(0, 0, 4, 0),
|
|
};
|
|
if (isExpanded)
|
|
{
|
|
textBlock.TextWrapping = TextWrapping.Wrap;
|
|
textBlock.TextTrimming = TextTrimming.None;
|
|
}
|
|
else
|
|
{
|
|
textBlock.TextWrapping = TextWrapping.NoWrap;
|
|
textBlock.TextTrimming = TextTrimming.CharacterEllipsis;
|
|
textBlock.ToolTip = step; // 접힌 상태: 호버 시 전체 텍스트 툴팁
|
|
}
|
|
Grid.SetColumn(textBlock, textCol);
|
|
cardGrid.Children.Add(textBlock);
|
|
|
|
// ── 펼침/접힘 Chevron ──
|
|
var chevron = new Border
|
|
{
|
|
Width = 22, Height = 22, CornerRadius = new CornerRadius(4),
|
|
Background = Brushes.Transparent, Cursor = Cursors.Hand,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, canEdit ? 4 : 0, 0),
|
|
Child = new TextBlock
|
|
{
|
|
Text = isExpanded ? "\uE70E" : "\uE70D", // ChevronUp / ChevronDown
|
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 9,
|
|
Foreground = new SolidColorBrush(Color.FromArgb(0x70, 0x80, 0x80, 0x80)),
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
},
|
|
};
|
|
chevron.MouseEnter += (s, _) => ((Border)s).Background = hoverBg;
|
|
chevron.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
|
chevron.MouseLeftButtonUp += (_, e) =>
|
|
{
|
|
if (_expandedSteps.Contains(capturedIdx)) _expandedSteps.Remove(capturedIdx);
|
|
else _expandedSteps.Add(capturedIdx);
|
|
RenderSteps();
|
|
e.Handled = true;
|
|
};
|
|
Grid.SetColumn(chevron, chevCol);
|
|
cardGrid.Children.Add(chevron);
|
|
|
|
// ── 편집 버튼 (위/아래/편집/삭제) ──
|
|
if (canEdit)
|
|
{
|
|
var editBtnPanel = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(2, 0, 0, 0),
|
|
};
|
|
|
|
if (i > 0)
|
|
{
|
|
var upBtn = CreateMiniButton("\uE70E", secondaryText, hoverBg);
|
|
upBtn.ToolTip = "위로 이동";
|
|
upBtn.MouseLeftButtonUp += (_, e) => { SwapSteps(capturedIdx, capturedIdx - 1); e.Handled = true; };
|
|
editBtnPanel.Children.Add(upBtn);
|
|
}
|
|
if (i < _steps.Count - 1)
|
|
{
|
|
var downBtn = CreateMiniButton("\uE70D", secondaryText, hoverBg);
|
|
downBtn.ToolTip = "아래로 이동";
|
|
downBtn.MouseLeftButtonUp += (_, e) => { SwapSteps(capturedIdx, capturedIdx + 1); e.Handled = true; };
|
|
editBtnPanel.Children.Add(downBtn);
|
|
}
|
|
|
|
var editBtn = CreateMiniButton("\uE70F", accentBrush, hoverBg);
|
|
editBtn.ToolTip = "편집";
|
|
editBtn.MouseLeftButtonUp += (_, e) => { EditStep(capturedIdx); e.Handled = true; };
|
|
editBtnPanel.Children.Add(editBtn);
|
|
|
|
var delBtn = CreateMiniButton("\uE74D", new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)), hoverBg);
|
|
delBtn.ToolTip = "삭제";
|
|
delBtn.MouseLeftButtonUp += (_, e) =>
|
|
{
|
|
if (_steps.Count > 1)
|
|
{
|
|
_steps.RemoveAt(capturedIdx);
|
|
_expandedSteps.Remove(capturedIdx);
|
|
RenderSteps();
|
|
}
|
|
e.Handled = true;
|
|
};
|
|
editBtnPanel.Children.Add(delBtn);
|
|
|
|
Grid.SetColumn(editBtnPanel, editCol);
|
|
cardGrid.Children.Add(editBtnPanel);
|
|
}
|
|
|
|
card.Child = cardGrid;
|
|
_stepsPanel.Children.Add(card);
|
|
}
|
|
|
|
// ── 단계 추가 버튼 (편집 모드) ──
|
|
if (canEdit)
|
|
{
|
|
var st2 = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var hb2 = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
|
var addBtn = new Border
|
|
{
|
|
CornerRadius = new CornerRadius(10),
|
|
Padding = new Thickness(14, 8, 14, 8),
|
|
Margin = new Thickness(0, 4, 0, 0),
|
|
Background = Brushes.Transparent,
|
|
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0x80, 0x80, 0x80)),
|
|
BorderThickness = new Thickness(1),
|
|
Cursor = Cursors.Hand,
|
|
};
|
|
var addSp = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
|
|
addSp.Children.Add(new TextBlock
|
|
{
|
|
Text = "\uE710", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 12, Foreground = st2,
|
|
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
|
});
|
|
addSp.Children.Add(new TextBlock { Text = "단계 추가", FontSize = 12, Foreground = st2 });
|
|
addBtn.Child = addSp;
|
|
addBtn.MouseEnter += (s, _) => ((Border)s).Background = hb2;
|
|
addBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
|
addBtn.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
_steps.Add("새 단계");
|
|
RenderSteps();
|
|
EditStep(_steps.Count - 1);
|
|
};
|
|
_stepsPanel.Children.Add(addBtn);
|
|
}
|
|
|
|
// 현재 단계로 자동 스크롤
|
|
if (_currentStep >= 0 && _stepsPanel.Children.Count > _currentStep)
|
|
{
|
|
_stepsPanel.UpdateLayout();
|
|
var target = (FrameworkElement)_stepsPanel.Children[Math.Min(_currentStep, _stepsPanel.Children.Count - 1)];
|
|
target.BringIntoView();
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════
|
|
// 단계 편집 / 교환
|
|
// ════════════════════════════════════════════════════════════
|
|
|
|
private void SwapSteps(int a, int b)
|
|
{
|
|
if (a < 0 || b < 0 || a >= _steps.Count || b >= _steps.Count) return;
|
|
(_steps[a], _steps[b]) = (_steps[b], _steps[a]);
|
|
RenderSteps();
|
|
}
|
|
|
|
private void EditStep(int index)
|
|
{
|
|
if (index < 0 || index >= _steps.Count) return;
|
|
var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
|
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
|
|
if (index >= _stepsPanel.Children.Count) return;
|
|
|
|
var editCard = new Border
|
|
{
|
|
CornerRadius = new CornerRadius(10),
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
Margin = new Thickness(0, 0, 0, 5),
|
|
Background = itemBg,
|
|
BorderBrush = accentBrush,
|
|
BorderThickness = new Thickness(1.5),
|
|
};
|
|
|
|
var textBox = new TextBox
|
|
{
|
|
Text = _steps[index],
|
|
FontSize = 13,
|
|
Background = Brushes.Transparent,
|
|
Foreground = primaryText,
|
|
CaretBrush = primaryText,
|
|
BorderThickness = new Thickness(0),
|
|
AcceptsReturn = false,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Padding = new Thickness(4),
|
|
};
|
|
|
|
var capturedIdx = index;
|
|
textBox.KeyDown += (_, e) =>
|
|
{
|
|
if (e.Key == Key.Enter) { _steps[capturedIdx] = textBox.Text.Trim(); RenderSteps(); e.Handled = true; }
|
|
if (e.Key == Key.Escape) { RenderSteps(); e.Handled = true; }
|
|
};
|
|
textBox.LostFocus += (_, _) => { _steps[capturedIdx] = textBox.Text.Trim(); RenderSteps(); };
|
|
|
|
editCard.Child = textBox;
|
|
_stepsPanel.Children[index] = editCard;
|
|
textBox.Focus();
|
|
textBox.SelectAll();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════
|
|
// 하단 버튼 빌드
|
|
// ════════════════════════════════════════════════════════════
|
|
|
|
private void BuildApprovalButtons()
|
|
{
|
|
_btnPanel.Children.Clear();
|
|
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
|
|
var approveLabel = _uiExpressionLevel == "simple" ? "승인" : "승인 후 실행";
|
|
var editLabel = _uiExpressionLevel == "simple" ? "수정" : "수정 피드백";
|
|
var rejectLabel = _uiExpressionLevel == "simple" ? "취소" : "거부";
|
|
|
|
var approveBtn = CreateActionButton("\uE73E", approveLabel, accentBrush, Brushes.White, true);
|
|
approveBtn.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
_tcs?.TrySetResult(null);
|
|
SwitchToExecutionMode();
|
|
};
|
|
_btnPanel.Children.Add(approveBtn);
|
|
|
|
var editBtn = CreateActionButton("\uE70F", editLabel, accentBrush, accentBrush, false);
|
|
editBtn.MouseLeftButtonUp += (_, _) => ShowEditInput();
|
|
_btnPanel.Children.Add(editBtn);
|
|
|
|
var cancelBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
|
|
var cancelBtn = CreateActionButton("\uE711", rejectLabel, cancelBrush, cancelBrush, false);
|
|
cancelBtn.MouseLeftButtonUp += (_, _) => { _tcs?.TrySetResult("취소"); Hide(); };
|
|
_btnPanel.Children.Add(cancelBtn);
|
|
}
|
|
|
|
private void BuildExecutionButtons()
|
|
{
|
|
_btnPanel.Children.Clear();
|
|
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var hideBtn = CreateActionButton("\uE921", "숨기기", secondaryText,
|
|
Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, false);
|
|
hideBtn.MouseLeftButtonUp += (_, _) => Hide();
|
|
_btnPanel.Children.Add(hideBtn);
|
|
}
|
|
|
|
private void BuildCloseButton()
|
|
{
|
|
_btnPanel.Children.Clear();
|
|
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
var closeBtn = CreateActionButton("\uE73E", "닫기", accentBrush, Brushes.White, true);
|
|
closeBtn.MouseLeftButtonUp += (_, _) => Hide();
|
|
_btnPanel.Children.Add(closeBtn);
|
|
}
|
|
|
|
private void ShowEditInput()
|
|
{
|
|
var editPanel = new Border
|
|
{
|
|
Margin = new Thickness(20, 0, 20, 12),
|
|
Padding = new Thickness(12, 8, 12, 8),
|
|
CornerRadius = new CornerRadius(10),
|
|
Background = Application.Current.TryFindResource("ItemBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)),
|
|
};
|
|
var editStack = new StackPanel();
|
|
editStack.Children.Add(new TextBlock
|
|
{
|
|
Text = "수정 사항을 입력하세요:",
|
|
FontSize = 11.5,
|
|
Foreground = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
});
|
|
var textBox = new TextBox
|
|
{
|
|
MinHeight = 44,
|
|
MaxHeight = 120,
|
|
AcceptsReturn = true,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
FontSize = 13,
|
|
Background = Application.Current.TryFindResource("LauncherBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)),
|
|
Foreground = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
|
CaretBrush = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
|
BorderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
|
BorderThickness = new Thickness(1),
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
};
|
|
editStack.Children.Add(textBox);
|
|
|
|
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
var sendBtn = new Border
|
|
{
|
|
Background = accentBrush,
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(14, 6, 14, 6),
|
|
Margin = new Thickness(0, 8, 0, 0),
|
|
Cursor = Cursors.Hand,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
Child = new TextBlock
|
|
{
|
|
Text = "전송", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White,
|
|
},
|
|
};
|
|
sendBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
|
sendBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
|
sendBtn.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
var feedback = textBox.Text.Trim();
|
|
if (string.IsNullOrEmpty(feedback)) return;
|
|
_tcs?.TrySetResult(feedback);
|
|
};
|
|
editStack.Children.Add(sendBtn);
|
|
editPanel.Child = editStack;
|
|
|
|
if (_btnPanel.Parent is Grid parentGrid)
|
|
{
|
|
for (int i = parentGrid.Children.Count - 1; i >= 0; i--)
|
|
{
|
|
if (parentGrid.Children[i] is Border b && b.Tag?.ToString() == "EditPanel")
|
|
parentGrid.Children.RemoveAt(i);
|
|
}
|
|
editPanel.Tag = "EditPanel";
|
|
Grid.SetRow(editPanel, 4); // row 4 = 하단 버튼 행 (toolBar 추가로 1 증가)
|
|
parentGrid.Children.Add(editPanel);
|
|
_btnPanel.Margin = new Thickness(20, 0, 20, 16);
|
|
textBox.Focus();
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════
|
|
// 공통 버튼 팩토리
|
|
// ════════════════════════════════════════════════════════════
|
|
|
|
private static Border CreateMiniButton(string icon, Brush fg, Brush hoverBg)
|
|
{
|
|
var btn = new Border
|
|
{
|
|
Width = 24, Height = 24,
|
|
CornerRadius = new CornerRadius(6),
|
|
Background = Brushes.Transparent,
|
|
Cursor = Cursors.Hand,
|
|
Margin = new Thickness(1, 0, 1, 0),
|
|
Child = new TextBlock
|
|
{
|
|
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 10, Foreground = fg,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
},
|
|
};
|
|
btn.MouseEnter += (s, _) => ((Border)s).Background = hoverBg;
|
|
btn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
|
return btn;
|
|
}
|
|
|
|
private static Border CreateActionButton(string icon, string text, Brush borderColor,
|
|
Brush textColor, bool filled)
|
|
{
|
|
var color = ((SolidColorBrush)borderColor).Color;
|
|
var btn = new Border
|
|
{
|
|
CornerRadius = new CornerRadius(12),
|
|
Padding = new Thickness(16, 8, 16, 8),
|
|
Margin = new Thickness(4, 0, 4, 0),
|
|
Cursor = Cursors.Hand,
|
|
Background = filled ? borderColor
|
|
: new SolidColorBrush(Color.FromArgb(0x18, color.R, color.G, color.B)),
|
|
BorderBrush = filled ? Brushes.Transparent
|
|
: new SolidColorBrush(Color.FromArgb(0x80, color.R, color.G, color.B)),
|
|
BorderThickness = new Thickness(filled ? 0 : 1.2),
|
|
};
|
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 12, Foreground = filled ? Brushes.White : textColor,
|
|
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
|
});
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = text, FontSize = 12.5, FontWeight = FontWeights.SemiBold,
|
|
Foreground = filled ? Brushes.White : textColor,
|
|
});
|
|
btn.Child = sp;
|
|
btn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
|
btn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
|
return btn;
|
|
}
|
|
|
|
private static string ResolveUiExpressionLevel()
|
|
{
|
|
if (Application.Current is App app)
|
|
return NormalizeUiExpressionLevel(app.SettingsService?.Settings?.Llm.AgentUiExpressionLevel);
|
|
|
|
return "balanced";
|
|
}
|
|
|
|
private static string NormalizeUiExpressionLevel(string? value)
|
|
{
|
|
return (value ?? "balanced").Trim().ToLowerInvariant() switch
|
|
{
|
|
"rich" => "rich",
|
|
"simple" => "simple",
|
|
_ => "balanced",
|
|
};
|
|
}
|
|
}
|