쿼리 편집기 세 가지 — 한글이 사라지던 것, 줄 번호가 밀리던 것, 클릭이 먹지 않던 것

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:
Msystech
2026-08-13 17:32:48 +09:00
co-authored by Claude Opus 5
parent 6ce055b00c
commit 749de6b566
5 changed files with 301 additions and 24 deletions
@@ -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>
/// 저장 전에 눈에 띄어야 하는 것만 알린다 — 닫히지 않은 조각과 축약된 치환 변수.
///