쿼리 편집기 — 한글에서 색과 글자가 어긋나던 문제, 다크 제목 표시줄, 목업 맞춤
■ 한글을 치면 색칠이 글자와 어긋났다 (가장 중요) 색칠 레이어가 글자 위치를 <b>직접 재서</b> 잡고 있었다. 앞부분을 FormattedText 로 측정해 그 폭만큼 오른쪽으로 옮겨 그리는 방식이다. ASCII 에서는 맞았지만 한글에서 무너졌다 — 편집 글꼴(Consolas)에 한글 글리프가 없어 폰트 대체가 일어나는데, TextBox 의 내부 조판과 이쪽 측정이 같은 글꼴·같은 셰이핑을 고르리라는 보장이 없다. 조금만 달라도 뒤로 갈수록 벌어져 색과 글자가 따로 논다. 이제 <b>TextBox 에게 좌표를 묻는다</b>(GetRectFromCharacterIndex). 여백·스크롤·줄 높이·폰트 대체가 전부 TextBox 기준으로 이미 반영돼 오고, 조각마다 위치를 새로 물으므로 오차가 누적되지 않는다. 줄 높이를 손으로 계산하던 코드도 함께 사라졌다. 편집 글꼴 순서도 한글이 있는 것(D2Coding → 굴림체 → Consolas)으로 바꿔 대체 자체를 줄였다. --query-popup 진단에 정렬 검사 2건을 넣었다: 한글이 섞인 줄에서 글자 좌표가 뒤로 밀리지 않는가, 마지막 글자 다음 좌표가 캐럿 자리와 이어지는가. 현재 9/9. ■ 다크 모드인데 제목 표시줄만 밝게 남던 문제 ThemedWindow 스타일은 창 <b>안쪽</b>만 칠한다. 제목 표시줄은 OS 가 그리고 기본값은 Windows 의 테마를 따르므로, 앱을 다크로 바꿔도 Windows 가 라이트면 흰 띠가 남는다. DWM 속성(DWMWA_USE_IMMERSIVE_DARK_MODE)으로 창별로 지정한다. 커스텀 크롬을 직접 그리는 것보다 훨씬 싸고, 최소화·최대화·닫기의 동작과 접근성이 OS 것 그대로 남는다. 창마다 손으로 넣으면 새 대화상자에서 반드시 빠지므로 App 에서 Window 클래스 핸들러로 한 번만 걸었다 — 지금 있는 창과 앞으로 만들 창 모두 자동으로 적용된다. 테마를 바꾸는 순간 이미 떠 있는 창들도 함께 맞춘다(ThemeManager.ApplyChromeToOpenWindows). ■ 목업과 위치 맞춤 앞 배지(아이콘) · 검색칸의 돋보기와 안내 문구 · 안내 상자의 ⓘ 를 넣었다. 갈래 칩 순서는 개수순에서 <b>고정 순서</b>(환자·서식·작업자·컨트롤)로 바꿨다 — 컨트롤 수는 서식마다 달라서 개수순으로 두면 서식을 바꿀 때마다 칩이 자리를 바꿔 손이 기억하지 못한다. 회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬 점검 9/9, 검증 실행 점검 10/10, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
68fc01c6ba
commit
3e26af8739
@@ -58,6 +58,18 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
Services.ThemeManager.LoadSaved();
|
||||
|
||||
// 제목 표시줄은 OS 가 그린다 — 창이 만들어질 때마다 앱 테마에 맞춰 준다.
|
||||
// 창마다 손으로 넣으면 새 대화상자에서 반드시 빠지므로 클래스 단위로 한 번만 건다.
|
||||
EventManager.RegisterClassHandler(typeof(Window), Window.LoadedEvent,
|
||||
new RoutedEventHandler((sender, _) =>
|
||||
{
|
||||
if (sender is Window window)
|
||||
{
|
||||
Services.WindowChromeTheme.Apply(window, !Services.ThemeManager.IsLight);
|
||||
}
|
||||
}));
|
||||
|
||||
var main = new MainView();
|
||||
if (launch?.ShtCod.Length > 0 && main.DataContext is ViewModels.MainViewModel viewModel)
|
||||
{
|
||||
|
||||
@@ -146,104 +146,87 @@ public sealed class SqlHighlightLayer : FrameworkElement
|
||||
var tokens = SqlTokenizer.Tokenize(text);
|
||||
var typeface = new Typeface(source.FontFamily, source.FontStyle, source.FontWeight, source.FontStretch);
|
||||
var dpi = VisualTreeHelper.GetDpi(this).PixelsPerDip;
|
||||
|
||||
// TextBox 의 내부 여백 + 스크롤 오프셋만큼 옮겨 그린다
|
||||
var offsetX = source.Padding.Left + source.BorderThickness.Left - (scroller?.HorizontalOffset ?? 0);
|
||||
var offsetY = source.Padding.Top + source.BorderThickness.Top - (scroller?.VerticalOffset ?? 0);
|
||||
|
||||
// 줄 높이는 실제 글꼴에서 잰다 — 상수로 두면 글꼴이 바뀔 때 어긋난다
|
||||
var probe = new FormattedText("Ag", CultureInfo.InvariantCulture, FlowDirection.LeftToRight,
|
||||
typeface, source.FontSize, Brushes.Black, dpi);
|
||||
var lineHeight = probe.Height;
|
||||
|
||||
// 커서가 있는 줄 — 긴 쿼리에서 지금 어디를 고치고 있는지 잃지 않게 옅은 띠를 깐다
|
||||
var caretLine = LineIndexOf(text, source.CaretIndex);
|
||||
|
||||
var lineIndex = 0;
|
||||
var lineStart = 0;
|
||||
var height = ActualHeight;
|
||||
|
||||
while (lineStart <= text.Length)
|
||||
// 커서가 있는 줄 — 긴 쿼리에서 지금 어디를 고치고 있는지 잃지 않게 옅은 띠를 깐다.
|
||||
// 위치는 TextBox 에게 묻는다(아래 이유와 같다).
|
||||
if (source.SelectionLength == 0 && TryFindResource("B.Hover") is Brush band)
|
||||
{
|
||||
var newline = text.IndexOf('\n', lineStart);
|
||||
var lineEnd = newline < 0 ? text.Length : newline;
|
||||
var y = offsetY + lineIndex * lineHeight;
|
||||
|
||||
if (lineIndex == caretLine && y + lineHeight >= 0 && y <= height
|
||||
&& source.SelectionLength == 0
|
||||
&& TryFindResource("B.Hover") is Brush band)
|
||||
var caretRect = RectAt(source.CaretIndex);
|
||||
if (caretRect is { } cr && cr.Bottom >= 0 && cr.Top <= height)
|
||||
{
|
||||
dc.DrawRectangle(band, null, new Rect(0, y, Math.Max(0, ActualWidth), lineHeight));
|
||||
dc.DrawRectangle(band, null, new Rect(0, cr.Top, Math.Max(0, ActualWidth), cr.Height));
|
||||
}
|
||||
}
|
||||
|
||||
// 화면 밖 줄은 그리지 않는다 — 긴 쿼리에서 매 입력마다 전부 그리면 눈에 띄게 느려진다
|
||||
if (y > height)
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (y + lineHeight >= 0)
|
||||
// 조각이 줄을 걸치면 줄마다 나눠 그린다 — 한 번에 그리면 줄바꿈이 무시된다
|
||||
var pieceStart = token.Start;
|
||||
while (pieceStart < token.End)
|
||||
{
|
||||
DrawLine(dc, text, tokens, lineStart, lineEnd, typeface, dpi, offsetX, y);
|
||||
var newline = text.IndexOf('\n', pieceStart, token.End - pieceStart);
|
||||
var pieceEnd = newline < 0 ? token.End : newline;
|
||||
if (pieceEnd > pieceStart)
|
||||
{
|
||||
DrawPiece(dc, text, pieceStart, pieceEnd, token.Kind, typeface, dpi, height);
|
||||
}
|
||||
|
||||
if (newline < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
lineStart = newline + 1;
|
||||
lineIndex++;
|
||||
pieceStart = newline + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>주어진 위치가 몇 번째 줄인지(0부터)</summary>
|
||||
private static int LineIndexOf(string text, int index)
|
||||
{
|
||||
var line = 0;
|
||||
var upto = Math.Clamp(index, 0, text.Length);
|
||||
for (var i = 0; i < upto; i++)
|
||||
{
|
||||
if (text[i] == '\n')
|
||||
{
|
||||
line++;
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 한 줄을 조각별 색으로 그린다.
|
||||
/// 조각 경계마다 <see cref="FormattedText"/> 를 새로 만들되, x 위치는 <b>앞부분 전체를 다시 재서</b> 잡는다 —
|
||||
/// 폭을 누적하면 자간·힌팅 때문에 뒤로 갈수록 한두 픽셀씩 밀린다.
|
||||
/// 그 글자의 화면 위치 — <b>TextBox 에게 직접 묻는다.</b>
|
||||
///
|
||||
/// 예전에는 앞부분 글자 폭을 이쪽에서 재서 x 를 계산했는데, 한글이 들어오면 어긋났다.
|
||||
/// 편집기 글꼴(Consolas)에는 한글 글리프가 없어 폰트 대체가 일어나는데,
|
||||
/// TextBox 의 내부 조판과 이쪽 <see cref="FormattedText"/> 측정이 같은 글꼴·같은 셰이핑을 고르리라는
|
||||
/// 보장이 없다. 조금만 달라도 뒤로 갈수록 벌어져 색과 글자가 따로 논다.
|
||||
///
|
||||
/// 좌표를 물어보면 여백·스크롤·줄 높이·폰트 대체가 전부 TextBox 기준으로 이미 반영돼 온다.
|
||||
/// </summary>
|
||||
private void DrawLine(DrawingContext dc, string text, IReadOnlyList<SqlToken> tokens,
|
||||
int lineStart, int lineEnd, Typeface typeface, double dpi, double offsetX, double y)
|
||||
private Rect? RectAt(int index)
|
||||
{
|
||||
foreach (var token in tokens)
|
||||
if (source is null)
|
||||
{
|
||||
if (token.End <= lineStart || token.Start >= lineEnd)
|
||||
{
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
var start = Math.Max(token.Start, lineStart);
|
||||
var end = Math.Min(token.End, lineEnd);
|
||||
if (end <= start)
|
||||
var clamped = Math.Clamp(index, 0, source.Text.Length);
|
||||
var rect = source.GetRectFromCharacterIndex(clamped);
|
||||
if (rect.IsEmpty || double.IsInfinity(rect.Top) || double.IsNaN(rect.Top))
|
||||
{
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
var prefix = text[lineStart..start];
|
||||
var x = offsetX;
|
||||
if (prefix.Length > 0)
|
||||
/// <summary>
|
||||
/// 한 줄 안의 조각 하나를 그 자리에 그린다.
|
||||
///
|
||||
/// 시작 위치를 TextBox 에게 물으므로 조각마다 오차가 초기화된다 —
|
||||
/// 폭을 누적하던 예전 방식과 달리 뒤로 갈수록 밀리지 않는다.
|
||||
/// </summary>
|
||||
private void DrawPiece(DrawingContext dc, string text, int start, int end,
|
||||
SqlTokenKind kind, Typeface typeface, double dpi, double height)
|
||||
{
|
||||
var measured = new FormattedText(prefix, CultureInfo.InvariantCulture, FlowDirection.LeftToRight,
|
||||
typeface, source!.FontSize, Brushes.Black, dpi);
|
||||
x += measured.WidthIncludingTrailingWhitespace;
|
||||
if (RectAt(start) is not { } rect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 화면 밖은 그리지 않는다 — 긴 쿼리에서 매 입력마다 전부 그리면 눈에 띄게 느려진다
|
||||
if (rect.Bottom < 0 || rect.Top > height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var piece = new FormattedText(text[start..end], CultureInfo.InvariantCulture, FlowDirection.LeftToRight,
|
||||
typeface, source!.FontSize, BrushFor(token.Kind), dpi);
|
||||
dc.DrawText(piece, new Point(x, y));
|
||||
}
|
||||
var piece = new FormattedText(text[start..end], CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
|
||||
typeface, source!.FontSize, BrushFor(kind), dpi);
|
||||
dc.DrawText(piece, new Point(rect.X, rect.Y));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -701,6 +701,35 @@ public static class DbSmoke
|
||||
// ⑦ 바깥 클릭으로 닫히는 설정인지
|
||||
Check("바깥 클릭으로 닫히는 설정(StaysOpen=False)", !popup.StaysOpen);
|
||||
|
||||
// ⑧ 색칠 레이어와 실제 글자 위치가 맞는지 — 한글이 섞이면 폰트 대체 때문에 어긋났었다.
|
||||
// 레이어는 TextBox 에게 좌표를 물으므로, 같은 인덱스의 사각형이 곧 그리는 자리다.
|
||||
// 여기서는 '캐럿이 글자 끝에 있을 때 그 x 가 앞 글자들 폭의 합과 단조 증가하는가'를 본다.
|
||||
Type("SELECT ㄴㅁㅇㄹ FROM ㅎㄱ");
|
||||
var xs = new List<double>();
|
||||
for (var i = 0; i <= box.Text.Length; i++)
|
||||
{
|
||||
var r = box.GetRectFromCharacterIndex(i);
|
||||
xs.Add(r.IsEmpty ? double.NaN : r.X);
|
||||
}
|
||||
var monotonic = true;
|
||||
for (var i = 1; i < xs.Count; i++)
|
||||
{
|
||||
if (!double.IsNaN(xs[i]) && !double.IsNaN(xs[i - 1]) && xs[i] < xs[i - 1] - 0.01)
|
||||
{
|
||||
monotonic = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Check("한글 섞인 줄에서 글자 좌표가 뒤로 밀리지 않는다", monotonic,
|
||||
$"첫 x {xs[0]:F1} → 끝 x {xs[^1]:F1}");
|
||||
|
||||
// 마지막 글자의 오른쪽 끝이 캐럿 자리와 같아야 한다(레이어가 이 값으로 그린다)
|
||||
var lastCharRect = box.GetRectFromCharacterIndex(box.Text.Length - 1);
|
||||
var caretRect = box.GetRectFromCharacterIndex(box.Text.Length);
|
||||
Check("마지막 글자 다음 좌표가 캐럿 자리와 이어진다",
|
||||
!lastCharRect.IsEmpty && !caretRect.IsEmpty && caretRect.X >= lastCharRect.X,
|
||||
$"글자 {lastCharRect.X:F1} / 캐럿 {caretRect.X:F1}");
|
||||
|
||||
window.Close();
|
||||
DrainDispatcher();
|
||||
}
|
||||
|
||||
@@ -43,6 +43,20 @@ public static class ThemeManager
|
||||
/// <summary>라이트↔다크 토글</summary>
|
||||
public static void Toggle() => Apply(!IsLight);
|
||||
|
||||
/// <summary>
|
||||
/// 열려 있는 모든 창의 제목 표시줄을 지금 테마에 맞춘다.
|
||||
///
|
||||
/// 제목 표시줄은 창 안쪽 자원(B.*)이 아니라 OS 가 그리므로 사전을 바꿔도 따라오지 않는다.
|
||||
/// 테마를 바꾼 순간 이미 떠 있는 창들도 함께 맞춰 줘야 위쪽만 밝은 창이 남지 않는다.
|
||||
/// </summary>
|
||||
public static void ApplyChromeToOpenWindows()
|
||||
{
|
||||
foreach (Window window in Application.Current.Windows)
|
||||
{
|
||||
WindowChromeTheme.Apply(window, !IsLight);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>테마 적용 — App.Resources 병합 사전에서 토큰 dict 를 찾아 교체</summary>
|
||||
public static void Apply(bool light)
|
||||
{
|
||||
@@ -57,11 +71,13 @@ public static class ThemeManager
|
||||
{
|
||||
dictionaries[i] = new ResourceDictionary { Source = tokensUri };
|
||||
Save();
|
||||
ApplyChromeToOpenWindows();
|
||||
return;
|
||||
}
|
||||
}
|
||||
dictionaries.Add(new ResourceDictionary { Source = tokensUri });
|
||||
Save();
|
||||
ApplyChromeToOpenWindows();
|
||||
}
|
||||
|
||||
private static void Save()
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 창 제목 표시줄을 앱 테마에 맞춘다.
|
||||
///
|
||||
/// <b>왜 필요한가.</b> ThemedWindow 스타일은 창 <i>안쪽</i>만 칠한다. 제목 표시줄은 OS 가 그리며
|
||||
/// 기본값은 <b>Windows 의 테마</b>를 따른다 — 앱을 다크로 바꿔도 Windows 가 라이트면 흰 띠가 남아
|
||||
/// 창 위쪽만 밝게 뜬다. 반대 조합도 마찬가지다.
|
||||
///
|
||||
/// DWM 속성 하나(DWMWA_USE_IMMERSIVE_DARK_MODE)로 창별 지정이 가능하다.
|
||||
/// 커스텀 크롬을 직접 그리는 것보다 훨씬 적은 비용으로 같은 결과를 얻는다 —
|
||||
/// 최소화·최대화·닫기의 동작과 접근성이 OS 것 그대로 남는다.
|
||||
///
|
||||
/// Windows 10 1809 이전에는 이 속성이 없다. 실패해도 그냥 무시한다(예전 모습으로 남을 뿐이다).
|
||||
/// </summary>
|
||||
public static class WindowChromeTheme
|
||||
{
|
||||
#region Member Fields
|
||||
// Windows 10 20H1(빌드 18985) 이상. 그 이전 빌드는 19 를 쓰던 시기가 있어 둘 다 시도한다.
|
||||
private const int UseImmersiveDarkMode = 20;
|
||||
private const int UseImmersiveDarkModeBefore20H1 = 19;
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
[DllImport("dwmapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
|
||||
|
||||
/// <summary>
|
||||
/// 이 창의 제목 표시줄을 어둡게/밝게 — 창 핸들이 생긴 뒤에 불러야 한다.
|
||||
/// </summary>
|
||||
public static void Apply(Window window, bool dark)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handle = new WindowInteropHelper(window).Handle;
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var value = dark ? 1 : 0;
|
||||
if (DwmSetWindowAttribute(handle, UseImmersiveDarkMode, ref value, sizeof(int)) != 0)
|
||||
{
|
||||
DwmSetWindowAttribute(handle, UseImmersiveDarkModeBefore20H1, ref value, sizeof(int));
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
// dwmapi 가 없는 환경 — 제목 표시줄만 OS 기본으로 남는다
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -65,13 +65,19 @@
|
||||
<!-- ── 머리말 ── -->
|
||||
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,12">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!-- 목업의 앞 배지 — 무슨 창인지 한눈에 -->
|
||||
<Border Width="34" Height="34" CornerRadius="9" Margin="0,0,10,0"
|
||||
Background="{DynamicResource B.AccentFill}">
|
||||
<TextBlock Text="🗄" FontSize="16" Foreground="{DynamicResource B.OnAccent}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="쿼리 편집" FontSize="17" FontWeight="Bold"
|
||||
Foreground="{DynamicResource B.Ink}" VerticalAlignment="Center"/>
|
||||
<Border x:Name="TargetChip" CornerRadius="6" Padding="8,2" Margin="10,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource B.Input}" BorderBrush="{DynamicResource B.InputBorder}"
|
||||
BorderThickness="1">
|
||||
<TextBlock x:Name="TargetChipText" FontSize="11.5" FontFamily="Consolas, D2Coding, 굴림체"
|
||||
<TextBlock x:Name="TargetChipText" FontSize="11.5" FontFamily="D2Coding, 굴림체, Consolas"
|
||||
Foreground="{DynamicResource B.AccentText}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
@@ -172,7 +178,7 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" Background="{DynamicResource B.Surface}" Padding="8,6">
|
||||
<TextBlock x:Name="LineNumbers" FontFamily="Consolas, D2Coding, 굴림체" FontSize="13"
|
||||
<TextBlock x:Name="LineNumbers" FontFamily="D2Coding, 굴림체, Consolas" FontSize="13"
|
||||
Foreground="{DynamicResource B.Muted}" TextAlignment="Right" MinWidth="24"/>
|
||||
</Border>
|
||||
<Grid Grid.Column="1">
|
||||
@@ -192,7 +198,7 @@
|
||||
<TextBlock DockPanel.Dock="Right" Text="{Binding KindLabel}" FontSize="10"
|
||||
Foreground="{DynamicResource B.Muted}" Margin="10,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Text}" FontFamily="Consolas, D2Coding, 굴림체"
|
||||
<TextBlock Text="{Binding Text}" FontFamily="D2Coding, 굴림체, Consolas"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
@@ -201,7 +207,7 @@
|
||||
</Border>
|
||||
</Popup>
|
||||
<TextBox x:Name="SqlBox"
|
||||
FontFamily="Consolas, D2Coding, 굴림체" FontSize="13"
|
||||
FontFamily="D2Coding, 굴림체, Consolas" FontSize="13"
|
||||
AcceptsReturn="True" AcceptsTab="False"
|
||||
TextWrapping="NoWrap" Padding="8,6"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
@@ -226,10 +232,20 @@
|
||||
<TextBlock DockPanel.Dock="Top" Text="치환 변수" Style="{StaticResource CardTitle}"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
<TextBox x:Name="VariableSearch" DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8,4"
|
||||
<!-- 검색: 돋보기를 겹쳐 둔다 — 빈 상자만 있으면 무엇을 하는 칸인지 안 읽힌다 -->
|
||||
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
|
||||
<TextBox x:Name="VariableSearch" Padding="28,4,8,4"
|
||||
VerticalContentAlignment="Center" TextChanged="OnVariableSearchChanged"
|
||||
AutomationProperties.Name="치환 변수 검색"
|
||||
ToolTip="변수명·설명·초성으로 찾습니다 (예: ㅊㅌㅂㅎ → 차트번호)"/>
|
||||
<TextBlock Text="🔍" FontSize="11" Margin="9,0,0,0" IsHitTestVisible="False"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource B.Muted}"/>
|
||||
<TextBlock x:Name="SearchPlaceholder" Text="변수명 또는 설명 검색" FontSize="11.5"
|
||||
Margin="30,0,0,0" IsHitTestVisible="False"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource B.Muted}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 갈래 칩 -->
|
||||
<ItemsControl x:Name="BucketChips" DockPanel.Dock="Top" Margin="0,0,0,8">
|
||||
@@ -253,8 +269,13 @@
|
||||
<!-- 안내 -->
|
||||
<Border DockPanel.Dock="Bottom" Margin="0,8,0,0" Padding="10,7" CornerRadius="8"
|
||||
Background="{DynamicResource B.Input}">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Left" Text="ⓘ" FontSize="11" Margin="0,0,7,0"
|
||||
VerticalAlignment="Top"
|
||||
Foreground="{DynamicResource B.AccentText}"/>
|
||||
<TextBlock x:Name="VariableHint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource B.Muted}"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<ListBox x:Name="VariableList" FontSize="11.5" MouseDoubleClick="OnInsertVariable"
|
||||
@@ -277,7 +298,7 @@
|
||||
ToolTip="커서 위치에 삽입"/>
|
||||
<TextBlock DockPanel.Dock="Right" Text="{Binding ShortForm}"
|
||||
Foreground="{DynamicResource B.Muted}" FontSize="10.5"
|
||||
FontFamily="Consolas, D2Coding, 굴림체"
|
||||
FontFamily="D2Coding, 굴림체, Consolas"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||
<TextBlock Text="{Binding Description}" FontFamily="Segoe UI, 맑은 고딕" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||
|
||||
@@ -792,7 +792,10 @@ public partial class QueryEditorWindow : Window
|
||||
|
||||
#region Methods - Variables
|
||||
private void OnVariableSearchChanged(object sender, TextChangedEventArgs e)
|
||||
=> ApplyVariableFilter(VariableSearch.Text);
|
||||
{
|
||||
SearchPlaceholder.Visibility = VariableSearch.Text.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ApplyVariableFilter(VariableSearch.Text);
|
||||
}
|
||||
|
||||
/// <summary>카탈로그 + 이 서식의 컨트롤 — 컨트롤은 문서마다 달라 여기서 합친다</summary>
|
||||
private IReadOnlyList<QueryVariable> AllVariables()
|
||||
@@ -808,9 +811,16 @@ public partial class QueryEditorWindow : Window
|
||||
{
|
||||
new { Name = "전체", Count = all.Count, IsActive = activeBucket.Length == 0 },
|
||||
};
|
||||
foreach (var group in all.GroupBy(v => v.Bucket).OrderByDescending(g => g.Count()))
|
||||
// 순서는 개수가 아니라 고정이다 — 컨트롤 수는 서식마다 달라서, 개수순으로 두면
|
||||
// 서식을 바꿀 때마다 칩이 자리를 바꿔 손이 기억하지 못한다.
|
||||
var order = new[] { "환자", "서식", "작업자", "컨트롤" };
|
||||
var byBucket = all.GroupBy(v => v.Bucket).ToDictionary(g => g.Key, g => g.Count());
|
||||
foreach (var name in order)
|
||||
{
|
||||
chips.Add(new { Name = group.Key, Count = group.Count(), IsActive = activeBucket == group.Key });
|
||||
if (byBucket.TryGetValue(name, out var count) && count > 0)
|
||||
{
|
||||
chips.Add(new { Name = name, Count = count, IsActive = activeBucket == name });
|
||||
}
|
||||
}
|
||||
BucketChips.ItemsSource = chips;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user