미리보기를 눌러 볼 수 있게 하고, Ctrl+드래그를 복사로 바꾼다
사용자 지적 두 건. 둘 다 라디오 배타 그룹이 걸린 문제였다.
## 미리보기 클릭이 안 먹었다
레거시 미리보기(TK_PREVIEW)는 정적 그림이 아니라 런타임 모드라 눌러 보며 확인한다.
우리 미리보기는 그림이었다 — 그래서 <b>라디오가 배타로 묶였는지 확인할 방법이 없었다</b>.
패널로 묶었다고 믿고 저장했다가 임상 화면에서 두 개가 동시에 켜지는 것이
이 서식에서 가장 흔한 사고인데, 저장 전에 그걸 볼 수단이 없었던 것이다.
PreviewCheckState — 컨트롤 Id → 체크 상태 사전. 문서는 건드리지 않는다.
확인하려고 누른 것이 서식 수정이 되면 미리보기를 열었다 닫은 것만으로
저장할 내용이 생긴다. 렌더 시점(WithTagValue)에만 덮고, 창을 닫으면 사라진다.
배타는 직계 부모 안에서만. 레거시 RadioButton 은 WinForms 를 그대로 상속해
GroupName 이 없고(RadioButton.vb:7-8) 배타 판정을 부모 컨테이너에 맡긴다.
운영 실측도 라디오 15,184개 중 15,026개(98.96%)가 패널 직계 자식 — 패널 1개 = 문항 1개다.
- PreventEditing=True 는 안 눌린다(RadioButton.vb:271-279 — 값 대입만 되고 클릭은 무시).
Locked 는 편집 보호일 뿐이라 눌린다.
- 라디오는 다시 눌러도 안 꺼진다. 끄는 것은 형제를 누르는 것뿐이다.
- 손으로 누른 값이 태그 값을 이긴다 — 그 칸을 직접 눌러 보는 중이므로 사용자의 손이 마지막이다.
- 환자를 바꾸거나 서식을 갈아타면 지운다. 안 지우면 손으로 누른 값이
새 환자의 실제 값을 가려, 확인하려고 만든 창이 확인을 막는다.
- 이 창에서 인쇄하면 화면과 같은 상태로 나간다(같은 checks 를 Print 까지 넘긴다).
히트테스트는 WPF 가 아니라 모델 트리로 한다. 종이 비주얼은 템플릿이 만든 그림이라
컨트롤 하나가 여러 요소로 쪼개져 있고 일부는 히트테스트에서 빠져 있다.
캔버스의 HitTestControl 과 두 군데만 다르다 — Locked 를 보지 않고,
그려지지 않은 것(숨김·데이터소스·인쇄 제외)을 뺀다.
## Ctrl+드래그가 복사가 아니었다
예전에는 '선택에 더하고 이동'이었다(Ctrl 이 선택 추가 수정자였으니까).
그래서 같은 컨트롤을 여러 개 놓는 방법이 복사·붙여넣기뿐이었는데,
붙여넣기는 <b>항상 최상위 +12,+12</b> 에 떨어진다.
패널 안 라디오를 그렇게 복사하면 사본이 패널 밖으로 나가 배타 그룹이 깨진다 —
눈으로는 안 보이고 임상 화면에서만 드러나는 고장이다.
DuplicateSelectionInPlace — 제자리 복제. 부모를 유지하므로 사본이 같은 문항 안에 남는다.
드래그 문턱을 넘은 지점에서 복제한다(누름 시점에 하면 Ctrl+클릭만으로도 사본이 생긴다).
복제가 스냅샷을 이미 찍었으므로 이어지는 이동은 추가로 찍지 않는다 —
안 그러면 "복사 후 이동"이 Undo 두 번이 되고, 한 번만 누른 사용자는
원본 자리에 사본이 겹쳐 남은 상태를 본다.
Ctrl 은 이 제스처에서 더하기가 아니다. 합집합으로 두면 아까 선택해 둔 컨트롤까지
함께 복제돼 하나 집어 끌었는데 화면 다른 곳에 사본이 여럿 생긴다. Shift 는 그대로 더하기다.
선택 단위가 패널이므로 자식을 그냥 집으면 <b>문항이 통째로</b> 복사되고,
더블클릭으로 들어간 뒤 집으면 <b>그 선택지만</b> 복사돼 같은 패널에 남는다.
둘 다 필요한 동작이라 둘 다 시험한다.
## 판정
- 단위 시험 368 (PreviewCheckState 10건 추가)
- edit-smoke 320건 전건 통과 (Ctrl+드래그 복사 9건, 미리보기 클릭 5건 추가)
· 20-5f 의 옛 단언 2건은 의미가 바뀌어 교체했다("합집합으로 함께 움직인다" → "복사된다")
· 20-5c(빈 선택 Ctrl+드래그)가 남기는 사본을 되돌린다 — 안 하면 뒤따르는 개수 검사가 흔들린다
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
- --db-patient "김" 39건 · --dialog-shots · --cleartype · --db-filecfg 전건 통과
시험을 처음 쓸 때 자식을 Children[0]/[1] 로 집었는데 자식 VM 은 z 순서로 온다.
이미 켜진 라디오를 누르는 시험이 되어 "문서가 바뀌었다"는 거짓 실패가 났다 — Id 로 집는다.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d965b3f217
commit
8cc2d68ac1
@@ -0,0 +1,77 @@
|
||||
using SheetMe.Core.Models;
|
||||
|
||||
namespace SheetMe.Core.Catalog;
|
||||
|
||||
/// <summary>
|
||||
/// 미리보기에서 눌러 본 체크·라디오 상태 — <b>문서를 건드리지 않는다</b>.
|
||||
///
|
||||
/// <b>왜 별도 상태인가.</b> 레거시 미리보기(TK_PREVIEW)는 정적 그림이 아니라 런타임 모드라
|
||||
/// 실제로 눌러 보며 확인한다. 우리 미리보기도 눌러 볼 수 있어야 하지만, 그 클릭이 디자인을
|
||||
/// 바꾸면 "확인하려고 눌렀는데 서식이 수정됨" 이 된다. 그래서 컨트롤 Id → 상태 사전으로만 들고
|
||||
/// 렌더 시점에 덮어쓴다. 미리보기를 닫으면 사라진다.
|
||||
///
|
||||
/// <b>배타 규칙은 직계 부모다.</b> 레거시 RadioButton 은 WinForms 를 그대로 상속해
|
||||
/// <c>GroupName</c> 이 없고(RadioButton.vb:7-8) 배타 판정을 부모 컨테이너에 맡긴다.
|
||||
/// 운영 실측도 라디오 15,184개 중 15,026개(98.96%)가 패널 직계 자식이다 —
|
||||
/// 즉 <b>패널 하나가 문항 하나</b>다. 여기서도 형제는 직계 부모의 자식만 본다.
|
||||
/// </summary>
|
||||
public sealed class PreviewCheckState
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly Dictionary<string, bool> overrides = new(StringComparer.Ordinal);
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>눌러 본 것이 하나라도 있는가 — 렌더가 덮어쓸 필요가 있는지 판단용</summary>
|
||||
public bool HasAny => overrides.Count > 0;
|
||||
|
||||
/// <summary>덮어쓸 상태 — 눌러 본 적 없으면 null</summary>
|
||||
public IReadOnlyDictionary<string, bool> Overrides => overrides;
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>이 컨트롤이 지금 체크된 상태인가 — 눌러 본 값이 없으면 서식의 기본값</summary>
|
||||
public bool IsChecked(ControlElement element)
|
||||
=> overrides.TryGetValue(element.Id, out var state)
|
||||
? state
|
||||
: string.Equals(element.Props.GetText("Checked"), "True", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>미리보기에서 누를 수 있는 타입인가</summary>
|
||||
public static bool IsClickable(ControlElement element)
|
||||
=> element.Type is "CheckBox" or "RadioButton"
|
||||
// 레거시는 읽기전용(PreventEditing)이면 OnClick 자체를 무시한다
|
||||
// (RadioButton.vb:271-279) — 값 대입은 되지만 클릭은 안 먹는다.
|
||||
&& !string.Equals(element.Props.GetText("PreventEditing"), "True", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 눌렀다 — 체크박스는 반전, 라디오는 자기를 켜고 <b>직계 형제 라디오를 끈다</b>.
|
||||
/// </summary>
|
||||
/// <param name="target">누른 컨트롤</param>
|
||||
/// <param name="siblings">그 컨트롤의 <b>직계 부모</b>가 담은 컨트롤 전부(자기 포함)</param>
|
||||
public void Click(ControlElement target, IReadOnlyList<ControlElement> siblings)
|
||||
{
|
||||
if (!IsClickable(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (target.Type == "CheckBox")
|
||||
{
|
||||
overrides[target.Id] = !IsChecked(target);
|
||||
return;
|
||||
}
|
||||
// 라디오 — 이미 켜져 있어도 다시 켠 상태로 둔다(레거시도 체크를 풀지 않는다).
|
||||
// 끄는 것은 같은 부모의 다른 라디오를 누르는 것뿐이다.
|
||||
foreach (var sibling in siblings)
|
||||
{
|
||||
if (sibling.Type == "RadioButton" && !ReferenceEquals(sibling, target))
|
||||
{
|
||||
overrides[sibling.Id] = false;
|
||||
}
|
||||
}
|
||||
overrides[target.Id] = true;
|
||||
}
|
||||
|
||||
/// <summary>눌러 본 것을 전부 잊는다 — 환자를 바꾸거나 서식을 다시 열 때</summary>
|
||||
public void Clear() => overrides.Clear();
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user