태그 행에서 모달 없이 태그를 고른다 — 인라인 완성
4단계 계속. 주 경로에서 1080x700 모달이 사라진다.
칩 상위 8종만으로 운영 사용의 41.7%, 상위 16종으로 61.5% 를 덮는다.
나머지는 이름을 정확히 16자 외워 치거나 모달을 열어야 했고, 자유 입력은 열려 있는데
제안이 없어서 <b>오타가 검증 없이 저장되고 EMR 에서 조용히 빈 칸이 됐다</b>.
같은 인스펙터의 '속성 추가' 행에는 이미 부분일치 제안이 있었다 —
패턴은 있는데 운영 8,032건이 걸린 칸에만 없었다.
<b>새 패턴을 발명하지 않았다.</b> QueryEditorWindow 의 CompletionPopup 에서 배운 것을 그대로 쓴다:
StaysOpen=False 로 두면 팝업이 마우스를 캡처해 항목 클릭을 '바깥 클릭'으로 먹어 버린다
(눌러도 입력이 안 되고 사라진다 — 그 함정을 이미 한 번 겪었다).
AllowsTransparency 도 켜지 않는다 — 레이어드 창이 되면 ClearType 이 꺼져 13px 한글이 뭉개진다
(진단 cleartype 의 D 표본). 대신 안쪽에 ClearTypeHint 를 건다.
<b>칩이 사라지는 기준을 바꿨다.</b> 전에는 ValueText.Length == 0 이라 값이 정해지면 칩이 사라졌다.
그래서 잘못 고른 태그를 바꿀 때 도움이 끊겼다 — 태그를 고치는 일은 처음 고르는 일만큼 잦은데
고치는 쪽만 맨손이었다. 이제 기준은 '값이 있는지'가 아니라 <b>'타이핑 중인지'</b>다.
타이핑을 시작하면 아래 완성 목록이 같은 일을 더 정확하게 하므로 칩을 접는다.
그 옛 규칙을 고정하던 검사 하나를 새 규칙으로 고쳤다 — 의도한 변경이다.
완성 목록은 입력 중인 글자를 <b>인자로</b> 받는다. 태그 행의 Text 바인딩이
UpdateSourceTrigger=LostFocus 라 타이핑 중에는 ValueText 가 아직 옛 값이고,
그것으로 목록을 만들면 한 글자 뒤처진다.
순위는 이 타입에서 자주 쓰이는 순서 → 운영 사용 건수 → 이름이다.
알파벳 순으로 두면 상위 10종(전체 사용의 47.5%)이 목록 아래로 흩어진다.
검색은 TagSearch 를 그대로 쓴다 — 별칭·초성이 여기서도 듣는다('환자명'으로도 뜬다).
<b>Popup.IsOpen 은 기본이 TwoWay 다.</b> 읽기 전용 속성에 바인딩하면 XamlParseException 이고,
그 실패가 --dialog-shots 를 통째로 죽였다(종료코드 3). Mode=OneWay 를 명시한다.
게이트가 아니면 인스펙터를 띄우는 화면에서만 터졌을 것이다.
실측 14건: 값이 있어도 칩 유지 · 타이핑 전 미표시 · 타이핑 시 칩 접힘 · 별칭 적용 ·
자주 쓰는 것이 위 · 10줄 상한 · ↑↓ 순환 · Enter 확정과 커밋 · 확정 후 닫힘/칩 복귀 ·
정확히 하나면 안 가림 · Esc 가 값을 보존.
게이트: 테스트 291/291, --edit-smoke 0실패(신규 14), --dialog-shots 글자 2,465개 넘침 0 대조군 4/4,
--modal-check 0실패, --cleartype 11/11, --maxrect 0실패, --scale-budget 5/5, 빌드 경고 0,
--db-smoke 1271건 diff 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
a869e5a5bc
commit
9b2ae0ea67
@@ -433,11 +433,134 @@ public sealed class TagPickerRowViewModel : PropertyRowViewModel
|
||||
public IReadOnlyList<string> Suggestions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 제안을 보여 줄지 — <b>값이 비어 있을 때만</b>.
|
||||
/// 이미 정해진 행 아래에도 칩을 깔면 좁은 인스펙터가 금방 길어진다.
|
||||
/// 제안이 필요한 순간은 '아직 안 골랐을 때'다.
|
||||
/// 제안을 보여 줄지.
|
||||
///
|
||||
/// 전에는 <c>ValueText.Length == 0</c> 이었다 — 값이 정해지면 칩이 사라졌다.
|
||||
/// 그래서 <b>잘못 고른 태그를 바꿀 때 도움이 끊겼다</b>. 실사용에서 태그를 고치는 일은
|
||||
/// 처음 고르는 일만큼 잦은데(운영 태그 사용 8,032건), 고치는 쪽만 맨손이었다.
|
||||
/// 이제 값이 있어도 남긴다. 대신 <b>타이핑을 시작하면 접는다</b> —
|
||||
/// 그때부터는 아래 완성 목록이 같은 일을 더 정확하게 한다.
|
||||
/// </summary>
|
||||
public bool ShowSuggestions => ValueText.Length == 0 && Suggestions.Count > 0;
|
||||
public bool ShowSuggestions => !IsTyping && Suggestions.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// 인라인 완성 목록 — 이 행에서 모달 없이 태그를 고르는 주 경로.
|
||||
///
|
||||
/// <b>왜 여기가 주 경로인가.</b> 칩 상위 8종만으로 운영 사용의 41.7%, 상위 16종으로 61.5% 를 덮는다.
|
||||
/// 나머지는 이름을 정확히 16자 외워 치거나 1080×700 모달을 열어야 했고,
|
||||
/// 자유 입력은 열려 있는데 제안이 없어서 <b>오타가 검증 없이 저장되고
|
||||
/// EMR 에서 조용히 빈 칸이 됐다</b>.
|
||||
/// 같은 인스펙터의 '속성 추가' 행에는 이미 부분일치 제안이 있다 — 패턴은 있는데
|
||||
/// 운영 8,032건이 걸린 칸에만 없었다.
|
||||
/// </summary>
|
||||
public System.Collections.ObjectModel.ObservableCollection<string> Completions { get; } = new();
|
||||
|
||||
/// <summary>완성 목록에서 지금 짚고 있는 줄 — ↑↓ 로 움직이고 Enter 로 확정한다</summary>
|
||||
public int CompletionIndex
|
||||
{
|
||||
get => completionIndex;
|
||||
set => SetProperty(ref completionIndex, value);
|
||||
}
|
||||
|
||||
public bool ShowCompletions
|
||||
{
|
||||
get => showCompletions;
|
||||
private set => SetProperty(ref showCompletions, value);
|
||||
}
|
||||
|
||||
/// <summary>타이핑 중인가 — 칩을 접고 완성을 여는 기준</summary>
|
||||
private bool IsTyping { get; set; }
|
||||
|
||||
private int completionIndex = -1;
|
||||
private bool showCompletions;
|
||||
|
||||
/// <summary>
|
||||
/// 지금 입력된 글자로 완성 목록을 다시 만든다.
|
||||
///
|
||||
/// 순위는 <b>이 타입에서 자주 쓰이는 순서 → 운영 사용 건수 → 이름</b>이다.
|
||||
/// 알파벳 순으로 두면 상위 10종(전체 사용의 47.5%)이 목록 아래로 흩어진다.
|
||||
/// 검색은 <see cref="Core.Catalog.TagSearch"/> 를 그대로 쓴다 — 별칭·초성이 여기서도 듣는다.
|
||||
/// </summary>
|
||||
public void RefreshCompletions(string typed)
|
||||
{
|
||||
// 인자로 받는다 — 태그 행의 Text 바인딩은 UpdateSourceTrigger=LostFocus 라
|
||||
// 타이핑 중에는 ValueText 가 아직 옛 값이다. 그것으로 목록을 만들면 한 글자 뒤처진다.
|
||||
var query = typed.Trim();
|
||||
Completions.Clear();
|
||||
if (!IsTyping || query.Length == 0)
|
||||
{
|
||||
ShowCompletions = false;
|
||||
CompletionIndex = -1;
|
||||
return;
|
||||
}
|
||||
var tokens = query.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var ranked = Choices
|
||||
.Where(tag => Core.Catalog.TagSearch.MatchesAll(tag, tokens))
|
||||
.OrderBy(tag =>
|
||||
{
|
||||
var rank = preferredAll.ToList().IndexOf(tag);
|
||||
return rank < 0 ? int.MaxValue : rank;
|
||||
})
|
||||
.ThenByDescending(Core.Catalog.LegacyTagUsageCounts.Of)
|
||||
.ThenBy(tag => tag, StringComparer.Ordinal)
|
||||
.Take(CompletionLimit)
|
||||
.ToList();
|
||||
foreach (var tag in ranked)
|
||||
{
|
||||
Completions.Add(tag);
|
||||
}
|
||||
// 이미 정확히 그 태그만 남았으면 목록을 띄우지 않는다 — 다 골랐는데 가리는 셈이다
|
||||
ShowCompletions = ranked.Count > 0
|
||||
&& !(ranked.Count == 1 && string.Equals(ranked[0], query, StringComparison.Ordinal));
|
||||
CompletionIndex = ShowCompletions ? 0 : -1;
|
||||
}
|
||||
|
||||
/// <summary>사용자가 글자를 넣기 시작했다 — 칩을 접고 완성을 연다</summary>
|
||||
public void BeginTyping()
|
||||
{
|
||||
if (IsTyping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
IsTyping = true;
|
||||
OnPropertyChanged(nameof(ShowSuggestions));
|
||||
}
|
||||
|
||||
/// <summary>편집이 끝났다(확정·취소·포커스 이동) — 칩을 되돌리고 완성을 닫는다</summary>
|
||||
public void EndTyping()
|
||||
{
|
||||
IsTyping = false;
|
||||
ShowCompletions = false;
|
||||
CompletionIndex = -1;
|
||||
Completions.Clear();
|
||||
OnPropertyChanged(nameof(ShowSuggestions));
|
||||
}
|
||||
|
||||
/// <summary>↑↓ — 목록을 벗어나지 않고 순환한다</summary>
|
||||
public void MoveCompletion(int delta)
|
||||
{
|
||||
if (Completions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CompletionIndex = ((CompletionIndex + delta) % Completions.Count + Completions.Count) % Completions.Count;
|
||||
}
|
||||
|
||||
/// <summary>짚고 있는 줄을 값으로 확정한다 — 확정했으면 true</summary>
|
||||
public bool CommitCompletion()
|
||||
{
|
||||
if (!ShowCompletions || CompletionIndex < 0 || CompletionIndex >= Completions.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var picked = Completions[CompletionIndex];
|
||||
EndTyping();
|
||||
ValueText = picked;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>완성 목록에 깔 줄 수 — 10행×24px 이면 인스펙터를 덮지 않는다</summary>
|
||||
private const int CompletionLimit = 10;
|
||||
|
||||
/// <summary>찾아보기 — 검색 대화상자 열기</summary>
|
||||
public M.Framework.WPF.ICustomCommand? BrowseCommand { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user