쿼리 편집기 세 가지 — 한글이 사라지던 것, 줄 번호가 밀리던 것, 클릭이 먹지 않던 것
1) 한글로 입력하면 글자가 안 보였다. 색칠은 "투명한 TextBox 뒤에 우리가 그린다"로 되어 있다. 그런데 IME 조합 중인 글자는 아직 TextBox.Text 에 없다 — TextChanged 가 오지 않으니 우리는 그릴 수 없고, TextBox 는 제 Foreground(투명)로 그리니 아무것도 안 보인다. 자음·모음을 맞추는 내내 빈 화면이었고, 확정하고 나서야 나타났다. 조합이 시작되면 TextBox 에게 잉크색을 돌려주고 색칠 층을 감춘다(ShowRawText). 확정되면(TextInput) 되돌리고, ESC 로 취소돼 CompositionText 가 비어도 되돌린다. 조합 도중 포커스를 잃으면 끝 신호가 오지 않으므로 blur 에서도 되돌린다 — 색 없는 상태로 굳지 않게. 조합 중 잠깐 단색이 되는 편이 글자가 안 보이는 것보다 낫다. 2) 줄 번호가 실제 줄과 어긋났다. 번호를 "1\n2\n3…" TextBlock 한 덩어리로 두면 번호의 줄 간격은 TextBlock 조판이, 본문의 줄 간격은 TextBox 가 정한다. 한글 글꼴 대체가 끼면 조금씩 벌어져 아래로 갈수록 눈에 띄게 밀렸다. LineNumberGutter 를 만들어 각 줄 첫 글자의 y 를 GetRectFromCharacterIndex 로 물어 그 자리에 번호를 그린다. 스크롤·여백·폰트 대체가 전부 TextBox 기준으로 반영돼 오므로 구조적으로 어긋날 수 없다. 폭은 가장 큰 번호에 맞춰 잡고 번호는 오른쪽 정렬한다. 3) 자동완성 목록의 항목을 클릭하면 아무것도 안 들어가고 목록만 닫혔다. Popup 이 StaysOpen=False 라 마우스를 캡처하고 있었다. 항목을 누른 그 클릭이 '바깥 클릭'으로 먼저 소비돼 팝업이 닫히고, 항목은 클릭을 받지 못했다. StaysOpen=True 로 바꾸고 닫는 일은 코드가 맡는다(커서 이동·포커스 상실·창 비활성 — 이미 있던 핸들러들). 집는 처리는 팝업 안쪽 Border 에서 터널 단계로 받는다. 진단(--query-popup)에 "목록 항목을 집으면 입력되고 목록이 닫힌다"를 더해 17건. 마우스 라우팅 자체는 화면 없이 흉내 낼 수 없어(Ctrl+Enter 때와 같은 이유) 핸들러가 부르는 경로를 그대로 태워 삽입까지 확인한다. IME 조합은 합성할 수 없어 1) 은 실기 확인이 필요하다. 게이트: 테스트 230/230, --edit-smoke 0건, --db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6ce055b00c
commit
749de6b566
@@ -333,23 +333,27 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 줄 번호 홈통 — 회색 면 + 곧은 세로 구분선 -->
|
||||
<Border Grid.Column="0" Background="{DynamicResource B.Chip}" Padding="12,10,10,10"
|
||||
<!-- 줄 번호 홈통 — 회색 면 + 곧은 세로 구분선.
|
||||
번호는 TextBox 가 알려 준 줄 위치에 그린다(TextBlock 한 덩어리로 두면
|
||||
본문과 줄 간격이 달라 아래로 갈수록 어긋난다). -->
|
||||
<Border Grid.Column="0" Background="{DynamicResource B.Chip}" Padding="12,0,0,0"
|
||||
BorderThickness="0,0,1,0" BorderBrush="{DynamicResource B.Line2}">
|
||||
<TextBlock x:Name="LineNumbers" FontFamily="D2Coding, 굴림체, Consolas" FontSize="13.5"
|
||||
Foreground="{DynamicResource B.Muted}" TextAlignment="Right" MinWidth="20"/>
|
||||
<ctl:LineNumberGutter x:Name="LineNumbers" IsHitTestVisible="False"/>
|
||||
</Border>
|
||||
<Grid Grid.Column="1">
|
||||
<ctl:SqlHighlightLayer x:Name="Highlight" IsHitTestVisible="False"/>
|
||||
<!-- StaysOpen=False 여야 바깥을 클릭했을 때 닫힌다 -->
|
||||
<!-- StaysOpen=True 다. False 로 두면 Popup 이 마우스를 캡처해
|
||||
항목을 누른 그 클릭이 '바깥 클릭'으로 먼저 소비돼 목록만 닫히고
|
||||
아무것도 입력되지 않는다. 대신 커서 이동·포커스 상실·스크롤·창 비활성에서
|
||||
코드가 직접 닫는다(위 핸들러들). -->
|
||||
<Popup x:Name="CompletionPopup" Placement="Relative" AllowsTransparency="True"
|
||||
StaysOpen="False" PopupAnimation="None" Focusable="False">
|
||||
StaysOpen="True" PopupAnimation="None" Focusable="False">
|
||||
<Border Background="{DynamicResource B.Surface}" BorderThickness="1"
|
||||
BorderBrush="{DynamicResource B.InputBorder}" CornerRadius="8" Padding="3">
|
||||
BorderBrush="{DynamicResource B.Line2}" CornerRadius="8" Padding="3"
|
||||
PreviewMouseLeftButtonDown="OnCompletionPicked">
|
||||
<ListBox x:Name="CompletionList" MaxHeight="260" MinWidth="280" FontSize="12"
|
||||
BorderThickness="0" Background="Transparent"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
MouseDoubleClick="OnCompletionPicked">
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DockPanel MinWidth="260">
|
||||
|
||||
@@ -47,6 +47,9 @@ public partial class QueryEditorWindow : Window
|
||||
/// </summary>
|
||||
private bool typing;
|
||||
|
||||
/// <summary>한글 조합 중인가 — 조합 중에는 색칠을 접고 TextBox 가 제 글자를 그린다</summary>
|
||||
private bool composing;
|
||||
|
||||
/// <summary>목록이 걸려 있는 낱말의 끝 — 커서가 이 범위를 벗어나면 목록은 더 이상 유효하지 않다</summary>
|
||||
private int completionEnd;
|
||||
|
||||
@@ -109,6 +112,11 @@ public partial class QueryEditorWindow : Window
|
||||
{
|
||||
Title = $"쿼리 편집 — {ownerLabel}";
|
||||
Highlight.Source = SqlBox;
|
||||
LineNumbers.Source = SqlBox;
|
||||
// 한글 조합(IME) 동안에는 색칠을 잠시 접고 TextBox 가 제 글자를 그리게 한다 — 아래 설명
|
||||
TextCompositionManager.AddTextInputStartHandler(SqlBox, OnComposing);
|
||||
TextCompositionManager.AddTextInputUpdateHandler(SqlBox, OnComposing);
|
||||
TextCompositionManager.AddTextInputHandler(SqlBox, OnComposed);
|
||||
RebuildBucketChips();
|
||||
ApplyVariableFilter(string.Empty);
|
||||
var connection = Services.ConfigService.Current.ConnectionString;
|
||||
@@ -228,7 +236,14 @@ public partial class QueryEditorWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSqlLostFocus(object sender, RoutedEventArgs e) => CompletionPopup.IsOpen = false;
|
||||
private void OnSqlLostFocus(object sender, RoutedEventArgs e) => CloseOnBlur();
|
||||
|
||||
private void CloseOnBlur()
|
||||
{
|
||||
CompletionPopup.IsOpen = false;
|
||||
// 조합 도중 포커스를 잃으면 조합 끝 신호가 오지 않는다 — 색칠 없는 상태로 굳지 않게 되돌린다
|
||||
ShowRawText(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스크롤로 자리가 움직였을 때 목록을 새 자리에 다시 놓는다.
|
||||
@@ -374,7 +389,49 @@ public partial class QueryEditorWindow : Window
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnCompletionPicked(object sender, MouseButtonEventArgs e) => AcceptCompletion();
|
||||
/// <summary>
|
||||
/// 목록 항목 클릭 — 누르는 즉시 넣는다.
|
||||
///
|
||||
/// 예전에는 더블클릭이었는데, 클릭이 편집기에서 포커스를 가져가면서 LostFocus 가 팝업을 닫아
|
||||
/// 아무것도 입력되지 않고 사라졌다. 여기서 눌린 항목을 직접 찾아 처리하고 이벤트를 소비해
|
||||
/// 포커스가 아예 옮겨 가지 않게 한다.
|
||||
/// </summary>
|
||||
private void OnCompletionPicked(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is DependencyObject origin && PickCompletionAt(origin))
|
||||
{
|
||||
// 소비하지 않으면 ListBoxItem 이 포커스를 가져가고 편집기가 LostFocus 를 받는다
|
||||
e.Handled = true;
|
||||
SqlBox.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 눌린 지점에서 항목을 찾아 넣는다 — 처리했으면 true.
|
||||
/// 클릭 지점은 항목 안의 글자일 수도, 항목 컨테이너 자체일 수도 있다.
|
||||
/// </summary>
|
||||
private bool PickCompletionAt(DependencyObject origin)
|
||||
{
|
||||
var container = ItemsControl.ContainerFromElement(CompletionList, origin) as ListBoxItem;
|
||||
if (container?.DataContext is not SqlCompletionItem item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
CompletionList.SelectedItem = item;
|
||||
AcceptCompletion();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 진단(--query-popup)에서 '항목 클릭'을 부르는 통로.
|
||||
/// WPF 의 마우스 라우팅은 화면 없이 흉내 낼 수 없어(합성 이벤트가 캡처·히트테스트를 못 거친다)
|
||||
/// 핸들러 본문을 그대로 태워 삽입 로직만 확인한다.
|
||||
/// </summary>
|
||||
internal bool PickCompletionForSmoke(int index)
|
||||
{
|
||||
var container = CompletionList.ItemContainerGenerator.ContainerFromIndex(index);
|
||||
return container is DependencyObject origin && PickCompletionAt(origin);
|
||||
}
|
||||
|
||||
/// <summary>목록이 떠 있는 동안의 키 처리 — 처리했으면 true</summary>
|
||||
private bool HandleCompletionKey(KeyEventArgs e)
|
||||
@@ -494,6 +551,36 @@ public partial class QueryEditorWindow : Window
|
||||
#endregion
|
||||
|
||||
#region Methods - Editing
|
||||
/// <summary>
|
||||
/// 한글을 치는 동안(IME 조합 중) 글자가 사라지던 문제.
|
||||
///
|
||||
/// 색칠은 "투명한 TextBox 뒤에 우리가 그린다"로 되어 있다. 그런데 조합 중인 글자는
|
||||
/// 아직 <see cref="TextBox.Text"/> 에 들어오지 않는다 — TextChanged 가 안 오니 우리는 그릴 수 없고,
|
||||
/// TextBox 는 제 Foreground(투명)로 그리니 아무것도 안 보인다. 자음·모음을 맞추는 내내 빈 화면이었다.
|
||||
///
|
||||
/// 그래서 조합이 시작되면 TextBox 에게 잉크색을 돌려주고 색칠 층을 감춘다. 조합이 끝나
|
||||
/// 글자가 확정되면(TextInput → 이어서 TextChanged) 원래대로 되돌린다. 조합 중 잠깐 단색이 되는
|
||||
/// 것이 글자가 안 보이는 것보다 낫다.
|
||||
/// </summary>
|
||||
private void OnComposing(object? sender, TextCompositionEventArgs e)
|
||||
// 조합이 취소되면(ESC) CompositionText 가 비면서 여기로 온다 — 그때는 바로 색칠로 돌아간다
|
||||
=> ShowRawText(e.TextComposition.CompositionText.Length > 0);
|
||||
|
||||
private void OnComposed(object? sender, TextCompositionEventArgs e) => ShowRawText(false);
|
||||
|
||||
private void ShowRawText(bool raw)
|
||||
{
|
||||
if (composing == raw)
|
||||
{
|
||||
return;
|
||||
}
|
||||
composing = raw;
|
||||
SqlBox.Foreground = raw
|
||||
? (System.Windows.Media.Brush)FindResource("B.Ink")
|
||||
: System.Windows.Media.Brushes.Transparent;
|
||||
Highlight.Visibility = raw ? Visibility.Hidden : Visibility.Visible;
|
||||
}
|
||||
|
||||
private void OnSqlChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
Refresh();
|
||||
@@ -516,7 +603,6 @@ public partial class QueryEditorWindow : Window
|
||||
var text = SqlBox.Text;
|
||||
LengthText.Text = $"{text.Length:N0}자 · {CountLines(text)}줄";
|
||||
UpdateCaretText();
|
||||
UpdateLineNumbers(text);
|
||||
UpdateProblems(text);
|
||||
}
|
||||
|
||||
@@ -581,17 +667,6 @@ public partial class QueryEditorWindow : Window
|
||||
return lines;
|
||||
}
|
||||
|
||||
private void UpdateLineNumbers(string text)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var count = CountLines(text);
|
||||
for (var i = 1; i <= count; i++)
|
||||
{
|
||||
builder.Append(i).Append('\n');
|
||||
}
|
||||
LineNumbers.Text = builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 저장 전에 눈에 띄어야 하는 것만 알린다 — 닫히지 않은 조각과 축약된 치환 변수.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user