인스펙터가 손을 놓지 않는다, 그리고 저장 확인이 사람 말을 한다
3단계 계속.
<b>① 선택이 바뀌면 편집하던 칸과 스크롤이 사라졌다.</b>
선택 집합이 바뀔 때마다 Rows.Clear() 로 행을 통째로 다시 만들기 때문이다.
하루 수백 건을 다루는 사람에게 그건 "다음 컨트롤로 넘어가면 손이 처음으로 돌아간다"는 뜻이다 —
라벨 20개의 글꼴을 차례로 고치는 일이 20번의 스크롤·클릭이 된다.
행을 키 기준 갱신으로 바꾸는 쪽이 더 근본적이지만 <b>이번에는 하지 않는다</b>.
이 패널은 회귀 기록이 가장 많은 곳이고(MainView.xaml:321-322, LayerPanelView.xaml:127-129),
실제 아픔은 재구성 비용이 아니라 포커스·스크롤 소실이다
(--scale-budget 이 뷰모델 작업은 마이크로초 단위임을 보여 준다).
그래서 재구성은 그대로 두고 포커스와 스크롤만 되돌린다. 행은 라벨로 찾는다 —
타입이 달라도 같은 이름의 행(글꼴·정렬·표시)은 같은 일을 한다.
Rebuild 를 감쌌다. 본문에 이른 return 이 여러 갈래 있어서 끝에 이벤트를 두면
도는 경로와 안 도는 경로가 갈리고, 그러면 <b>어떤 선택 변경에서만</b> 포커스가 안 돌아온다 —
그런 결함은 재현 조건을 찾기 전까지 "가끔 그런다"로만 보인다.
복원은 DispatcherPriority.Loaded 로 미룬다. 즉시 부르면 컨테이너가 아직 없어 빈 트리를 뒤진다
(이 세션에서 같은 함정을 두 번 밟았다).
사용자가 그 사이 다른 칸을 눌렀으면 포커스를 빼앗지 않는다.
<b>② F4 로 인스펙터에 들어간다.</b> 인스펙터로 포커스를 보내는 키가 저장소에 하나도 없었다.
캔버스에서 Tab 을 누르면 좌측 패널·문서 탭·플로팅 바를 다 지나야 인스펙터에 닿았다.
커맨드로 만들지 않는다 — 그러면 뷰모델이 뷰의 포커스를 알아야 한다.
<b>③ 저장 확인 문면을 두 층으로 갈랐다.</b>
이 대화상자는 세 부류가 다 보는 유일한 위험 지점인데(사내 인력·병원 전산팀·의료진),
본문에 E_SdgMst/E_SctMst, SdgDelYon='Y', ShtCneYon='Y' 가 그대로 노출되어 있었다.
사내 인력에게는 정확한 정보이고, 전산팀에게는 읽을 수 없는 문장이고, 간호부에게는 공포다.
위층은 사람 말("이 서식의 새 버전을 만듭니다. 지금 쓰이는 버전은 이력으로 남습니다"),
테이블·컬럼은 접힌 상세로 내렸다.
게이트: 테스트 285/285, --edit-smoke 0실패, --dialog-shots 글자 2,465개 검사 넘침 0,
--modal-check 0실패, --scale-budget 5/5, --maxrect 0실패, --cleartype 11/11, 빌드 경고 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
7e38ba654a
commit
d72ab3ce1e
@@ -1,12 +1,156 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using SheetMe.Designer.ViewModels.Inspector;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>속성 인스펙터 뷰 — 행 VM 암시적 템플릿 렌더.</summary>
|
||||
/// <summary>
|
||||
/// 속성 인스펙터 뷰 — 행 VM 암시적 템플릿 렌더.
|
||||
///
|
||||
/// <b>선택이 바뀌어도 손이 이어지게 한다.</b> 선택 집합이 바뀌면 뷰모델이 행을 통째로 다시 만들어서,
|
||||
/// 그대로 두면 편집하던 칸과 스크롤 위치가 매번 사라진다. 하루 수백 건을 다루는 사람에게 그건
|
||||
/// "다음 컨트롤로 넘어가면 손이 처음으로 돌아간다"는 뜻이다 —
|
||||
/// 라벨 20개의 글꼴을 차례로 고치는 일이 20번의 스크롤·클릭이 된다.
|
||||
///
|
||||
/// 행을 키 기준으로 갱신하는 쪽이 더 근본적이지만 이 패널은 회귀 기록이 가장 많은 곳이라
|
||||
/// 이번에는 재구성을 그대로 두고 <b>포커스와 스크롤만 되돌린다</b>.
|
||||
/// 행을 라벨로 찾는다 — 타입이 달라도 같은 이름의 행(글꼴·정렬·표시)은 같은 일을 한다.
|
||||
/// </summary>
|
||||
public partial class InspectorView : UserControl
|
||||
{
|
||||
#region Member Fields
|
||||
/// <summary>재구성 직전에 포커스가 있던 행의 라벨</summary>
|
||||
private string? focusedLabel;
|
||||
|
||||
private double savedOffset;
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public InspectorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.OldValue is InspectorViewModel old)
|
||||
{
|
||||
old.Rebuilding -= OnRebuilding;
|
||||
old.Rebuilt -= OnRebuilt;
|
||||
}
|
||||
if (e.NewValue is InspectorViewModel fresh)
|
||||
{
|
||||
fresh.Rebuilding += OnRebuilding;
|
||||
fresh.Rebuilt += OnRebuilt;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRebuilding()
|
||||
{
|
||||
savedOffset = RowScroll.VerticalOffset;
|
||||
focusedLabel = null;
|
||||
if (Keyboard.FocusedElement is DependencyObject focused && IsDescendantOfRows(focused))
|
||||
{
|
||||
focusedLabel = RowLabelOf(focused);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRebuilt()
|
||||
{
|
||||
// 컨테이너가 아직 없다 — 레이아웃이 한 번 돌아야 행이 생긴다.
|
||||
// Loaded 나 즉시 호출로는 늘 빈 트리를 뒤지게 된다(이 저장소에서 같은 함정을 두 번 밟았다).
|
||||
Dispatcher.BeginInvoke(new Action(Restore), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void Restore()
|
||||
{
|
||||
RowScroll.ScrollToVerticalOffset(savedOffset);
|
||||
if (focusedLabel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var target = FindRowByLabel(focusedLabel);
|
||||
focusedLabel = null;
|
||||
// 포커스를 강제로 옮기지 않는다 — 사용자가 그 사이에 다른 곳을 눌렀으면 그쪽이 옳다
|
||||
if (target is not null && Keyboard.FocusedElement is not TextBoxBase)
|
||||
{
|
||||
target.MoveFocus(new TraversalRequest(FocusNavigationDirection.First));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ctrl+1/2/3 이나 F4 로 인스펙터에 들어올 때 첫 편집 칸을 잡아 준다</summary>
|
||||
public void FocusFirstRow()
|
||||
{
|
||||
if (RowHost.Items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
RowHost.MoveFocus(new TraversalRequest(FocusNavigationDirection.First));
|
||||
}
|
||||
|
||||
private bool IsDescendantOfRows(DependencyObject node)
|
||||
{
|
||||
for (var i = 0; i < 40; i++)
|
||||
{
|
||||
if (ReferenceEquals(node, RowHost))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var parent = VisualParentOf(node);
|
||||
if (parent is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
node = parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>이 요소가 속한 행의 라벨 — 행 VM 을 DataContext 로 갖는 조상을 찾는다</summary>
|
||||
private static string? RowLabelOf(DependencyObject node)
|
||||
{
|
||||
for (var i = 0; i < 40; i++)
|
||||
{
|
||||
if (node is FrameworkElement { DataContext: PropertyRowViewModel row })
|
||||
{
|
||||
return row.Label;
|
||||
}
|
||||
var parent = VisualParentOf(node);
|
||||
if (parent is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
node = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FrameworkElement? FindRowByLabel(string label)
|
||||
{
|
||||
foreach (var item in RowHost.Items)
|
||||
{
|
||||
if (item is not PropertyRowViewModel row || !string.Equals(row.Label, label, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (RowHost.ItemContainerGenerator.ContainerFromItem(row) is FrameworkElement container)
|
||||
{
|
||||
return container;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>ContentPresenter 는 시각 부모만 갖는 경우가 있어 논리 부모로도 올라간다</summary>
|
||||
private static DependencyObject? VisualParentOf(DependencyObject node)
|
||||
=> node is Visual or System.Windows.Media.Media3D.Visual3D
|
||||
? VisualTreeHelper.GetParent(node)
|
||||
: LogicalTreeHelper.GetParent(node);
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user