Files
AX-Copilot/src/AxCopilot/Views/PlanViewerWindow.StepRenderer.cs
lacvet 5bed67f64e [Phase50] PlanViewerWindow·SettingsWindow 분리 — 6개 파일
변경 파일:
- PlanViewerWindow.StepRenderer.cs: 616 → 425줄 (RenderSteps/SwapSteps/EditStep 유지)
- PlanViewerWindow.EditButtons.cs (신규): BuildApprovalButtons, BuildExecutionButtons,
  BuildCloseButton, ShowEditInput, CreateMiniButton, CreateActionButton (197줄)
- SettingsWindow.AgentConfig.cs: 608 → 303줄 (모델/스킬/템플릿 관리 유지)
- SettingsWindow.AiToggle.cs (신규): ApplyAiEnabledState, AiEnabled_Changed,
  NetworkMode_Changed, StepApprovalCheckBox_Checked, BtnClearMemory_Click (316줄)
- SettingsWindow.AgentHooks.cs: 605 → 334줄 (훅 관리 유지)
- SettingsWindow.McpAdvanced.cs (신규): MCP 서버 관리, 감사 로그, 폴백 모델,
  LoadAdvancedSettings/SaveAdvancedSettings (271줄)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 21:26:50 +09:00

426 lines
20 KiB
C#

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace AxCopilot.Views;
internal sealed partial class PlanViewerWindow
{
// ════════════════════════════════════════════════════════════
// 단계 목록 렌더링
// ════════════════════════════════════════════════════════════
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; // 승인 대기 중에만 편집/순서변경 가능
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 = ThemeResourceHelper.SegoeMdl2,
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 = ThemeResourceHelper.SegoeMdl2,
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 = ThemeResourceHelper.SegoeMdl2,
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 = ThemeResourceHelper.SegoeMdl2,
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 = ThemeResourceHelper.SegoeMdl2,
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();
}
}