diff --git a/src/SheetMe.Designer/Behaviors/LayerListBehavior.cs b/src/SheetMe.Designer/Behaviors/LayerListBehavior.cs index 500febf..0f677f8 100644 --- a/src/SheetMe.Designer/Behaviors/LayerListBehavior.cs +++ b/src/SheetMe.Designer/Behaviors/LayerListBehavior.cs @@ -138,6 +138,58 @@ public static class LayerListBehavior { syncing.Remove(list); } + RevealPrimary(list, designer); + } + + /// + /// 캔버스에서 고른 것을 목록이 따라오게 한다 — 조상을 펴고 그 행으로 스크롤한다. + /// + /// 왜 필요한가. 1,067개짜리 서식에서 종이 한가운데 컨트롤을 클릭하면 + /// 목록에는 아무 변화가 없는 경우가 흔했다 — 행이 뷰포트 밖이거나, 조상이 접혀 있어 + /// 행 자체가 만들어지지 않았거나. 강조를 해도 눈에 안 보이니 선택이 안 된 것으로 읽힌다. + /// 반대 방향(목록 더블클릭 → 캔버스)은 이미 잘 되어 있어 비대칭이 더 눈에 띄었다. + /// + /// 펴기는 한 번만 시도한다. 편 결과로 목록이 다시 만들어지면 재구성 이벤트가 + /// 이 경로를 한 번 더 부르는데, 그때는 행이 있으므로 스크롤만 하고 끝난다. + /// 행이 끝내 없으면(검색·타입 필터에 걸려 빠진 경우) 아무것도 하지 않는다 — + /// 필터를 사용자가 건 것이라 임의로 풀면 그쪽이 더 놀랍다. + /// + private static void RevealPrimary(ListBox list, DesignerViewModel designer) + { + if (designer.Selection.Primary is not { } primary) + { + return; + } + if (RowOf(list, primary) is { } row) + { + list.ScrollIntoView(row); + return; + } + if (!designer.ExpandLayerPathTo(primary)) + { + return; // 펼 것이 없는데도 행이 없다 = 필터에 걸린 것이다 + } + // 재구성은 비동기로 도는 경로가 있다 — 한 틱 뒤에 다시 찾아본다 + list.Dispatcher.BeginInvoke(new Action(() => + { + if (RowOf(list, primary) is { } revealed) + { + list.ScrollIntoView(revealed); + } + }), System.Windows.Threading.DispatcherPriority.Background); + } + + /// 이 컨트롤을 담고 있는 목록 행 — 없으면 null(접혔거나 필터에 걸렸다) + private static object? RowOf(ListBox list, ControlViewModel control) + { + foreach (var row in list.Items) + { + if (DesignerViewModel.ControlsOfLayerRow(row).Contains(control)) + { + return row; + } + } + return null; } /// 현재 선택으로부터 강조돼야 할 행들 diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index a1f9433..e952da2 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -1236,6 +1236,22 @@ public static class EditSmoke fresh.LayerSubText == fresh.Id, $"'{fresh.LayerName}' / '{fresh.LayerSubText}'"); + // 접힌 조상 때문에 행 자체가 없는 상태를 편다. + // 접기는 레이어 목록만의 개념이라 캔버스에는 자식이 그대로 보인다 — + // 종이에서 클릭했는데 목록에 아무 반응이 없으면 어느 쪽이 맞는지 알 수 없다. + wrapped.IsLayerExpanded = false; + tree.RebuildLayerRows(); + var hiddenChild = wrapped.Children[0]; + Check("자동 스크롤: 접히면 자식 행이 목록에 없다", + !tree.LayerRows.Any(r => DesignerViewModel.ControlsOfLayerRow(r).Contains(hiddenChild))); + + Check("자동 스크롤: 조상을 펴면 true 를 돌려준다", tree.ExpandLayerPathTo(hiddenChild)); + tree.RebuildLayerRows(); + Check("자동 스크롤: 편 뒤에는 행이 생긴다", + tree.LayerRows.Any(r => DesignerViewModel.ControlsOfLayerRow(r).Contains(hiddenChild))); + Check("자동 스크롤: 이미 펴져 있으면 false(무한 재구성 방지)", + !tree.ExpandLayerPathTo(hiddenChild)); + var bare = new ControlElement { Type = "TextBox", Id = "TextBox9", diff --git a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs index fcdffbd..2cb669e 100644 --- a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs @@ -2780,6 +2780,46 @@ public sealed class DesignerViewModel : ViewModelBase /// 페이지 펼침 상태 — 기본은 펼침. 키를 인덱스가 아니라 페이지 인스턴스로 잡는다 /// (인덱스로 잡으면 페이지를 중간에 끼우거나 지웠을 때 엉뚱한 페이지가 접힌다) + /// + /// 이 컨트롤의 행이 목록에 나타나도록 조상을 전부 편다 — 실제로 편 것이 있으면 true. + /// + /// 왜 필요한가. 접기는 레이어 목록만의 개념이라 캔버스에는 자식이 그대로 보인다. + /// 그래서 종이 한가운데 컨트롤을 클릭했는데 그 조상이 접혀 있으면 + /// 목록에는 행 자체가 없어 아무 반응이 없다 — 사용자 눈에는 선택이 안 된 것으로 보이고, + /// 캔버스와 목록 중 어느 쪽이 맞는지 판단할 단서를 잃는다. + /// 접기·검색·그룹을 쓸수록 어긋나므로, 기존 기능이 오히려 신뢰를 깎고 있었다. + /// + /// 반대 방향(목록 더블클릭 → 캔버스로 스크롤)은 로 이미 있다 — + /// 이것은 그 역방향을 잇는 일이지 새 기능이 아니다. + /// + public bool ExpandLayerPathTo(ControlViewModel viewModel) + { + var changed = false; + + // 페이지 마디 — 공개 메서드를 쓴다(목록 재구성 요청까지 함께 도는 경로다) + if (PageOf(viewModel) is { } page && !IsPageExpanded(page)) + { + SetPageExpanded(page, true); + changed = true; + } + // 그룹 폴더 — 그룹 멤버는 폴더가 접히면 행이 안 만들어진다 + if (viewModel.Model.GroupId is { } groupId && !IsGroupExpanded(groupId)) + { + SetGroupExpanded(groupId, true); + changed = true; + } + // 컨테이너 조상 — 자기 자신은 펴지 않는다(자기 행은 이미 있다) + for (var parent = viewModel.Parent; parent is not null; parent = parent.Parent) + { + if (!parent.IsLayerExpanded) + { + parent.IsLayerExpanded = true; + changed = true; + } + } + return changed; + } + public bool IsPageExpanded(PageViewModel page) => !collapsedPages.Contains(page); /// 페이지 펼침/접기