스크롤 맨아래 이동 버튼 위치와 입력바 연동 보정

AX Agent의 스크롤 맨아래 이동 FAB가 입력창 아래로 잘려 보이던 문제를 수정했습니다. ChatWindow.xaml에서 버튼을 Grid.RowSpan=2로 옮기고 기본 하단 여백을 높여 메시지 영역과 입력 바를 함께 기준으로 배치되도록 맞췄습니다.

ChatWindow.xaml.cs에는 UpdateScrollToBottomFabPosition()을 추가해 ComposerShell 높이, 입력창 크기 변화, 창 리사이즈 시 FAB 하단 margin을 자동 계산하도록 연결했습니다. 함께 Loaded 구간의 InputBox/InputBorder 이벤트 연결을 null-safe로 정리해 경고 없이 같은 UI 흐름을 유지하도록 보강했습니다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_scroll_to_bottom_fab\ -p:IntermediateOutputPath=obj\verify_scroll_to_bottom_fab\ (경고 0 / 오류 0), dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter ChatWindowSlashPolicyTests -p:OutputPath=bin\verify_scroll_to_bottom_fab_tests\ -p:IntermediateOutputPath=obj\verify_scroll_to_bottom_fab_tests\ (통과 59)
This commit is contained in:
2026-04-16 00:02:50 +09:00
parent 5161e46ac2
commit 13061fa3ca
4 changed files with 98 additions and 54 deletions

View File

@@ -1764,10 +1764,10 @@
</ListBox>
<!-- ── 스크롤투바텀 FAB (Claude 스타일) ── -->
<Button x:Name="ScrollToBottomFab" Grid.Row="3"
<Button x:Name="ScrollToBottomFab" Grid.Row="3" Grid.RowSpan="2"
Visibility="Collapsed"
HorizontalAlignment="Right" VerticalAlignment="Bottom"
Margin="0,0,28,16" Width="36" Height="36"
Margin="0,0,28,96" Width="36" Height="36"
Style="{StaticResource GhostBtn}"
Click="ScrollToBottomFab_Click"
ToolTip="아래로 스크롤" Cursor="Hand"

View File

@@ -562,7 +562,13 @@ public partial class ChatWindow : Window
UpdateTopicPresetScrollMode();
UpdateResponsiveChatLayout();
UpdateInputBoxHeight();
InputBox.Focus();
UpdateScrollToBottomFabPosition();
if (ComposerShell != null)
ComposerShell.SizeChanged += (_, _) => UpdateScrollToBottomFabPosition();
if (InputBox != null)
InputBox.SizeChanged += (_, _) => UpdateScrollToBottomFabPosition();
SizeChanged += (_, _) => UpdateScrollToBottomFabPosition();
InputBox?.Focus();
// ── 무거운 작업은 유휴 시점에 비동기 실행 ──
// A-1: 패널 이벤트 위임 1회 초기화 — 개별 람다 대신 부모 레벨에서 처리
InitConversationPanelDelegation();
@@ -618,59 +624,65 @@ public partial class ChatWindow : Window
// 입력 바 포커스 글로우 효과
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var inputFocusBorderBrush = TryFindResource("InputFocusBorderColor") as Brush ?? borderBrush;
InputBox.GotFocus += (_, _) => InputBorder.BorderBrush = inputFocusBorderBrush;
InputBox.LostFocus += (_, _) => InputBorder.BorderBrush = borderBrush;
if (InputBox != null && InputBorder != null)
{
InputBox.GotFocus += (_, _) => InputBorder.BorderBrush = inputFocusBorderBrush;
InputBox.LostFocus += (_, _) => InputBorder.BorderBrush = borderBrush;
}
// 드래그 앤 드롭 파일 첨부 — Claude Desktop 스타일
InputBorder.AllowDrop = true;
Brush? _dragOverOriginalBorder = null;
InputBorder.DragEnter += (_, de) =>
if (InputBorder != null)
{
if (de.Data.GetDataPresent(DataFormats.FileDrop))
InputBorder.AllowDrop = true;
Brush? _dragOverOriginalBorder = null;
InputBorder.DragEnter += (_, de) =>
{
_dragOverOriginalBorder = InputBorder.BorderBrush;
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
InputBorder.BorderBrush = accent;
InputBorder.BorderThickness = new Thickness(2);
}
};
InputBorder.DragLeave += (_, _) =>
{
if (_dragOverOriginalBorder != null)
if (de.Data.GetDataPresent(DataFormats.FileDrop))
{
_dragOverOriginalBorder = InputBorder.BorderBrush;
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
InputBorder.BorderBrush = accent;
InputBorder.BorderThickness = new Thickness(2);
}
};
InputBorder.DragLeave += (_, _) =>
{
InputBorder.BorderBrush = _dragOverOriginalBorder;
InputBorder.BorderThickness = new Thickness(1);
_dragOverOriginalBorder = null;
}
};
InputBorder.DragOver += (_, de) =>
{
de.Effects = de.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
de.Handled = true;
};
InputBorder.Drop += (_, de) =>
{
// 드래그 오버 하이라이트 복원
if (_dragOverOriginalBorder != null)
if (_dragOverOriginalBorder != null)
{
InputBorder.BorderBrush = _dragOverOriginalBorder;
InputBorder.BorderThickness = new Thickness(1);
_dragOverOriginalBorder = null;
}
};
InputBorder.DragOver += (_, de) =>
{
InputBorder.BorderBrush = _dragOverOriginalBorder;
InputBorder.BorderThickness = new Thickness(1);
_dragOverOriginalBorder = null;
}
de.Effects = de.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
de.Handled = true;
};
InputBorder.Drop += (_, de) =>
{
// 드래그 오버 하이라이트 복원
if (_dragOverOriginalBorder != null)
{
InputBorder.BorderBrush = _dragOverOriginalBorder;
InputBorder.BorderThickness = new Thickness(1);
_dragOverOriginalBorder = null;
}
if (de.Data.GetData(DataFormats.FileDrop) is string[] files && files.Length > 0)
{
// AI 액션 팝업은 Cowork/Code 탭에서만, 그 외에는 항상 단순 첨부
var enableAi = _settings.Settings.Llm.EnableDragDropAiActions
&& _activeTab is "Cowork" or "Code";
if (enableAi && files.Length <= 5)
ShowDropActionMenu(files);
else
foreach (var f in files) AddAttachedFile(f);
if (de.Data.GetData(DataFormats.FileDrop) is string[] files && files.Length > 0)
{
// AI 액션 팝업은 Cowork/Code 탭에서만, 그 외에는 항상 단순 첨부
var enableAi = _settings.Settings.Llm.EnableDragDropAiActions
&& _activeTab is "Cowork" or "Code";
if (enableAi && files.Length <= 5)
ShowDropActionMenu(files);
else
foreach (var f in files) AddAttachedFile(f);
InputBox.Focus();
}
};
InputBox?.Focus();
}
};
}
// 스킬 시스템 초기화
if (_settings.Settings.Llm.EnableSkillSystem)
@@ -684,16 +696,19 @@ public partial class ChatWindow : Window
SlashChipClose.MouseLeftButtonUp += (_, _) =>
{
HideSlashChip(restoreText: true);
InputBox.Focus();
InputBox?.Focus();
};
// InputBox에서 슬래시 팝업 열린 상태로 마우스 휠 → 팝업 스크롤
InputBox.PreviewMouseWheel += (_, me) =>
if (InputBox != null)
{
if (!SlashPopup.IsOpen) return;
me.Handled = true;
SlashPopup_ScrollByDelta(me.Delta);
};
InputBox.PreviewMouseWheel += (_, me) =>
{
if (!SlashPopup.IsOpen) return;
me.Handled = true;
SlashPopup_ScrollByDelta(me.Delta);
};
}
// 탭 UI 초기 상태
UpdateFolderBar();
@@ -979,7 +994,10 @@ public partial class ChatWindow : Window
var atBottom = GetTranscriptVerticalOffset() >= GetTranscriptScrollableHeight() - 120;
_userScrolled = !atBottom;
if (ScrollToBottomFab != null)
{
UpdateScrollToBottomFabPosition();
ScrollToBottomFab.Visibility = _userScrolled ? Visibility.Visible : Visibility.Collapsed;
}
}
private void ScrollToBottomFab_Click(object sender, RoutedEventArgs e)
@@ -987,6 +1005,16 @@ public partial class ChatWindow : Window
ForceScrollToEnd();
}
private void UpdateScrollToBottomFabPosition()
{
if (ScrollToBottomFab == null)
return;
var composerHeight = ComposerShell?.ActualHeight ?? 0;
var bottomOffset = Math.Max(96, composerHeight + 18);
ScrollToBottomFab.Margin = new Thickness(0, 0, 28, bottomOffset);
}
private void AutoScrollIfNeeded()
{
if (!_userScrolled)