레이어 패널에 [200]SheetMe 그룹 기능 이식 — 폴더 행·계층·타입 필터·다중선택
[200] 의 그룹도 컨테이너가 아니라 GroupId 태그 + 이름 사전이었다. SheetMe 가 이미 같은 구조(ControlElement.GroupId, JsonIgnore)를 갖고 있어 저장 포맷은 그대로 두고 표시·조작 계층만 얹는다 — 왕복 XML 에는 아무 흔적도 남지 않는다. 레이어 목록을 평탄화 결과(LayerRows)로 바꿨다. 그룹 폴더 행과 컨트롤 행이 한 시퀀스로 섞이고, 계층은 들여쓰기 여백으로만 표현한다(TreeView 를 쓰면 가상화와 기존 행 템플릿을 다 버려야 한다 — 인스펙터 섹션이 쓰는 방식과 같게 맞췄다). 새로 되는 것 - 그룹 폴더 행: 접기/펼치기, 멤버 수, 이름 인라인 편집(더블클릭), 그룹 전체 눈·자물쇠 - 그룹 이름 — FormDocument.GroupNames(편집 전용). 비우면 기본 그룹 으로 복귀 - 컨테이너(패널/그룹박스) 자식이 레이어에 보인다. 지금까지는 아예 접근할 수 없었다 - 목록 다중선택(Shift/Ctrl) — 캔버스 선택과 양방향 동기 - 타입 필터: 그 페이지에 실제로 있는 타입만 후보로 띄운다 - 검색 0건·빈 페이지 안내 문구 함께 고친 결함 - 붙여넣기가 원본 그룹에 끼어들었다(Clone 이 GroupId 를 그대로 복사) → 복제본끼리 새 그룹 - 그룹 해제가 선택된 멤버만 풀어 1개짜리 반쪽 그룹이 남았다 → 그룹 전 멤버를 푼다 - 멤버가 1개 이하로 줄어든 그룹이 계속 남았다 → CleanupGroups 자가치유 - 셰브론이 늘 눌린 것처럼 보였다(IsChecked 채움) → MiniChevron 스타일 분리 --edit-smoke 에 레이어 검사 19건 추가(그룹 위생·평탄화·필터·XML 무영향). 회귀: 테스트 124/124, edit-smoke 실패 0, db-smoke 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
d09bd90c8e
commit
a30421f9b5
@@ -0,0 +1,159 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
using SheetMe.Designer.ViewModels.Controls;
|
||||
|
||||
namespace SheetMe.Designer.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// 레이어 목록 ↔ 캔버스 선택 양방향 동기.
|
||||
///
|
||||
/// 목록을 <c>SelectedItem</c> 한 개로 바인딩하면 다중선택이 불가능하고(그룹 폴더 행을 고르면
|
||||
/// 형이 맞지 않아 바인딩이 조용히 실패한다), 반대로 선택 서비스만 진실로 두면 목록이 캔버스 선택을
|
||||
/// 따라오지 못한다. 그래서 <c>SelectionMode=Extended</c> 로 두고 여기서 양쪽을 잇는다.
|
||||
///
|
||||
/// 진실은 언제나 <see cref="Services.SelectionService"/> 다 — 목록은 그 투영이며,
|
||||
/// 잠긴 컨트롤처럼 선택에서 걸러진 항목은 목록에서도 도로 풀린다.
|
||||
/// </summary>
|
||||
public static class LayerListBehavior
|
||||
{
|
||||
#region Attached Properties
|
||||
/// <summary>동기 대상 디자이너 — 문서 탭이 바뀌면 구독을 갈아탄다</summary>
|
||||
public static readonly DependencyProperty DesignerProperty = DependencyProperty.RegisterAttached(
|
||||
"Designer", typeof(DesignerViewModel), typeof(LayerListBehavior),
|
||||
new PropertyMetadata(null, OnDesignerChanged));
|
||||
|
||||
public static void SetDesigner(DependencyObject element, DesignerViewModel? value)
|
||||
=> element.SetValue(DesignerProperty, value);
|
||||
|
||||
public static DesignerViewModel? GetDesigner(DependencyObject element)
|
||||
=> (DesignerViewModel?)element.GetValue(DesignerProperty);
|
||||
#endregion
|
||||
|
||||
#region Member Fields
|
||||
// 되먹임 차단 — 목록→서비스 반영 중에 서비스→목록 동기가 돌면 선택이 진동한다
|
||||
private static readonly HashSet<ListBox> syncing = new();
|
||||
private static readonly Dictionary<ListBox, (DesignerViewModel Designer, Action OnSelection, Action OnRebuilt)> hooks = new();
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private static void OnDesignerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is not ListBox list)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hooks.Remove(list, out var previous))
|
||||
{
|
||||
previous.Designer.Selection.SetChanged -= previous.OnSelection;
|
||||
previous.Designer.LayerRowsRebuilt -= previous.OnRebuilt;
|
||||
}
|
||||
list.SelectionChanged -= OnListSelectionChanged;
|
||||
list.SelectionChanged += OnListSelectionChanged;
|
||||
|
||||
if (e.NewValue is not DesignerViewModel designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
void OnSelection() => SyncFromSelection(list, designer);
|
||||
// 목록을 다시 만들면 ListBox 선택이 통째로 비워진다 — 그 직후 강조를 되살린다
|
||||
void OnRebuilt() => SyncFromSelection(list, designer);
|
||||
hooks[list] = (designer, OnSelection, OnRebuilt);
|
||||
designer.Selection.SetChanged += OnSelection;
|
||||
designer.LayerRowsRebuilt += OnRebuilt;
|
||||
SyncFromSelection(list, designer);
|
||||
}
|
||||
|
||||
/// <summary>목록 → 선택 서비스. 그룹 폴더 행은 멤버 전체로 펼쳐서 넘긴다.</summary>
|
||||
private static void OnListSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is not ListBox list || syncing.Contains(list) || GetDesigner(list) is not { } designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 목록 재구성 중의 선택 해제는 사용자의 조작이 아니다 — 그대로 흘리면 캔버스 선택이 풀린다
|
||||
if (designer.IsRebuildingLayers)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targets = new List<ControlViewModel>();
|
||||
foreach (var row in list.SelectedItems)
|
||||
{
|
||||
foreach (var control in DesignerViewModel.ControlsOfLayerRow(row))
|
||||
{
|
||||
if (!targets.Contains(control))
|
||||
{
|
||||
targets.Add(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막에 고른 것을 Primary 로 — 정렬·크기맞춤의 기준 컨트롤이 된다
|
||||
var primary = e.AddedItems.Count > 0
|
||||
? DesignerViewModel.ControlsOfLayerRow(e.AddedItems[^1]).LastOrDefault()
|
||||
: null;
|
||||
|
||||
syncing.Add(list);
|
||||
try
|
||||
{
|
||||
designer.Selection.Set(targets, primary ?? targets.LastOrDefault());
|
||||
}
|
||||
finally
|
||||
{
|
||||
syncing.Remove(list);
|
||||
}
|
||||
|
||||
// 결과가 목록의 현재 강조와 다를 때만 되맞춘다 — 그룹 폴더를 골라 멤버까지 켜야 하거나,
|
||||
// 서비스가 잠긴 항목을 걸러낸 경우다.
|
||||
//
|
||||
// 항상 되맞추면 SelectedItems 를 갈아 끼우면서 ListBox 의 <b>범위 선택 기준점(anchor)</b>이
|
||||
// 매번 초기화돼 Shift+클릭·Shift+↓ 가 연속 범위가 아니라 띄엄띄엄 골라진다.
|
||||
var expected = ExpectedRows(list, designer);
|
||||
if (expected.Count != list.SelectedItems.Count
|
||||
|| expected.Any(row => !list.SelectedItems.Contains(row)))
|
||||
{
|
||||
SyncFromSelection(list, designer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>선택 서비스 → 목록. 그룹 폴더는 멤버가 전부 선택됐을 때만 함께 강조한다.</summary>
|
||||
private static void SyncFromSelection(ListBox list, DesignerViewModel designer)
|
||||
{
|
||||
if (syncing.Contains(list))
|
||||
{
|
||||
return;
|
||||
}
|
||||
syncing.Add(list);
|
||||
try
|
||||
{
|
||||
list.SelectedItems.Clear();
|
||||
foreach (var row in ExpectedRows(list, designer))
|
||||
{
|
||||
list.SelectedItems.Add(row);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
syncing.Remove(list);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>현재 선택으로부터 강조돼야 할 행들</summary>
|
||||
private static List<object> ExpectedRows(ListBox list, DesignerViewModel designer)
|
||||
{
|
||||
var selected = designer.Selection.Items;
|
||||
var rows = new List<object>();
|
||||
foreach (var row in list.Items)
|
||||
{
|
||||
var controls = DesignerViewModel.ControlsOfLayerRow(row).ToList();
|
||||
if (controls.Count > 0 && controls.All(selected.Contains))
|
||||
{
|
||||
rows.Add(row);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user