Compare commits
6
Commits
f6b456753c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5771a824d7 | ||
|
|
592275fa97 | ||
|
|
3e75bdc9b9 | ||
|
|
4b400b94e0 | ||
|
|
6a0331ec8a | ||
|
|
8c76a978a9 |
@@ -66,7 +66,9 @@ public static class ControlRegistry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static PropertyDef DataTableField => new()
|
public static PropertyDef DataTableField => new()
|
||||||
{
|
{
|
||||||
Key = "DataTableField", Label = "데이터 필드", Editor = PropEditorKind.DataTableField,
|
// 라벨에 '배선' 을 쓰지 않는다 — 전산실에서 배선은 랜선·전원이다.
|
||||||
|
// '값 가져오기' 는 방향이 이름에 들어 있어 형제 속성(데이터 태그)과도 구별된다.
|
||||||
|
Key = "DataTableField", Label = "값 가져오기", Editor = PropEditorKind.DataTableField,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,58 @@ public static class LayerListBehavior
|
|||||||
{
|
{
|
||||||
syncing.Remove(list);
|
syncing.Remove(list);
|
||||||
}
|
}
|
||||||
|
RevealPrimary(list, designer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 캔버스에서 고른 것을 목록이 <b>따라오게</b> 한다 — 조상을 펴고 그 행으로 스크롤한다.
|
||||||
|
///
|
||||||
|
/// <b>왜 필요한가.</b> 1,067개짜리 서식에서 종이 한가운데 컨트롤을 클릭하면
|
||||||
|
/// 목록에는 아무 변화가 없는 경우가 흔했다 — 행이 뷰포트 밖이거나, 조상이 접혀 있어
|
||||||
|
/// <b>행 자체가 만들어지지 않았거나</b>. 강조를 해도 눈에 안 보이니 선택이 안 된 것으로 읽힌다.
|
||||||
|
/// 반대 방향(목록 더블클릭 → 캔버스)은 이미 잘 되어 있어 비대칭이 더 눈에 띄었다.
|
||||||
|
///
|
||||||
|
/// <b>펴기는 한 번만 시도한다.</b> 편 결과로 목록이 다시 만들어지면 재구성 이벤트가
|
||||||
|
/// 이 경로를 한 번 더 부르는데, 그때는 행이 있으므로 스크롤만 하고 끝난다.
|
||||||
|
/// 행이 끝내 없으면(검색·타입 필터에 걸려 빠진 경우) 아무것도 하지 않는다 —
|
||||||
|
/// 필터를 사용자가 건 것이라 임의로 풀면 그쪽이 더 놀랍다.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>이 컨트롤을 담고 있는 목록 행 — 없으면 null(접혔거나 필터에 걸렸다)</summary>
|
||||||
|
private static object? RowOf(ListBox list, ControlViewModel control)
|
||||||
|
{
|
||||||
|
foreach (var row in list.Items)
|
||||||
|
{
|
||||||
|
if (DesignerViewModel.ControlsOfLayerRow(row).Contains(control))
|
||||||
|
{
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>현재 선택으로부터 강조돼야 할 행들</summary>
|
/// <summary>현재 선택으로부터 강조돼야 할 행들</summary>
|
||||||
|
|||||||
@@ -1112,6 +1112,73 @@ public static class EditSmoke
|
|||||||
$"ValueText='{choiceRow.ValueText}' IsMixed={choiceRow.IsMixed}");
|
$"ValueText='{choiceRow.ValueText}' IsMixed={choiceRow.IsMixed}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 15-a0000000) 끄는 동안 치수·좌표가 캔버스에 보인다.
|
||||||
|
//
|
||||||
|
// 전에는 숫자가 300px 떨어진 인스펙터에만 떴다 — 끌고, 손 떼고, 오른쪽을 보고, 다시 끈다.
|
||||||
|
// 간격 수치 칩은 균등 스냅이 성립할 때만 나타나므로 "지금 몇 px 인가"는 알 방법이 없었다.
|
||||||
|
{
|
||||||
|
var badge = new DesignerViewModel(new FormDesignDataBusiness().CreateNew());
|
||||||
|
badge.AddControlAt("Label", new Point(80, 80));
|
||||||
|
var one = badge.Pages[0].Controls[0];
|
||||||
|
badge.Selection.SetSingle(one);
|
||||||
|
Check("치수 칩: 평소에는 안 보인다", !badge.Overlay.ShowDragBadge);
|
||||||
|
|
||||||
|
var world = badge.WorldBoundsOf(one);
|
||||||
|
var grab = new Point(world.X + world.Width / 2, world.Y + world.Height / 2);
|
||||||
|
var noSnap = new PointerContext(false, false, true, 1);
|
||||||
|
badge.Interaction.PointerDown(grab, noSnap);
|
||||||
|
badge.Interaction.PointerMove(new Point(grab.X + 30, grab.Y + 10), noSnap);
|
||||||
|
Check("치수 칩: 끄는 동안 좌표가 보인다",
|
||||||
|
badge.Overlay.ShowDragBadge && badge.Overlay.DragBadgeText.StartsWith("X ", StringComparison.Ordinal),
|
||||||
|
badge.Overlay.DragBadgeText);
|
||||||
|
|
||||||
|
badge.Interaction.PointerUp(new Point(grab.X + 30, grab.Y + 10), noSnap);
|
||||||
|
Check("치수 칩: 놓으면 사라진다", !badge.Overlay.ShowDragBadge);
|
||||||
|
|
||||||
|
// 크기 조절일 때는 <b>크기</b>를 보여 준다 — 지금 바꾸고 있는 값이 그것이다
|
||||||
|
badge.Selection.SetSingle(one);
|
||||||
|
var bounds = badge.WorldBoundsOf(one);
|
||||||
|
var handles = ViewModels.SelectionOverlayViewModel.HandlePositions(bounds);
|
||||||
|
badge.Interaction.PointerDown(handles[4], noSnap); // 우하단
|
||||||
|
badge.Interaction.PointerMove(new Point(handles[4].X + 25, handles[4].Y + 12), noSnap);
|
||||||
|
Check("치수 칩: 크기 조절 중에는 크기가 보인다",
|
||||||
|
badge.Overlay.ShowDragBadge && badge.Overlay.DragBadgeText.Contains('×'),
|
||||||
|
badge.Overlay.DragBadgeText);
|
||||||
|
badge.Interaction.PointerUp(new Point(handles[4].X + 25, handles[4].Y + 12), noSnap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15-a000000) 패널 안에 그린 마퀴는 그 안의 것을 고른다.
|
||||||
|
//
|
||||||
|
// 전에는 마퀴가 최상위만 훑어서, 패널 안 체크박스 5개를 한 번에 고르려고
|
||||||
|
// 패널 안쪽을 끌면 패널만 잡히거나 아무것도 안 잡혔다 —
|
||||||
|
// 결국 Ctrl+클릭 반복이나 레이어 목록으로 우회했다.
|
||||||
|
// 컨테이너 자식은 전체 컨트롤의 18.3%(실측)라 드문 경우가 아니다.
|
||||||
|
{
|
||||||
|
var mq = new DesignerViewModel(new FormDesignDataBusiness().CreateNew());
|
||||||
|
mq.AddControlAt("CheckBox", new Point(100, 100));
|
||||||
|
mq.AddControlAt("CheckBox", new Point(100, 140));
|
||||||
|
mq.Selection.Set(mq.Pages[0].Controls.ToList(), mq.Pages[0].Controls[0]);
|
||||||
|
mq.GroupSelection();
|
||||||
|
var box = mq.Pages[0].Controls.OfType<ViewModels.Controls.ContainerViewModel>().First();
|
||||||
|
var boxWorld = mq.WorldBoundsOf(box);
|
||||||
|
|
||||||
|
// ① 패널 <b>안쪽</b>을 끈다 — 자식이 잡혀야 한다
|
||||||
|
var inside = new Rect(boxWorld.X + 2, boxWorld.Y + 2,
|
||||||
|
Math.Max(1, boxWorld.Width - 4), Math.Max(1, boxWorld.Height - 4));
|
||||||
|
var innerHits = mq.MarqueeHits(inside);
|
||||||
|
Check("마퀴: 패널 안에 그리면 안의 것이 잡힌다",
|
||||||
|
innerHits.Count == box.Children.Count
|
||||||
|
&& innerHits.All(h => ReferenceEquals(h.Parent, box)),
|
||||||
|
$"{innerHits.Count}개 (패널 자식 {box.Children.Count}개)");
|
||||||
|
|
||||||
|
// ② 패널을 <b>걸치도록</b> 끈다 — 예전처럼 패널 자체가 잡혀야 한다
|
||||||
|
var across = new Rect(boxWorld.X - 20, boxWorld.Y - 20, boxWorld.Width + 40, 30);
|
||||||
|
var outerHits = mq.MarqueeHits(across);
|
||||||
|
Check("마퀴: 패널에 걸치면 패널이 잡힌다(기존 동작 유지)",
|
||||||
|
outerHits.Count == 1 && ReferenceEquals(outerHits[0], box),
|
||||||
|
$"{outerHits.Count}개 — {string.Join(",", outerHits.Select(h => h.Type))}");
|
||||||
|
}
|
||||||
|
|
||||||
// 15-a00000) 태그 행이 사실대로 말하는가.
|
// 15-a00000) 태그 행이 사실대로 말하는가.
|
||||||
{
|
{
|
||||||
// 카탈로그에 없는 이름은 표식이 붙어야 한다 — 그러나 <b>막지는 않는다</b>.
|
// 카탈로그에 없는 이름은 표식이 붙어야 한다 — 그러나 <b>막지는 않는다</b>.
|
||||||
@@ -1162,6 +1229,53 @@ public static class EditSmoke
|
|||||||
Check("레이어: 컨테이너가 자식 수를 말한다", wrapped.LayerChildCountText == "(2)",
|
Check("레이어: 컨테이너가 자식 수를 말한다", wrapped.LayerChildCountText == "(2)",
|
||||||
wrapped.LayerChildCountText);
|
wrapped.LayerChildCountText);
|
||||||
|
|
||||||
|
// 둘째 줄은 <b>항상</b> 있어야 한다 — 없으면 1줄/2줄이 섞여 훑는 리듬이 깨진다.
|
||||||
|
// 세 갈래를 다 본다: 문구 없음 / 문구가 타입명 그대로(갓 놓은 것) / 사람이 쓴 문구.
|
||||||
|
var fresh = wrapped.Children[0]; // 갓 놓은 라디오 — 문구가 "라디오버튼"이다
|
||||||
|
Check("레이어: 갓 놓은 것은 둘째 줄이 이름뿐이다(타입명 중복 없음)",
|
||||||
|
fresh.LayerSubText == fresh.Id,
|
||||||
|
$"'{fresh.LayerName}' / '{fresh.LayerSubText}'");
|
||||||
|
|
||||||
|
// 접힌 조상 때문에 <b>행 자체가 없는</b> 상태를 편다.
|
||||||
|
// 접기는 레이어 목록만의 개념이라 캔버스에는 자식이 그대로 보인다 —
|
||||||
|
// 종이에서 클릭했는데 목록에 아무 반응이 없으면 어느 쪽이 맞는지 알 수 없다.
|
||||||
|
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",
|
||||||
|
Bounds = new LayoutRect { X = 10, Y = 10, W = 100, H = 20 },
|
||||||
|
};
|
||||||
|
var bareDoc = new FormDocument();
|
||||||
|
var barePage = new FormPage { Width = 720, Height = 856 };
|
||||||
|
barePage.Controls.Add(bare);
|
||||||
|
bareDoc.Pages.Add(barePage);
|
||||||
|
var bareVm = new DesignerViewModel(bareDoc).Pages[0].Controls[0];
|
||||||
|
Check("레이어: 문구가 없는 행도 둘째 줄이 있다", bareVm.LayerSubText.Length > 0,
|
||||||
|
$"'{bareVm.LayerName}' / '{bareVm.LayerSubText}'");
|
||||||
|
Check("레이어: 문구가 없으면 둘째 줄은 타입명이다", bareVm.LayerSubText == "텍스트박스",
|
||||||
|
bareVm.LayerSubText);
|
||||||
|
Check("레이어: 같은 글자를 두 줄에 두 번 쓰지 않는다",
|
||||||
|
!bareVm.LayerSubText.Contains(bareVm.Id, StringComparison.Ordinal),
|
||||||
|
$"Id={bareVm.Id} 둘째줄='{bareVm.LayerSubText}'");
|
||||||
|
|
||||||
|
bare.Props.SetText("Text", "환자명");
|
||||||
|
var named = new DesignerViewModel(bareDoc).Pages[0].Controls[0];
|
||||||
|
Check("레이어: 사람이 쓴 문구면 타입·이름을 함께 보여 준다",
|
||||||
|
named.LayerSubText == "텍스트박스 · TextBox9",
|
||||||
|
$"'{named.LayerName}' / '{named.LayerSubText}'");
|
||||||
|
|
||||||
// 중첩까지 센다 — "이걸 지우면 몇 개가 같이 지워지나"가 그 수다.
|
// 중첩까지 센다 — "이걸 지우면 몇 개가 같이 지워지나"가 그 수다.
|
||||||
// 중첩 구조는 모델로 직접 짠다(Ctrl+G 를 두 번 겹치는 경로는 이 검사의 대상이 아니다).
|
// 중첩 구조는 모델로 직접 짠다(Ctrl+G 를 두 번 겹치는 경로는 이 검사의 대상이 아니다).
|
||||||
var nestDoc = new FormDocument();
|
var nestDoc = new FormDocument();
|
||||||
@@ -2401,22 +2515,25 @@ public static class EditSmoke
|
|||||||
designer.Inspector.SelectedTab = ViewModels.Inspector.InspectorTab.Data;
|
designer.Inspector.SelectedTab = ViewModels.Inspector.InspectorTab.Data;
|
||||||
var dtfRow = designer.Inspector.Rows
|
var dtfRow = designer.Inspector.Rows
|
||||||
.OfType<ViewModels.Inspector.DataTableFieldRowViewModel>().FirstOrDefault();
|
.OfType<ViewModels.Inspector.DataTableFieldRowViewModel>().FirstOrDefault();
|
||||||
Check("배선: 전용 편집 행이 붙는다", dtfRow is not null);
|
Check("값 가져오기: 전용 편집 행이 붙는다", dtfRow is not null);
|
||||||
if (dtfRow is null)
|
if (dtfRow is null)
|
||||||
{
|
{
|
||||||
designer.Inspector.SelectedTab = previousTab;
|
designer.Inspector.SelectedTab = previousTab;
|
||||||
goto skipWiring;
|
goto skipWiring;
|
||||||
}
|
}
|
||||||
Check("배선: 값이 없으면 안내 문구", dtfRow.Summary == "(배선 없음)", dtfRow.Summary);
|
// 빈 상태 문구는 대화상자의 지우기 버튼과 <b>글자까지 같다</b> —
|
||||||
|
// 그 버튼을 누르면 무엇이 되는지 미리 읽히게 하려는 것이다
|
||||||
|
Check("값 가져오기: 값이 없으면 지우기 버튼과 같은 말을 쓴다",
|
||||||
|
dtfRow.Summary == "(가져오지 않음)", dtfRow.Summary);
|
||||||
|
|
||||||
// 운영 다수파인 Rows 형을 읽어야 한다(1,624건, 76%)
|
// 운영 다수파인 Rows 형을 읽어야 한다(1,624건, 76%)
|
||||||
dtfRow.ValueText = $"{source.Id}.Rows(0).Item(\"ALGYON\")";
|
dtfRow.ValueText = $"{source.Id}.Rows(0).Item(\"ALGYON\")";
|
||||||
Check("배선: Rows 형을 읽는다", dtfRow.Summary.Contains(source.Id) && dtfRow.Summary.Contains("ALGYON"),
|
Check("값 가져오기: 저장된 값을 사람 말로 요약한다", dtfRow.Summary.Contains(source.Id) && dtfRow.Summary.Contains("ALGYON"),
|
||||||
dtfRow.Summary);
|
dtfRow.Summary);
|
||||||
|
|
||||||
// 해석 못 하는 값은 조용히 고치지 않고 경고로 드러낸다
|
// 해석 못 하는 값은 조용히 고치지 않고 경고로 드러낸다
|
||||||
dtfRow.ValueText = "MDataTable1.Rows";
|
dtfRow.ValueText = "MDataTable1.Rows";
|
||||||
Check("배선: 미완성 값은 경고로 드러낸다", dtfRow.Summary.StartsWith("⚠"), dtfRow.Summary);
|
Check("값 가져오기: 미완성 값은 경고로 드러낸다", dtfRow.Summary.StartsWith("⚠"), dtfRow.Summary);
|
||||||
|
|
||||||
dtfRow.ValueText = string.Empty;
|
dtfRow.ValueText = string.Empty;
|
||||||
designer.Inspector.SelectedTab = previousTab;
|
designer.Inspector.SelectedTab = previousTab;
|
||||||
|
|||||||
@@ -233,20 +233,21 @@ public sealed class InteractionController
|
|||||||
designer.ReassignPagesAfterMove(origBounds.Keys.ToList());
|
designer.ReassignPagesAfterMove(origBounds.Keys.ToList());
|
||||||
designer.Overlay.ClearGuides();
|
designer.Overlay.ClearGuides();
|
||||||
designer.Snap.EndDrag();
|
designer.Snap.EndDrag();
|
||||||
|
designer.Overlay.ClearDragBadge();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode.Resize:
|
case Mode.Resize:
|
||||||
designer.Overlay.ClearGuides();
|
designer.Overlay.ClearGuides();
|
||||||
designer.Snap.EndDrag();
|
designer.Snap.EndDrag();
|
||||||
|
designer.Overlay.ClearDragBadge();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode.Marquee:
|
case Mode.Marquee:
|
||||||
var rect = RectFrom(downPoint, world);
|
var rect = RectFrom(downPoint, world);
|
||||||
designer.Overlay.UpdateMarquee(null);
|
designer.Overlay.UpdateMarquee(null);
|
||||||
var hits = designer.ControlsIntersecting(rect)
|
// 마퀴 전용 규칙 — 그룹은 넓히되 컨테이너 자식은 부모로 되돌리지 않는다.
|
||||||
.SelectMany(designer.GroupMatesOf)
|
// GroupMatesOf 를 그대로 쓰면 패널 안쪽을 정확히 골라 놓고도 결과가 패널 하나가 된다.
|
||||||
.Distinct()
|
var hits = designer.MarqueeHits(rect);
|
||||||
.ToList();
|
|
||||||
if (ctx.Ctrl || ctx.Shift)
|
if (ctx.Ctrl || ctx.Shift)
|
||||||
{
|
{
|
||||||
var merged = designer.Selection.Items.Union(hits).ToList();
|
var merged = designer.Selection.Items.Union(hits).ToList();
|
||||||
@@ -285,6 +286,7 @@ public sealed class InteractionController
|
|||||||
{
|
{
|
||||||
designer.Overlay.ClearGuides();
|
designer.Overlay.ClearGuides();
|
||||||
designer.Snap.EndDrag();
|
designer.Snap.EndDrag();
|
||||||
|
designer.Overlay.ClearDragBadge();
|
||||||
mode = Mode.None;
|
mode = Mode.None;
|
||||||
origBounds.Clear();
|
origBounds.Clear();
|
||||||
downControl = null;
|
downControl = null;
|
||||||
@@ -443,6 +445,11 @@ public sealed class InteractionController
|
|||||||
}
|
}
|
||||||
|
|
||||||
designer.Selection.NotifyBoundsChanged();
|
designer.Selection.NotifyBoundsChanged();
|
||||||
|
// 끄는 동안 지금 값을 보여 준다 — 숫자가 인스펙터에만 있으면 끌다 말고 오른쪽을 봐야 한다
|
||||||
|
if (designer.SelectionWorldBounds() is { } movedBounds)
|
||||||
|
{
|
||||||
|
designer.Overlay.SetDragBadge(movedBounds, resizing: false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DoResize(Point world, PointerContext ctx)
|
private void DoResize(Point world, PointerContext ctx)
|
||||||
@@ -509,6 +516,10 @@ public sealed class InteractionController
|
|||||||
}
|
}
|
||||||
|
|
||||||
designer.Selection.NotifyBoundsChanged();
|
designer.Selection.NotifyBoundsChanged();
|
||||||
|
if (designer.SelectionWorldBounds() is { } sizedBounds)
|
||||||
|
{
|
||||||
|
designer.Overlay.SetDragBadge(sizedBounds, resizing: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>드래그 첫 실변경 시 1회 스냅샷 — 드래그 전체 = Undo 1스텝</summary>
|
/// <summary>드래그 첫 실변경 시 1회 스냅샷 — 드래그 전체 = Undo 1스텝</summary>
|
||||||
|
|||||||
@@ -199,6 +199,33 @@ public abstract class ControlViewModel : ViewModelBase, ILayerRow
|
|||||||
/// <summary>이름 칸을 그릴지 — 문구와 이름이 다를 때만</summary>
|
/// <summary>이름 칸을 그릴지 — 문구와 이름이 다를 때만</summary>
|
||||||
public bool HasLayerId => LayerId.Length > 0;
|
public bool HasLayerId => LayerId.Length > 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 레이어 행 둘째 줄 — <b>항상</b> 있다. 그래야 행 높이가 일정하다.
|
||||||
|
///
|
||||||
|
/// 전에는 표시명이 곧 Id 인 행(문구가 없는 컨트롤)에서 둘째 줄이 통째로 빠져
|
||||||
|
/// 목록에 1줄과 2줄이 섞였다. 훑을 때 리듬이 깨지고, 어느 행이 무엇인지
|
||||||
|
/// 눈이 매번 다시 자리를 잡아야 했다.
|
||||||
|
///
|
||||||
|
/// <b>타입명을 앞에 둔다.</b> 아이콘만으로는 타입이 안 갈리는 짝이 실제로 있다 —
|
||||||
|
/// 라벨과 마스크입력이 같은 글리프를 쓰고, 리스트박스와 체크리스트도 같다.
|
||||||
|
/// 이름(Id)은 표시명과 다를 때만 뒤에 붙인다 — 같으면 윗줄과 같은 글자를 두 번 쓰는 셈이다.
|
||||||
|
/// 이름을 계속 보여 주는 이유는 그것이 <b>외부 참조 키</b>이기 때문이다(수식·액션 대상·이벤트 규약).
|
||||||
|
/// </summary>
|
||||||
|
public string LayerSubText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var typeName = SheetMe.Core.Catalog.ControlRegistry.Find(Type)?.DisplayName ?? Type;
|
||||||
|
// 이름이 곧 표시명이면(문구 없는 컨트롤) 타입만 — 같은 글자를 두 번 쓰지 않는다
|
||||||
|
if (!HasLayerId)
|
||||||
|
{
|
||||||
|
return typeName;
|
||||||
|
}
|
||||||
|
// 갓 놓은 컨트롤은 문구가 타입명 그대로다("라디오버튼") — 윗줄이 이미 타입을 말했으므로 이름만
|
||||||
|
return LayerName == typeName ? LayerId : $"{typeName} · {LayerId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>레이어 잠금 토글 아이콘명</summary>
|
/// <summary>레이어 잠금 토글 아이콘명</summary>
|
||||||
public string LockIcon => Model.Locked ? "lock" : "unlock";
|
public string LockIcon => Model.Locked ? "lock" : "unlock";
|
||||||
|
|
||||||
|
|||||||
@@ -864,21 +864,70 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
var result = new List<ControlViewModel>();
|
var result = new List<ControlViewModel>();
|
||||||
foreach (var page in Pages)
|
foreach (var page in Pages)
|
||||||
{
|
{
|
||||||
foreach (var vm in page.Controls)
|
CollectIntersecting(page.Controls, worldRect, result);
|
||||||
{
|
|
||||||
if (vm.Model.Hidden || vm.Model.Locked)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (worldRect.IntersectsWith(WorldBoundsOf(vm)))
|
|
||||||
{
|
|
||||||
result.Add(vm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 마퀴에 걸리는 것을 모은다 — <b>컨테이너 안에 그린 마퀴는 그 안의 것을 고른다</b>.
|
||||||
|
///
|
||||||
|
/// 전에는 최상위만 훑어서, 패널 안 체크박스 5개를 한 번에 고르려고 패널 안쪽을 끌면
|
||||||
|
/// 패널만 잡히거나 아무것도 안 잡혔다 — 결국 Ctrl+클릭 반복이나 레이어 목록으로 우회했다.
|
||||||
|
/// 컨테이너 자식은 전체 컨트롤의 18.3%(실측 18,749/컨테이너 4,099개)라 드문 경우가 아니다.
|
||||||
|
///
|
||||||
|
/// <b>규칙은 '담김'이다.</b> 마퀴가 컨테이너 사각형 <b>안에</b> 들어가 있으면
|
||||||
|
/// 사용자는 그 안의 것을 고르려는 것이다(내 상자가 패널보다 작으니까).
|
||||||
|
/// 걸치기만 하면 예전처럼 컨테이너 자체가 잡힌다 — 패널을 통째로 고르는 조작이 그대로 남는다.
|
||||||
|
/// 모드도 수정키도 없이 그린 모양만으로 갈리므로 예측할 수 있다.
|
||||||
|
/// </summary>
|
||||||
|
private void CollectIntersecting(IEnumerable<ControlViewModel> controls, Rect worldRect,
|
||||||
|
List<ControlViewModel> result)
|
||||||
|
{
|
||||||
|
foreach (var vm in controls)
|
||||||
|
{
|
||||||
|
if (vm.Model.Hidden || vm.Model.Locked)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var bounds = WorldBoundsOf(vm);
|
||||||
|
if (!worldRect.IntersectsWith(bounds))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (vm is ContainerViewModel container && bounds.Contains(worldRect))
|
||||||
|
{
|
||||||
|
CollectIntersecting(container.Children, worldRect, result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.Add(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 마퀴가 최종적으로 고를 것 — 그룹은 통째로 넓히되 <b>컨테이너 자식은 그대로 둔다</b>.
|
||||||
|
///
|
||||||
|
/// 클릭 선택은 <see cref="GroupMatesOf"/> 를 쓰는데 그것은 자식을 부모 컨테이너로 되돌린다
|
||||||
|
/// (패널 안의 것을 집으면 패널이 움직여야 하므로 옳다). 마퀴에 그것을 그대로 쓰면
|
||||||
|
/// 안쪽을 정확히 골라 놓고도 결과가 다시 패널 하나가 되어, 위 규칙이 아무 일도 하지 않는다.
|
||||||
|
/// </summary>
|
||||||
|
public List<ControlViewModel> MarqueeHits(Rect worldRect)
|
||||||
|
{
|
||||||
|
var hits = ControlsIntersecting(worldRect);
|
||||||
|
var result = new List<ControlViewModel>();
|
||||||
|
foreach (var vm in hits)
|
||||||
|
{
|
||||||
|
// 그룹(GroupId)은 페이지 최상위 개념이다 — 컨테이너 자식에는 붙지 않는다
|
||||||
|
if (vm.Parent is null && vm.Model.GroupId is not null)
|
||||||
|
{
|
||||||
|
result.AddRange(GroupMatesOf(vm));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.Add(vm);
|
||||||
|
}
|
||||||
|
return result.Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>오버레이(선택 박스/핸들) 재계산</summary>
|
/// <summary>오버레이(선택 박스/핸들) 재계산</summary>
|
||||||
public void RefreshOverlay()
|
public void RefreshOverlay()
|
||||||
{
|
{
|
||||||
@@ -2731,6 +2780,46 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
|
|
||||||
/// <summary>페이지 펼침 상태 — 기본은 펼침. 키를 인덱스가 아니라 <b>페이지 인스턴스</b>로 잡는다
|
/// <summary>페이지 펼침 상태 — 기본은 펼침. 키를 인덱스가 아니라 <b>페이지 인스턴스</b>로 잡는다
|
||||||
/// (인덱스로 잡으면 페이지를 중간에 끼우거나 지웠을 때 엉뚱한 페이지가 접힌다)</summary>
|
/// (인덱스로 잡으면 페이지를 중간에 끼우거나 지웠을 때 엉뚱한 페이지가 접힌다)</summary>
|
||||||
|
/// <summary>
|
||||||
|
/// 이 컨트롤의 행이 목록에 <b>나타나도록</b> 조상을 전부 편다 — 실제로 편 것이 있으면 true.
|
||||||
|
///
|
||||||
|
/// <b>왜 필요한가.</b> 접기는 레이어 목록만의 개념이라 캔버스에는 자식이 그대로 보인다.
|
||||||
|
/// 그래서 종이 한가운데 컨트롤을 클릭했는데 그 조상이 접혀 있으면
|
||||||
|
/// 목록에는 <b>행 자체가 없어</b> 아무 반응이 없다 — 사용자 눈에는 선택이 안 된 것으로 보이고,
|
||||||
|
/// 캔버스와 목록 중 어느 쪽이 맞는지 판단할 단서를 잃는다.
|
||||||
|
/// 접기·검색·그룹을 쓸수록 어긋나므로, 기존 기능이 오히려 신뢰를 깎고 있었다.
|
||||||
|
///
|
||||||
|
/// 반대 방향(목록 더블클릭 → 캔버스로 스크롤)은 <see cref="RevealControl"/> 로 이미 있다 —
|
||||||
|
/// 이것은 <b>그 역방향을 잇는 일</b>이지 새 기능이 아니다.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
public bool IsPageExpanded(PageViewModel page) => !collapsedPages.Contains(page);
|
||||||
|
|
||||||
/// <summary>페이지 펼침/접기</summary>
|
/// <summary>페이지 펼침/접기</summary>
|
||||||
|
|||||||
@@ -922,10 +922,17 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label,
|
PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label,
|
||||||
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags,
|
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags,
|
||||||
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)) { IsActionTag = true },
|
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)) { IsActionTag = true },
|
||||||
PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label),
|
// 컨트롤 이름을 넘긴다 — 안 넘기면 인스펙터로 연 편집기에서만 《컨트롤명》 칩이 사라져
|
||||||
|
// 같은 창이 진입 경로에 따라 능력이 달라진다
|
||||||
|
PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label) { ControlNamesOf = ControlNames },
|
||||||
PropEditorKind.Mask => new MaskRowViewModel(def.Label),
|
PropEditorKind.Mask => new MaskRowViewModel(def.Label),
|
||||||
// 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다
|
// 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다
|
||||||
PropEditorKind.DataTableField => new DataTableFieldRowViewModel(def.Label, DataTableNames),
|
PropEditorKind.DataTableField => new DataTableFieldRowViewModel(def.Label, DataTableNames)
|
||||||
|
{
|
||||||
|
// 컬럼 목록과 대상 컨트롤 이름도 함께 준다 — 창이 사용자를 밖으로 내보내지 않게 한다
|
||||||
|
OwnerLabelOf = () => designer.Selection.Primary?.Id ?? string.Empty,
|
||||||
|
ColumnsOf = DataSourceColumns,
|
||||||
|
},
|
||||||
_ => new TextRowViewModel(def.Label),
|
_ => new TextRowViewModel(def.Label),
|
||||||
};
|
};
|
||||||
// 선의 방향·굵기는 값만 바꾸는 것이 아니라 <b>크기까지</b> 바꾼다(ApplyLineChange).
|
// 선의 방향·굵기는 값만 바꾸는 것이 아니라 <b>크기까지</b> 바꾼다(ApplyLineChange).
|
||||||
@@ -953,6 +960,67 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
/// 레거시 편집기는 열려 있는 <b>모든</b> 디자인 서피스를 훑어 다른 서식의 데이터소스까지 섞어 보여 주고,
|
/// 레거시 편집기는 열려 있는 <b>모든</b> 디자인 서피스를 훑어 다른 서식의 데이터소스까지 섞어 보여 주고,
|
||||||
/// 그걸 고르면 런타임에 조용히 실패한다(bzMDataTableField.vb:18-63). 우리는 현재 문서로 한정한다.
|
/// 그걸 고르면 런타임에 조용히 실패한다(bzMDataTableField.vb:18-63). 우리는 현재 문서로 한정한다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// 이 서식의 모든 컨트롤 이름 — 쿼리 편집기의 《컨트롤명》 치환 후보다.
|
||||||
|
///
|
||||||
|
/// 데이터소스 아이콘에서 편집기를 열면 이 목록이 넘어가는데 인스펙터의 쿼리 행에서는
|
||||||
|
/// 안 넘어가서, 같은 창이 진입 경로에 따라 능력이 달랐다.
|
||||||
|
/// </summary>
|
||||||
|
private IReadOnlyList<string> ControlNames()
|
||||||
|
{
|
||||||
|
var names = new List<string>();
|
||||||
|
void Visit(SheetMe.Core.Models.ControlElement element)
|
||||||
|
{
|
||||||
|
if (element.Id.Length > 0)
|
||||||
|
{
|
||||||
|
names.Add(element.Id);
|
||||||
|
}
|
||||||
|
foreach (var child in element.Children)
|
||||||
|
{
|
||||||
|
Visit(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var page in designer.Document.Pages)
|
||||||
|
{
|
||||||
|
foreach (var control in page.Controls)
|
||||||
|
{
|
||||||
|
Visit(control);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names.Sort(StringComparer.Ordinal);
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터소스의 조회 결과 컬럼 이름 — 못 읽으면 null(미접속·쿼리 실패).
|
||||||
|
///
|
||||||
|
/// 값 가져오기 창이 컬럼을 목록으로 보여 줄 수 있게 하는 재료다. 전에는 그 창이
|
||||||
|
/// "컬럼 후보는 목록으로 제시하지 않습니다" 라고 적어 두고 사용자를 밖으로 보냈는데,
|
||||||
|
/// 그 창은 모달이라 밖이 잠겨 있다 — 취소하고 쿼리를 열어 컬럼명을 외워 다시 와야 했다.
|
||||||
|
///
|
||||||
|
/// 실행은 <see cref="Services.MDataTableRunner"/> 가 한다 — 접속·치환·캐시·실패 처리가
|
||||||
|
/// 이미 그 한 군데에 있고, 여기서 다시 만들면 두 벌이 된다.
|
||||||
|
/// 러너가 데이터소스별로 결과를 캐시하므로 창이 여러 번 물어도 조회는 한 번이다.
|
||||||
|
/// </summary>
|
||||||
|
private IReadOnlyList<string>? DataSourceColumns(string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var patient = Services.PatientSession.Current;
|
||||||
|
var runner = new Services.MDataTableRunner(designer.Document,
|
||||||
|
patient is null ? null
|
||||||
|
: new Services.PatientQueryVariableSource(patient, Services.UserSession.Current.UidCod));
|
||||||
|
var result = runner.Run(name);
|
||||||
|
return result.Ok && result.Columns.Count > 0 ? result.Columns : null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 컬럼 목록은 <b>도움</b>이다 — 못 읽는다고 창이 안 열려서는 안 된다.
|
||||||
|
// 창은 null 을 받으면 이유를 적고 직접 입력 칸을 남긴다.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IReadOnlyList<string> DataTableNames()
|
private IReadOnlyList<string> DataTableNames()
|
||||||
{
|
{
|
||||||
var names = new List<string>();
|
var names = new List<string>();
|
||||||
|
|||||||
@@ -782,9 +782,20 @@ public sealed class QueryRowViewModel : PropertyRowViewModel
|
|||||||
|
|
||||||
protected override void OnValueApplied() => OnPropertyChanged(nameof(Summary));
|
protected override void OnValueApplied() => OnPropertyChanged(nameof(Summary));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이 서식의 컨트롤 이름 — 편집기의 《컨트롤명》 치환 후보가 된다.
|
||||||
|
///
|
||||||
|
/// 안 넘기면 <b>같은 창이 진입 경로에 따라 능력이 달라진다</b> —
|
||||||
|
/// 데이터소스 아이콘에서 열면 컨트롤 변수 칩이 나오는데
|
||||||
|
/// 인스펙터의 쿼리 행에서 열면 그 칩이 통째로 사라졌다.
|
||||||
|
/// 목록은 문서마다 다르므로 카탈로그가 아니라 호출부가 준다.
|
||||||
|
/// </summary>
|
||||||
|
public Func<IReadOnlyList<string>>? ControlNamesOf { get; init; }
|
||||||
|
|
||||||
private void OnEdit()
|
private void OnEdit()
|
||||||
{
|
{
|
||||||
var dialog = new Views.QueryEditorWindow(Label, ValueText)
|
var dialog = new Views.QueryEditorWindow(Label, ValueText,
|
||||||
|
showVariables: true, controlNames: ControlNamesOf?.Invoke())
|
||||||
{
|
{
|
||||||
Owner = System.Windows.Application.Current.MainWindow,
|
Owner = System.Windows.Application.Current.MainWindow,
|
||||||
};
|
};
|
||||||
@@ -957,7 +968,12 @@ public sealed class DataTableFieldRowViewModel : PropertyRowViewModel
|
|||||||
{
|
{
|
||||||
private readonly Func<IReadOnlyList<string>> tablesOf;
|
private readonly Func<IReadOnlyList<string>> tablesOf;
|
||||||
|
|
||||||
/// <summary>사람이 읽는 요약 — 배선이 없으면 안내 문구</summary>
|
/// <summary>
|
||||||
|
/// 사람이 읽는 요약. <b>'배선' 이라는 낱말을 쓰지 않는다</b> —
|
||||||
|
/// 전산실에서 배선은 랜선·전원이고, '배선 없음' 은 데이터소스가 없는 것처럼 읽힌다.
|
||||||
|
/// 빈 상태 문구는 대화상자의 지우기 버튼과 <b>글자까지 같게</b> 둔다 —
|
||||||
|
/// 그 버튼을 누르면 무엇이 되는지 미리 보이게 하려는 것이다.
|
||||||
|
/// </summary>
|
||||||
public string Summary
|
public string Summary
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -968,19 +984,40 @@ public sealed class DataTableFieldRowViewModel : PropertyRowViewModel
|
|||||||
}
|
}
|
||||||
if (ValueText.Length == 0)
|
if (ValueText.Length == 0)
|
||||||
{
|
{
|
||||||
return "(배선 없음)";
|
return "(가져오지 않음)";
|
||||||
}
|
}
|
||||||
var spec = SheetMe.Core.Catalog.DataTableFieldSpec.Parse(ValueText);
|
var spec = SheetMe.Core.Catalog.DataTableFieldSpec.Parse(ValueText);
|
||||||
return spec is null
|
return spec is null
|
||||||
// 해석 못 하는 값은 원문을 그대로 보여 준다 — 조용히 고쳐 쓰지 않는다
|
// 해석 못 하는 값은 원문을 그대로 보여 준다 — 조용히 고쳐 쓰지 않는다
|
||||||
? $"⚠ 형식을 알 수 없음: {ValueText}"
|
? $"⚠ 형식을 알 수 없음: {ValueText}"
|
||||||
: $"{spec.TableName} · {spec.Field}" + (spec.RowIndex == 0 ? string.Empty : $" · {spec.RowIndex}행");
|
// 행 0 은 '첫 행' 이다. 그 밖은 사람이 세는 번호(0 → 1번째)로 적는다.
|
||||||
|
// 조건이 있으면 그 사실을 덧붙인다 — 조건이 화면 어디에도 안 보인 채
|
||||||
|
// 결과를 바꾸는 상태를 남기지 않는다.
|
||||||
|
: $"{spec.TableName} · {spec.Field}"
|
||||||
|
+ (spec.RowIndex == 0 ? string.Empty : $" · {spec.RowIndex + 1}번째 행")
|
||||||
|
+ (spec.Form == SheetMe.Core.Catalog.DataTableFieldForm.Select && spec.Filter.Length > 0
|
||||||
|
? " · 조건 있음" : string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>편집기 열기</summary>
|
/// <summary>편집기 열기</summary>
|
||||||
public M.Framework.WPF.ICustomCommand? EditCommand { get; set; }
|
public M.Framework.WPF.ICustomCommand? EditCommand { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대상 컨트롤 이름 — 창 제목에 넣는다. 모달이라 인스펙터가 가려져서,
|
||||||
|
/// 여러 칸을 연달아 채울 때 지금 어느 칸 차례인지 알 방법이 없었다.
|
||||||
|
/// </summary>
|
||||||
|
public Func<string>? OwnerLabelOf { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터소스 이름 → 그 조회 결과의 컬럼 이름(없으면 null).
|
||||||
|
///
|
||||||
|
/// 전에는 창이 '컬럼 후보는 목록으로 제시하지 않습니다' 라고 적어 두고 사용자를 밖으로 보냈는데,
|
||||||
|
/// 그 창은 모달이라 밖이 잠겨 있다 — 취소하고 쿼리를 열어 컬럼명을 외워 다시 와야 했다.
|
||||||
|
/// 실행은 이 행이 하지 않는다(호출부가 이미 만들어 둔 러너를 함수로 받는다).
|
||||||
|
/// </summary>
|
||||||
|
public Func<string, IReadOnlyList<string>?>? ColumnsOf { get; init; }
|
||||||
|
|
||||||
public DataTableFieldRowViewModel(string label, Func<IReadOnlyList<string>> tablesOf) : base(label)
|
public DataTableFieldRowViewModel(string label, Func<IReadOnlyList<string>> tablesOf) : base(label)
|
||||||
{
|
{
|
||||||
this.tablesOf = tablesOf;
|
this.tablesOf = tablesOf;
|
||||||
@@ -996,7 +1033,8 @@ public sealed class DataTableFieldRowViewModel : PropertyRowViewModel
|
|||||||
|
|
||||||
private void OnEdit()
|
private void OnEdit()
|
||||||
{
|
{
|
||||||
var dialog = new Views.DataTableFieldDialogView(ValueText, tablesOf())
|
var dialog = new Views.DataTableFieldDialogView(ValueText, tablesOf(),
|
||||||
|
OwnerLabelOf?.Invoke(), ColumnsOf)
|
||||||
{
|
{
|
||||||
Owner = System.Windows.Application.Current.MainWindow,
|
Owner = System.Windows.Application.Current.MainWindow,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ public sealed class SelectionOverlayViewModel : ViewModelBase
|
|||||||
private double selW;
|
private double selW;
|
||||||
private double selH;
|
private double selH;
|
||||||
private bool showHandles;
|
private bool showHandles;
|
||||||
|
private string dragBadgeText = string.Empty;
|
||||||
|
private bool showDragBadge;
|
||||||
|
private double dragBadgeX;
|
||||||
|
private double dragBadgeY;
|
||||||
private bool hasMarquee;
|
private bool hasMarquee;
|
||||||
private double marX;
|
private double marX;
|
||||||
private double marY;
|
private double marY;
|
||||||
@@ -189,6 +193,43 @@ public sealed class SelectionOverlayViewModel : ViewModelBase
|
|||||||
/// <summary>탭순서 배지 목록(탭순서 편집 모드 전용)</summary>
|
/// <summary>탭순서 배지 목록(탭순서 편집 모드 전용)</summary>
|
||||||
public ObservableCollection<TabBadgeInfo> TabBadges { get; } = new();
|
public ObservableCollection<TabBadgeInfo> TabBadges { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 끄는 동안 보여 주는 치수·좌표 — <b>드래그 중에만</b> 있다.
|
||||||
|
///
|
||||||
|
/// 전에는 숫자가 300px 떨어진 인스펙터에만 떴다. 라벨 폭을 종이 칸에 맞추려면
|
||||||
|
/// 끌고, 손 떼고, 오른쪽을 보고, 다시 끄는 왕복이 붙었다.
|
||||||
|
/// 간격 수치 칩은 <b>균등 스냅이 성립하는 순간에만</b> 나타나므로
|
||||||
|
/// "지금 이게 몇 px 인가"는 드래그 중에 알 방법이 없었다.
|
||||||
|
/// </summary>
|
||||||
|
public string DragBadgeText { get => dragBadgeText; private set => SetProperty(ref dragBadgeText, value); }
|
||||||
|
|
||||||
|
/// <summary>치수 칩을 보일지</summary>
|
||||||
|
public bool ShowDragBadge { get => showDragBadge; private set => SetProperty(ref showDragBadge, value); }
|
||||||
|
|
||||||
|
/// <summary>치수 칩 X(월드) — 선택 상자 좌하단 바깥</summary>
|
||||||
|
public double DragBadgeX { get => dragBadgeX; private set => SetProperty(ref dragBadgeX, value); }
|
||||||
|
|
||||||
|
/// <summary>치수 칩 Y(월드)</summary>
|
||||||
|
public double DragBadgeY { get => dragBadgeY; private set => SetProperty(ref dragBadgeY, value); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 끄는 동안의 치수·좌표를 갱신한다 — 이동이면 좌표, 크기 조절이면 크기를 보여 준다.
|
||||||
|
/// 사용자가 지금 <b>바꾸고 있는 값</b>을 보여 주는 것이 요점이다.
|
||||||
|
/// </summary>
|
||||||
|
public void SetDragBadge(Rect bounds, bool resizing)
|
||||||
|
{
|
||||||
|
DragBadgeText = resizing
|
||||||
|
? $"{Math.Round(bounds.Width)} × {Math.Round(bounds.Height)}"
|
||||||
|
: $"X {Math.Round(bounds.X)} · Y {Math.Round(bounds.Y)}";
|
||||||
|
// 선택 상자 <b>아래</b>에 둔다 — 위에 두면 위쪽으로 끌 때 커서가 칩을 밀고 다닌다
|
||||||
|
DragBadgeX = bounds.X;
|
||||||
|
DragBadgeY = bounds.Bottom + (6 * InverseZoom);
|
||||||
|
ShowDragBadge = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>드래그가 끝났다 — 칩을 치운다</summary>
|
||||||
|
public void ClearDragBadge() => ShowDragBadge = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 잠긴 컨트롤의 테두리 — <b>편집 크롬</b>이라 종이에는 그려지지 않는다.
|
/// 잠긴 컨트롤의 테두리 — <b>편집 크롬</b>이라 종이에는 그려지지 않는다.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,29 +1,58 @@
|
|||||||
<Window x:Class="SheetMe.Designer.Views.DataTableFieldDialogView"
|
<Window x:Class="SheetMe.Designer.Views.DataTableFieldDialogView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="데이터 필드 배선" Width="560" Height="520"
|
Title="값 가져오기" Width="620" Height="560"
|
||||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
|
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
|
||||||
Style="{StaticResource ThemedDialogWindow}">
|
Style="{StaticResource ThemedDialogWindow}">
|
||||||
|
<!--
|
||||||
|
이 창은 "조회 결과의 한 칸을 이 컨트롤에 채우는" 설정이다.
|
||||||
|
|
||||||
|
<b>왜 다시 썼나.</b> 전에는 네 칸(형태·필터·행 번호·필드)이 같은 굵기로 나란히 있어서
|
||||||
|
넷 다 채워야 하는 것처럼 보였다. 정작 <b>필수는 둘</b>(데이터소스·컬럼)이고
|
||||||
|
그 둘이 화면에서 가장 눈에 안 띄는 자리에 있었다.
|
||||||
|
게다가 선언 순서 때문에 첫 Tab 정지점이 '배선 지우기' 라,
|
||||||
|
화면이 "무엇을 먼저 하나"의 답을 '지우기' 로 주고 있었다.
|
||||||
|
|
||||||
|
실사용 근거: 조건(필터)을 쓴 배선이 <b>0건</b>이고 행 번호는 98%가 0 이다.
|
||||||
|
그래서 그 둘은 접어 두고, 기본값이 아닐 때만 칩으로 드러낸다.
|
||||||
|
-->
|
||||||
<DockPanel Margin="16">
|
<DockPanel Margin="16">
|
||||||
<TextBlock DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" Margin="0,0,0,10"
|
<TextBlock DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" Margin="0,0,0,12"
|
||||||
Text="데이터소스(조회 결과)의 한 칸을 이 컨트롤에 꽂습니다. 형식이 어긋나면 런타임이 아무 경고 없이 값을 비워 두므로, 문법은 여기서 조립합니다."/>
|
Text="서식이 열릴 때 데이터소스가 받아 온 조회 결과에서 한 칸을 읽어 이 칸을 자동으로 채웁니다. 고를 것은 둘입니다 — ① 어느 데이터소스에서, ② 그 결과의 어느 컬럼을."/>
|
||||||
|
|
||||||
<!-- 아래쪽 버튼 -->
|
<!-- 아래쪽 버튼. '가져오지 않음' 은 왼쪽으로 떼어 확인·취소와 섞이지 않게 한다 -->
|
||||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0" LastChildFill="False">
|
||||||
<Button Content="배선 지우기" Padding="12,4" Click="OnClear"/>
|
<Button DockPanel.Dock="Left" Content="가져오지 않음" Padding="12,4" Click="OnClear"
|
||||||
<Button x:Name="OkButton" Content="적용" Padding="18,4" Margin="8,0,0,0"
|
ToolTip="이 칸을 자동으로 채우지 않습니다 — 데이터소스나 쿼리는 지워지지 않습니다"/>
|
||||||
Style="{StaticResource Primary}" Click="OnApply" IsDefault="True"/>
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||||
<Button Content="취소" Padding="18,4" Margin="8,0,0,0" IsCancel="True"/>
|
<Button x:Name="OkButton" Content="확인" Padding="18,4"
|
||||||
</StackPanel>
|
Style="{StaticResource Primary}" Click="OnApply" IsDefault="True"/>
|
||||||
|
<Button Content="취소" Padding="18,4" Margin="8,0,0,0" IsCancel="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
<!-- 조립 결과 미리보기 + 검증 메시지 -->
|
<!-- 사람 말 한 줄 + 접힌 저장 형식 -->
|
||||||
<Border DockPanel.Dock="Bottom" Margin="0,12,0,0" Padding="10,8" CornerRadius="6"
|
<Border DockPanel.Dock="Bottom" Margin="0,12,0,0" Padding="10,8" CornerRadius="6"
|
||||||
Background="{DynamicResource B.Input}" BorderBrush="{DynamicResource B.InputBorder}" BorderThickness="1">
|
Background="{DynamicResource B.Input}" BorderBrush="{DynamicResource B.InputBorder}" BorderThickness="1">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="저장될 값" FontSize="10.5" Foreground="{DynamicResource B.Muted}"/>
|
<TextBlock x:Name="PlainText" TextWrapping="Wrap" FontSize="12.5"/>
|
||||||
<TextBlock x:Name="PreviewText" FontFamily="Consolas" FontSize="12" TextWrapping="Wrap" Margin="0,3,0,0"/>
|
|
||||||
<TextBlock x:Name="ProblemText" FontSize="11" TextWrapping="Wrap" Margin="0,5,0,0"
|
<TextBlock x:Name="ProblemText" FontSize="11" TextWrapping="Wrap" Margin="0,5,0,0"
|
||||||
Foreground="{DynamicResource B.Danger}" Visibility="Collapsed"/>
|
Foreground="{DynamicResource B.Danger}" Visibility="Collapsed"/>
|
||||||
|
<!--
|
||||||
|
저장 원문을 없애지 않고 접어 둔다. 인스펙터 툴팁·진단 덤프·레거시 문서가
|
||||||
|
전부 이 표기로 이야기되므로 원문을 들고 온 사람이 대조할 자리가 있어야 한다.
|
||||||
|
다만 큰 자리를 주면 창이 '문법 조립하는 곳' 으로 스스로를 재정의한다 —
|
||||||
|
아무것도 안 고른 상태의 첫 표시가 마침표로 시작하는 것도 그 탓이었다.
|
||||||
|
-->
|
||||||
|
<Expander x:Name="RawExpander" Margin="0,6,0,0" Header="저장 형식">
|
||||||
|
<DockPanel Margin="0,4,0,0">
|
||||||
|
<Button DockPanel.Dock="Right" Content="복사" Padding="8,2" VerticalAlignment="Top"
|
||||||
|
Margin="6,0,0,0" Click="OnCopyRaw"/>
|
||||||
|
<TextBlock x:Name="RawText" FontFamily="Consolas" FontSize="11.5" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource B.Muted}"
|
||||||
|
ToolTip="레거시 XML 에 이 글자 그대로 들어갑니다"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Expander>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -34,39 +63,67 @@
|
|||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- 데이터소스 -->
|
<!-- ① 데이터소스 -->
|
||||||
<DockPanel Grid.Column="0">
|
<DockPanel Grid.Column="0">
|
||||||
<TextBlock DockPanel.Dock="Top" Text="데이터소스" Margin="0,0,0,4"/>
|
<TextBlock DockPanel.Dock="Top" Text="① 가져올 데이터소스" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
<TextBlock x:Name="NoTableHint" DockPanel.Dock="Bottom" FontSize="11" TextWrapping="Wrap"
|
<TextBlock x:Name="NoTableHint" DockPanel.Dock="Bottom" FontSize="11" TextWrapping="Wrap"
|
||||||
Margin="0,6,0,0" Foreground="{DynamicResource B.Warning}" Visibility="Collapsed"
|
Margin="0,6,0,0" Foreground="{DynamicResource B.Warning}" Visibility="Collapsed"
|
||||||
Text="이 서식에 데이터소스가 없습니다. 팔레트에서 '데이터소스'를 먼저 놓고 쿼리를 지정하세요."/>
|
Text="이 서식에 데이터소스가 없습니다. 팔레트에서 '데이터소스'를 먼저 놓고 쿼리를 지정하세요."/>
|
||||||
<ListBox x:Name="TableList" SelectionChanged="OnFormChanged" FontSize="12.5"/>
|
<!-- 도움말은 라벨 바로 아래다. Bottom 에 두면 목록이 열을 채워
|
||||||
|
설명이 목록에서 한참 떨어진 빈 공간에 떠 있었다. -->
|
||||||
|
<TextBlock DockPanel.Dock="Top" FontSize="11" TextWrapping="Wrap" Margin="0,0,0,6"
|
||||||
|
Foreground="{DynamicResource B.Muted}"
|
||||||
|
Text="이 서식에 놓아 둔 조회 상자입니다 — 목록에서만 고릅니다."/>
|
||||||
|
<ListBox x:Name="TableList" SelectionChanged="OnTableChanged" FontSize="12.5"/>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<!-- 나머지 조각 -->
|
<!-- ② 컬럼 + ③ 접힌 행 고르기 -->
|
||||||
<StackPanel Grid.Column="2">
|
<DockPanel Grid.Column="2">
|
||||||
<TextBlock Text="형태" Margin="0,0,0,4"/>
|
<TextBlock DockPanel.Dock="Top" Text="② 가져올 컬럼" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
<ComboBox x:Name="FormBox" SelectionChanged="OnFormChanged"
|
<TextBox x:Name="FieldBox" DockPanel.Dock="Top" TextChanged="OnFormChanged"/>
|
||||||
ToolTip="Select 형은 레거시 편집기가 만드는 형태, Rows 형은 손으로 쓰던 형태입니다. 연 값의 형태를 그대로 유지합니다.">
|
<TextBlock x:Name="FieldHint" DockPanel.Dock="Top" FontSize="11" TextWrapping="Wrap" Margin="0,6,0,0"
|
||||||
<ComboBoxItem Content="Select — 필터로 행을 고름"/>
|
Foreground="{DynamicResource B.Muted}"/>
|
||||||
<ComboBoxItem Content="Rows — 행 번호로 직접"/>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<TextBlock x:Name="FilterLabel" Text="필터" Margin="0,10,0,4"/>
|
<!--
|
||||||
<TextBox x:Name="FilterBox" TextChanged="OnFormChanged"
|
③ 은 접어 둔다. 조건을 쓴 실사용 배선이 0건이고 행 번호는 98%가 0 이라,
|
||||||
ToolTip="DataTable.Select 문법 (예: CODCOD='A'). 비워 두면 전체 행입니다. 큰따옴표는 쓸 수 없습니다."/>
|
첫 방문자가 평생 안 쓸 개념을 먼저 보게 할 이유가 없다.
|
||||||
|
기본값이 아닐 때만 머리에 칩을 띄워 접혀 있어도 숨지 않게 한다.
|
||||||
|
-->
|
||||||
|
<Expander x:Name="RowExpander" DockPanel.Dock="Top" Margin="0,14,0,0">
|
||||||
|
<Expander.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="③ 여러 행 중에서 고르기"/>
|
||||||
|
<Border x:Name="RowChip" Margin="8,0,0,0" Padding="6,1" CornerRadius="3"
|
||||||
|
Background="{DynamicResource B.Chip}" Visibility="Collapsed">
|
||||||
|
<TextBlock x:Name="RowChipText" FontSize="10.5"
|
||||||
|
Foreground="{DynamicResource B.Muted}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Expander.Header>
|
||||||
|
<StackPanel Margin="0,8,0,0">
|
||||||
|
<TextBlock Text="몇 번째 행" Margin="0,0,0,4"/>
|
||||||
|
<TextBox x:Name="RowBox" TextChanged="OnFormChanged"/>
|
||||||
|
<TextBlock FontSize="11" TextWrapping="Wrap" Margin="0,4,0,0"
|
||||||
|
Foreground="{DynamicResource B.Muted}"
|
||||||
|
Text="첫 행이 0 입니다. 1 은 두 번째 행입니다. 반각 숫자만 됩니다."/>
|
||||||
|
|
||||||
<TextBlock Text="행 번호" Margin="0,10,0,4"/>
|
<!--
|
||||||
<TextBox x:Name="RowBox" TextChanged="OnFormChanged" ToolTip="0부터 셉니다"/>
|
조건은 저장된 값이 조건 형태일 때만 보인다. 새로 만들 때는 아예 안 보인다 —
|
||||||
|
실사용에서 조건을 쓴 배선이 0건이고, 이 문법은 SQL WHERE 가 아니라
|
||||||
<TextBlock Text="필드" Margin="0,10,0,4"/>
|
이미 받아 온 표를 거르는 식이라 NVL·TO_CHAR·|| 가 통하지 않는다.
|
||||||
<TextBox x:Name="FieldBox" TextChanged="OnFormChanged"
|
-->
|
||||||
ToolTip="조회 결과의 컬럼명. SQL 별칭 표현식도 됩니다 (예: A||'-'||B). 큰따옴표는 쓸 수 없습니다."/>
|
<StackPanel x:Name="FilterArea" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="행 조건" Margin="0,12,0,4"/>
|
||||||
<TextBlock x:Name="FieldHint" FontSize="11" TextWrapping="Wrap" Margin="0,6,0,0"
|
<TextBox x:Name="FilterBox" TextChanged="OnFormChanged"/>
|
||||||
Foreground="{DynamicResource B.Muted}"
|
<TextBlock FontSize="11" TextWrapping="Wrap" Margin="0,4,0,0"
|
||||||
Text="필드 후보는 쿼리를 실행해야 알 수 있어 목록으로 제시하지 않습니다. 데이터소스의 쿼리를 열어 컬럼명을 확인하세요."/>
|
Foreground="{DynamicResource B.Muted}"
|
||||||
</StackPanel>
|
Text="이 배선은 조건으로 행을 고르게 저장돼 있습니다. SQL 의 WHERE 가 아니라 받아 온 표를 거르는 식입니다 — 되는 예 CODCOD='A' · 안 되는 예 NVL, TO_CHAR, ||"/>
|
||||||
|
<Button x:Name="DropFilterButton" Content="조건을 버리고 순서대로 고르기"
|
||||||
|
Margin="0,8,0,0" Padding="10,3" Click="OnDropFilter"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Expander>
|
||||||
|
</DockPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -6,30 +6,74 @@ using SheetMe.Core.Catalog;
|
|||||||
namespace SheetMe.Designer.Views;
|
namespace SheetMe.Designer.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DataTableField 전용 편집기 — 문법 골격을 코드가 조립하고, 저장 전에 검사한다.
|
/// 조회 결과의 한 칸을 컨트롤에 채우는 설정 — 문법 골격을 코드가 조립하고 저장 전에 검사한다.
|
||||||
///
|
///
|
||||||
/// 레거시 편집기와 다르게 <b>기존 값을 되읽어 채운다</b>. 레거시는 열 때마다 백지에서 시작해
|
/// 레거시 편집기와 다르게 <b>기존 값을 되읽어 채운다</b>. 레거시는 열 때마다 백지에서 시작해
|
||||||
/// 한 조각만 고치려 해도 전부 다시 골라야 했다(fmMDataTableField.InitializeSelectedData).
|
/// 한 조각만 고치려 해도 전부 다시 골라야 했다(fmMDataTableField.InitializeSelectedData).
|
||||||
/// 형태(Select/Rows)도 연 값 그대로 유지한다 — 운영 다수는 Rows 형이고(1,624건, 76%),
|
///
|
||||||
/// 형태를 바꾸면 그 형태만 이해하는 보조 로더에서 동작이 달라질 수 있다.
|
/// <b>새 배선은 Rows 형이다.</b> 전에는 레거시 편집기와 같은 Select 형으로 시작했는데,
|
||||||
|
/// 그러면 첫 화면이 굳이 <b>조건을 요구하는 어려운 쪽</b>이 된다.
|
||||||
|
/// 동작은 같다 — 조건이 비면 <c>Select("")(0)</c> 과 <c>Rows(0)</c> 이 같은 행을 가리키고,
|
||||||
|
/// 해석기 자신이 그 등가를 코드로 인정한다(DataTableFieldPicker 는 조건이 비어 있으면
|
||||||
|
/// 필터 경로를 아예 타지 않는다). 실사용에서 조건을 쓴 배선은 <b>0건</b>이다.
|
||||||
|
///
|
||||||
|
/// <b>기존 값의 형태는 그대로 유지한다</b> — 저장된 원문을 우리가 바꾸지 않는 것이
|
||||||
|
/// XML 왕복 무손실의 조건이다. Select→Rows 전환은 조건을 버리는 일이라
|
||||||
|
/// 명시적 버튼으로만 하고, 버려지는 조건 원문을 확인 문구에 적어 보여 준다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class DataTableFieldDialogView : Window
|
public partial class DataTableFieldDialogView : Window
|
||||||
{
|
{
|
||||||
#region Member Fields
|
#region Member Fields
|
||||||
private readonly IReadOnlyList<string> tables;
|
private readonly IReadOnlyList<string> tables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터소스 이름 → 그 조회 결과의 컬럼 이름. 없으면 null(오프라인·미접속).
|
||||||
|
///
|
||||||
|
/// <b>여기가 이 창의 유일한 작업 포기 지점이었다.</b> 전에는 "필드 후보는 쿼리를 실행해야
|
||||||
|
/// 알 수 있어 목록으로 제시하지 않습니다" 라고 적어 두고 사용자를 창 밖으로 보냈는데,
|
||||||
|
/// 이 창은 모달이라 밖이 잠겨 있다 — 취소 → 데이터소스 찾기 → 쿼리 편집기 → 검증 실행 →
|
||||||
|
/// 컬럼명 외우기 → 다시 열기. 레거시 편집기는 이 목록을 이미 보여 줬으므로 그 문구는
|
||||||
|
/// 퇴행을 설명하는 문장이었다.
|
||||||
|
///
|
||||||
|
/// 실행 자체는 이 창이 하지 않는다 — 호출부가 이미 만들어 둔 러너를 함수로 감싸 넘긴다.
|
||||||
|
/// 그래야 접속·캐시·실패 처리가 한 군데(MDataTableRunner)에만 있다.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Func<string, IReadOnlyList<string>?>? columnsOf;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터소스별 컬럼 캐시 — 이 창이 사는 동안 <b>데이터소스마다 한 번만</b> 묻는다.
|
||||||
|
/// Refresh 는 키 입력마다 도는데 그때마다 조회하면 타이핑이 끊긴다.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, IReadOnlyList<string>?> columnCache = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
|
|
||||||
|
/// <summary>연 값의 형태 — 저장할 때 그대로 되돌려 쓴다(원문을 우리가 바꾸지 않는다)</summary>
|
||||||
|
private DataTableFieldForm form = DataTableFieldForm.Rows;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
/// <summary>적용 결과 — 취소면 null, '배선 지우기'면 빈 문자열</summary>
|
/// <summary>적용 결과 — 취소면 null, '가져오지 않음'이면 빈 문자열</summary>
|
||||||
public string? Result { get; private set; }
|
public string? Result { get; private set; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
public DataTableFieldDialogView(string? current, IReadOnlyList<string> dataTables)
|
/// <param name="current">지금 저장된 값 — 없으면 새 배선</param>
|
||||||
|
/// <param name="dataTables">이 서식의 데이터소스 이름</param>
|
||||||
|
/// <param name="ownerLabel">대상 컨트롤 이름 — 제목에 넣어 "지금 어느 칸 차례인지"를 알린다</param>
|
||||||
|
/// <param name="columnsOf">데이터소스의 컬럼 이름을 주는 함수(없으면 직접 입력만)</param>
|
||||||
|
public DataTableFieldDialogView(string? current, IReadOnlyList<string> dataTables,
|
||||||
|
string? ownerLabel = null, Func<string, IReadOnlyList<string>?>? columnsOf = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
tables = dataTables;
|
tables = dataTables;
|
||||||
|
this.columnsOf = columnsOf;
|
||||||
|
|
||||||
|
if (ownerLabel is { Length: > 0 })
|
||||||
|
{
|
||||||
|
// 모달이라 인스펙터가 가려진다 — 여러 칸을 연달아 채울 때 어느 칸 차례인지 알 수 없었다
|
||||||
|
Title = $"값 가져오기 — {ownerLabel}";
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var name in tables)
|
foreach (var name in tables)
|
||||||
{
|
{
|
||||||
@@ -40,16 +84,21 @@ public partial class DataTableFieldDialogView : Window
|
|||||||
var spec = DataTableFieldSpec.Parse(current);
|
var spec = DataTableFieldSpec.Parse(current);
|
||||||
if (spec is not null)
|
if (spec is not null)
|
||||||
{
|
{
|
||||||
FormBox.SelectedIndex = spec.Form == DataTableFieldForm.Rows ? 1 : 0;
|
form = spec.Form;
|
||||||
TableList.SelectedItem = tables.FirstOrDefault(t => string.Equals(t, spec.TableName, StringComparison.Ordinal));
|
TableList.SelectedItem = tables.FirstOrDefault(t => string.Equals(t, spec.TableName, StringComparison.Ordinal));
|
||||||
FilterBox.Text = spec.Filter;
|
FilterBox.Text = spec.Filter;
|
||||||
RowBox.Text = spec.RowIndex.ToString(CultureInfo.InvariantCulture);
|
RowBox.Text = spec.RowIndex.ToString(CultureInfo.InvariantCulture);
|
||||||
FieldBox.Text = spec.Field;
|
FieldBox.Text = spec.Field;
|
||||||
|
// 기본값이 아닌 것이 접혀 있으면 숨는다 — 그때만 펼친 채로 연다
|
||||||
|
if (spec.RowIndex != 0 || (form == DataTableFieldForm.Select && spec.Filter.Length > 0))
|
||||||
|
{
|
||||||
|
RowExpander.IsExpanded = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 새 배선 — 레거시 편집기와 같은 Select 형으로 시작한다
|
// 새 배선 — 조건 없이 첫 행. 첫 화면에 평생 안 쓸 개념을 두지 않는다.
|
||||||
FormBox.SelectedIndex = 0;
|
form = DataTableFieldForm.Rows;
|
||||||
RowBox.Text = "0";
|
RowBox.Text = "0";
|
||||||
if (tables.Count == 1)
|
if (tables.Count == 1)
|
||||||
{
|
{
|
||||||
@@ -59,47 +108,134 @@ public partial class DataTableFieldDialogView : Window
|
|||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
Refresh();
|
Refresh();
|
||||||
|
|
||||||
|
// 첫 포커스 — 데이터소스가 이미 정해졌으면 다음 할 일(컬럼)로 보낸다.
|
||||||
|
// 이 호출이 없으면 선언 순서상 첫 Tab 정지점이 파괴적 버튼이었다.
|
||||||
|
Loaded += (_, _) =>
|
||||||
|
{
|
||||||
|
if (TableList.SelectedItem is null && TableList.Items.Count > 0)
|
||||||
|
{
|
||||||
|
TableList.Focus();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FieldBox.Focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Methods
|
#region Methods
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 현재 입력을 명세로 옮긴다 — 행 번호는 숫자가 아니면 0 으로 본다
|
/// 현재 입력을 명세로 옮긴다.
|
||||||
/// (레거시도 빈 값을 0 으로 강제한다: fmMDataTableField.vb:135).
|
///
|
||||||
|
/// <b>행 번호를 조용히 0 으로 바꾸지 않는다.</b> 전에는 파싱 실패가 그대로 0 이 됐는데,
|
||||||
|
/// 그러면 전각 '1' 을 넣은 배선이 아무 말 없이 첫 행을 가리킨다(실사용에 그런 값이 실재한다).
|
||||||
|
/// 잘못된 입력은 <see cref="Refresh"/> 가 이유를 말하고 확인을 막는다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private DataTableFieldSpec CurrentSpec()
|
private DataTableFieldSpec CurrentSpec() => new()
|
||||||
{
|
{
|
||||||
var row = int.TryParse(RowBox.Text.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
Form = form,
|
||||||
? parsed
|
TableName = TableList.SelectedItem as string ?? string.Empty,
|
||||||
: 0;
|
Filter = form == DataTableFieldForm.Select ? FilterBox.Text : string.Empty,
|
||||||
return new DataTableFieldSpec
|
RowIndex = ParsedRow() ?? 0,
|
||||||
|
Field = FieldBox.Text.Trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>행 번호 — 반각 숫자로 읽히지 않으면 null</summary>
|
||||||
|
private int? ParsedRow()
|
||||||
|
{
|
||||||
|
var text = RowBox.Text.Trim();
|
||||||
|
if (text.Length == 0)
|
||||||
{
|
{
|
||||||
Form = FormBox.SelectedIndex == 1 ? DataTableFieldForm.Rows : DataTableFieldForm.Select,
|
return 0; // 빈 값은 첫 행이다(레거시도 빈 값을 0 으로 본다)
|
||||||
TableName = TableList.SelectedItem as string ?? string.Empty,
|
}
|
||||||
Filter = FilterBox.Text,
|
return int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out var parsed) ? parsed : null;
|
||||||
RowIndex = row,
|
|
||||||
Field = FieldBox.Text.Trim(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Refresh()
|
private void Refresh()
|
||||||
{
|
{
|
||||||
|
// 조건 칸은 저장된 값이 조건 형태일 때만 보인다
|
||||||
|
FilterArea.Visibility = form == DataTableFieldForm.Select ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
var spec = CurrentSpec();
|
var spec = CurrentSpec();
|
||||||
var rowsForm = spec.Form == DataTableFieldForm.Rows;
|
RawText.Text = spec.Compose();
|
||||||
|
UpdateRowChip(spec);
|
||||||
|
UpdateFieldHint(spec.TableName);
|
||||||
|
|
||||||
// Rows 형은 필터를 아예 쓰지 않는다 — 켜 두면 입력해도 무시되는 칸이 된다
|
// 전각 숫자처럼 읽을 수 없는 행 번호는 <b>먼저</b> 말한다 — 조용히 0 으로 바꾸지 않는다
|
||||||
FilterBox.IsEnabled = !rowsForm;
|
var problem = ParsedRow() is null
|
||||||
FilterLabel.Opacity = rowsForm ? 0.45 : 1;
|
? "몇 번째 행: 반각 숫자만 됩니다"
|
||||||
FilterBox.Opacity = rowsForm ? 0.45 : 1;
|
// 고를 데이터소스가 아예 없으면 '고르세요' 는 따를 수 없는 지시다 — 다음 행동을 말한다
|
||||||
|
: tables.Count == 0
|
||||||
|
? "이 서식에 데이터소스가 없습니다 — 팔레트에서 먼저 놓고 쿼리를 지정하세요"
|
||||||
|
: spec.Validate(tables);
|
||||||
|
|
||||||
PreviewText.Text = spec.Compose();
|
PlainText.Text = problem is null ? PlainSentence(spec) : "아직 가져올 값이 정해지지 않았습니다.";
|
||||||
var problem = spec.Validate(tables);
|
|
||||||
ProblemText.Text = problem ?? string.Empty;
|
ProblemText.Text = problem ?? string.Empty;
|
||||||
ProblemText.Visibility = problem is null ? Visibility.Collapsed : Visibility.Visible;
|
ProblemText.Visibility = problem is null ? Visibility.Collapsed : Visibility.Visible;
|
||||||
// 레거시는 아무것도 안 고르고 확인하면 ")(" 같은 찌꺼기를 저장한다 — 그 경로를 막는다
|
// 레거시는 아무것도 안 고르고 확인하면 ")(" 같은 찌꺼기를 저장한다 — 그 경로를 막는다
|
||||||
OkButton.IsEnabled = problem is null;
|
OkButton.IsEnabled = problem is null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>사람 말 한 줄 — 이 창이 무엇을 하기로 했는지</summary>
|
||||||
|
private static string PlainSentence(DataTableFieldSpec spec)
|
||||||
|
{
|
||||||
|
var where = spec.RowIndex == 0 ? "첫 행" : $"{spec.RowIndex + 1}번째 행";
|
||||||
|
var scope = spec.Form == DataTableFieldForm.Select && spec.Filter.Length > 0
|
||||||
|
? "조건을 통과한 행 중 " + where
|
||||||
|
: where;
|
||||||
|
return $"서식을 열면 — {spec.TableName} 조회 결과의 {scope}에서 "
|
||||||
|
+ $"{spec.Field} 값을 가져와 이 칸에 넣습니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>접힌 ③ 의 머리 칩 — 기본값이 아니면 접혀 있어도 드러낸다</summary>
|
||||||
|
private void UpdateRowChip(DataTableFieldSpec spec)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
if (spec.RowIndex != 0)
|
||||||
|
{
|
||||||
|
parts.Add($"{spec.RowIndex + 1}번째 행");
|
||||||
|
}
|
||||||
|
if (spec.Form == DataTableFieldForm.Select && spec.Filter.Length > 0)
|
||||||
|
{
|
||||||
|
parts.Add("조건 있음");
|
||||||
|
}
|
||||||
|
RowChipText.Text = string.Join(" · ", parts);
|
||||||
|
RowChip.Visibility = parts.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 컬럼 칸 아래 안내 — 실제 컬럼 목록을 얻을 수 있으면 그것을 보여 준다.
|
||||||
|
/// 못 얻으면 <b>왜 못 얻는지</b>를 말한다(전에는 "목록으로 제시하지 않습니다"로 끝났다).
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateFieldHint(string tableName)
|
||||||
|
{
|
||||||
|
if (tableName.Length == 0)
|
||||||
|
{
|
||||||
|
FieldHint.Text = "데이터소스를 먼저 고르면 그 조회 결과의 컬럼 이름을 보여 줍니다.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!columnCache.TryGetValue(tableName, out var columns))
|
||||||
|
{
|
||||||
|
columns = columnsOf?.Invoke(tableName);
|
||||||
|
columnCache[tableName] = columns;
|
||||||
|
}
|
||||||
|
if (columns is { Count: > 0 })
|
||||||
|
{
|
||||||
|
FieldHint.Text = $"{tableName} 의 컬럼 {columns.Count}개 — "
|
||||||
|
+ string.Join(", ", columns.Take(12))
|
||||||
|
+ (columns.Count > 12 ? " …" : string.Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 못 읽었을 때도 <b>왜</b>인지 말한다. 전에는 "목록으로 제시하지 않습니다" 로 끝나
|
||||||
|
// 사용자가 무엇을 하면 되는지 알 수 없었다. 문구는 짧게 — 여기가 도움말 자리다.
|
||||||
|
FieldHint.Text = "컬럼 목록을 읽지 못했습니다(DB 미접속·쿼리 실패) — 직접 입력하세요."
|
||||||
|
+ " 조회 결과 표의 머리글 이름이고 대개 전부 대문자입니다(예: ALB, ACPDTE).";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTableChanged(object sender, SelectionChangedEventArgs e) => OnFormChanged(sender, e);
|
||||||
|
|
||||||
private void OnFormChanged(object sender, RoutedEventArgs e)
|
private void OnFormChanged(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!loading)
|
if (!loading)
|
||||||
@@ -108,8 +244,42 @@ public partial class DataTableFieldDialogView : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 조건을 버리고 순서대로 고르기 — <b>되돌릴 수 없어</b> 버려지는 조건을 먼저 보여 준다.
|
||||||
|
///
|
||||||
|
/// 전에는 형태 콤보를 바꾸는 것만으로 조건이 조립에서 조용히 사라졌고(Rows 조립은 Filter 를
|
||||||
|
/// 아예 쓰지 않는다) 다시 열면 Rows 해석에 조건 항목이 없어 영구히 없어졌다.
|
||||||
|
/// 화면은 칸을 흐리게만 해서 값이 살아 있는 것처럼 보였다.
|
||||||
|
/// </summary>
|
||||||
|
private void OnDropFilter(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var filter = FilterBox.Text;
|
||||||
|
if (filter.Length > 0 && !Services.DialogService.Confirm("조건 버리기",
|
||||||
|
"이 배선의 행 조건을 지우고 순서대로 고르게 바꿉니다.",
|
||||||
|
$"지워지는 조건: {filter}\n확인을 누르면 되돌릴 수 없습니다.",
|
||||||
|
destructive: true, owner: this))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form = DataTableFieldForm.Rows;
|
||||||
|
FilterBox.Text = string.Empty;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCopyRaw(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (RawText.Text is { Length: > 0 } raw)
|
||||||
|
{
|
||||||
|
Clipboard.SetText(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnApply(object sender, RoutedEventArgs e)
|
private void OnApply(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (ParsedRow() is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
var spec = CurrentSpec();
|
var spec = CurrentSpec();
|
||||||
if (spec.Validate(tables) is not null)
|
if (spec.Validate(tables) is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -175,7 +175,8 @@
|
|||||||
(자르기보다 낫다 — 잘린 라벨은 툴팁을 띄워봐야 아는 정보가 된다).
|
(자르기보다 낫다 — 잘린 라벨은 툴팁을 띄워봐야 아는 정보가 된다).
|
||||||
-->
|
-->
|
||||||
<Style x:Key="RowLabel" TargetType="TextBlock">
|
<Style x:Key="RowLabel" TargetType="TextBlock">
|
||||||
<Setter Property="Width" Value="72"/>
|
<!-- 위 주석이 92 를 논증해 놓고 값은 72 였다 — 그래서 '액션 대상 컨…' 처럼 잘렸다 -->
|
||||||
|
<Setter Property="Width" Value="92"/>
|
||||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
||||||
<Setter Property="FontSize" Value="12"/>
|
<Setter Property="FontSize" Value="12"/>
|
||||||
@@ -694,8 +695,15 @@
|
|||||||
<DataTemplate DataType="{x:Type ins:ReadOnlyRowViewModel}">
|
<DataTemplate DataType="{x:Type ins:ReadOnlyRowViewModel}">
|
||||||
<DockPanel Margin="0,2">
|
<DockPanel Margin="0,2">
|
||||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||||
|
<!--
|
||||||
|
이 자리는 값이 아니라 <b>설명</b>이다 — 자르면 결론이 사라진다.
|
||||||
|
"타입이 달라 값의 뜻이 다른 속성 2개 — 타입별로 따로 고르세요" 에서
|
||||||
|
지시에 해당하는 뒷부분이 잘려 나가 무엇을 하라는 건지 알 수 없었다.
|
||||||
|
접어서 다 보여 주고, 그래도 좁으면 툴팁이 받는다.
|
||||||
|
-->
|
||||||
<TextBlock Text="{Binding ValueText}" FontSize="11" Foreground="{DynamicResource B.Muted}"
|
<TextBlock Text="{Binding ValueText}" FontSize="11" Foreground="{DynamicResource B.Muted}"
|
||||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
VerticalAlignment="Center" TextWrapping="Wrap"
|
||||||
|
ToolTip="{Binding ValueText}"/>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
@@ -906,11 +914,11 @@
|
|||||||
<ContentControl DockPanel.Dock="Left" Margin="0,0,7,0" VerticalAlignment="Center"
|
<ContentControl DockPanel.Dock="Left" Margin="0,0,7,0" VerticalAlignment="Center"
|
||||||
IsTabStop="False" Focusable="False"
|
IsTabStop="False" Focusable="False"
|
||||||
Content="{Binding HeaderIcon, Converter={StaticResource IconName}, ConverterParameter=16|B.Muted}"/>
|
Content="{Binding HeaderIcon, Converter={StaticResource IconName}, ConverterParameter=16|B.Muted}"/>
|
||||||
<Button DockPanel.Dock="Right" Width="24" Height="24" Padding="0"
|
<!--
|
||||||
Style="{StaticResource Subtle}" ToolTip="이 컨트롤에 대한 명령"
|
⋮ 버튼을 지웠다. Command·Click·ContextMenu 가 <b>전부 없어</b> 눌러도 아무 일이 없었는데,
|
||||||
AutomationProperties.Name="더보기"
|
툴팁은 '이 컨트롤에 대한 명령' 이라 '단일 선택일 때만 되는 뭔가가 있다'고 오해시켰다.
|
||||||
Visibility="{Binding IsSingleSelection, Converter={StaticResource BoolToVisibility}}"
|
없는 버튼보다 죽은 버튼이 나쁘다. 컨트롤 단위 명령은 캔버스 우클릭 메뉴에 다 있다.
|
||||||
Content="{Binding Source=ellipsis-vertical, Converter={StaticResource IconName}, ConverterParameter=14|B.Muted}"/>
|
-->
|
||||||
<TextBlock Text="{Binding HeaderName}" FontWeight="Bold" FontSize="12.5"
|
<TextBlock Text="{Binding HeaderName}" FontWeight="Bold" FontSize="12.5"
|
||||||
Foreground="{DynamicResource B.Ink}" VerticalAlignment="Center"
|
Foreground="{DynamicResource B.Ink}" VerticalAlignment="Center"
|
||||||
TextTrimming="CharacterEllipsis" ToolTip="{Binding Summary}"/>
|
TextTrimming="CharacterEllipsis" ToolTip="{Binding Summary}"/>
|
||||||
|
|||||||
@@ -349,10 +349,16 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</TextBlock.Style>
|
</TextBlock.Style>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock Text="{Binding LayerId}" FontSize="10.5" Margin="0,1,0,0"
|
<!--
|
||||||
|
둘째 줄은 <b>항상</b> 있다 — 없으면 목록에 1줄 행과 2줄 행이 섞여
|
||||||
|
훑을 때 눈이 매번 자리를 다시 잡아야 한다.
|
||||||
|
타입명을 앞에 두는 이유: 아이콘만으로 안 갈리는 짝이 실제로 있다
|
||||||
|
(라벨·마스크입력이 같은 글리프, 리스트박스·체크리스트도 같다).
|
||||||
|
이름(Id)은 표시명과 다를 때만 뒤에 붙는다 — 외부 참조 키라 계속 보여야 한다.
|
||||||
|
-->
|
||||||
|
<TextBlock Text="{Binding LayerSubText}" FontSize="10.5" Margin="0,1,0,0"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
ToolTip="컨트롤 이름 — 다른 속성이 이 이름으로 참조합니다"
|
ToolTip="{Binding LayerSubText}">
|
||||||
Visibility="{Binding HasLayerId, Converter={StaticResource BoolToVisibility}}">
|
|
||||||
<TextBlock.Style>
|
<TextBlock.Style>
|
||||||
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
|
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
||||||
|
|||||||
@@ -1,55 +1,83 @@
|
|||||||
<Window x:Class="SheetMe.Designer.Views.PreviewWindow"
|
<Window x:Class="SheetMe.Designer.Views.PreviewWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:ctl="clr-namespace:SheetMe.Designer.Controls"
|
||||||
Title="미리보기" Width="900" Height="1000"
|
Title="미리보기" Width="900" Height="1000"
|
||||||
WindowStartupLocation="CenterOwner" Background="{DynamicResource B.CanvasBg}"
|
WindowStartupLocation="CenterOwner" Background="{DynamicResource B.CanvasBg}"
|
||||||
Style="{StaticResource ThemedDialogWindow}">
|
Style="{StaticResource ThemedDialogWindow}">
|
||||||
|
<Window.Resources>
|
||||||
|
<ctl:IconNameToVisualConverter x:Key="IconName"/>
|
||||||
|
</Window.Resources>
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<ToolBarTray DockPanel.Dock="Top">
|
<ToolBarTray DockPanel.Dock="Top">
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<Button Content="인쇄..." Padding="10,3" Click="OnPrint"/>
|
<!--
|
||||||
<Button Content="새로 고침" Padding="10,3" Click="OnRefresh"
|
아이콘 버튼으로 둔다. 전에는 여덟 개가 전부 글자라 툴바가 글자 벽이었고,
|
||||||
ToolTip="구조 변경은 자동으로 따라오고, 창을 누르면 다시 그립니다"/>
|
무엇이 명령이고 무엇이 상태인지 구분되지 않았다.
|
||||||
|
글자로 남기는 것은 <b>값과 상태</b>뿐이다 — 고른 환자 이름과 배율.
|
||||||
|
이름은 툴팁이 받는다(셸 제목줄의 아이콘 줄과 같은 규범).
|
||||||
|
-->
|
||||||
|
<Button Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0"
|
||||||
|
Click="OnPrint" ToolTip="인쇄..."
|
||||||
|
Content="{Binding Source=printer, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||||
|
<Button Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0"
|
||||||
|
Click="OnRefresh"
|
||||||
|
ToolTip="다시 그리기 — 구조 변경은 자동으로 따라오고, 창을 누르면 다시 그립니다"
|
||||||
|
Content="{Binding Source=rotate-cw, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<!--
|
<!--
|
||||||
레거시 미리보기와 레거시 인쇄가 서로 다르다 — 미리보기는 PrintOutPut=False 를
|
레거시 미리보기와 레거시 인쇄가 서로 다르다 — 미리보기는 PrintOutPut=False 를
|
||||||
그대로 보여 주고 인쇄는 뺀다. 둘 다 확인할 수 있어야 해서 보기를 가른다.
|
그대로 보여 주고 인쇄는 뺀다. 둘 다 확인할 수 있어야 해서 보기를 가른다.
|
||||||
기본은 인쇄 기준 — 저장 전에 확인하려는 것이 대개 종이 결과다.
|
기본은 인쇄 기준 — 저장 전에 확인하려는 것이 대개 종이 결과다.
|
||||||
|
토글은 눌린 상태가 곧 '인쇄 기준'이라는 뜻이다(툴팁이 양쪽을 설명한다).
|
||||||
-->
|
-->
|
||||||
<CheckBox x:Name="PrintViewToggle" Content="인쇄될 것만" IsChecked="True"
|
<ToggleButton x:Name="PrintViewToggle" IsChecked="True"
|
||||||
VerticalAlignment="Center" Margin="4,0"
|
Style="{StaticResource MiniToggle}" Width="34" Height="28" Padding="0"
|
||||||
Checked="OnViewToggled" Unchecked="OnViewToggled"
|
Checked="OnViewToggled" Unchecked="OnViewToggled"
|
||||||
ToolTip="끄면 화면에 보이는 대로 — 인쇄 출력을 끈 컨트롤도 보입니다(레거시 미리보기와 같음)"/>
|
ToolTip="인쇄될 것만 보기 — 끄면 화면에 보이는 대로(인쇄 출력을 끈 컨트롤도 보입니다)"
|
||||||
|
Content="{Binding Source=eye, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<!--
|
<!--
|
||||||
실제 환자로 미리보기 — 태그가 값을 뽑는지 확인하는 유일한 방법이다.
|
실제 환자로 미리보기 — 태그가 값을 뽑는지 확인하는 유일한 방법이다.
|
||||||
고르지 않은 상태가 기본이다. 여는 순간 환자를 붙이면 미리보기를 열 때마다
|
고르지 않은 상태가 기본이다. 여는 순간 환자를 붙이면 미리보기를 열 때마다
|
||||||
환자 정보를 조회하게 되고, 그건 이 도구가 할 일이 아니다.
|
환자 정보를 조회하게 되고, 그건 이 도구가 할 일이 아니다.
|
||||||
|
|
||||||
|
선택·해제를 <b>한 버튼</b>으로 합쳤다 — 셸 제목줄의 환자 아이콘과 같은 방식이다.
|
||||||
|
안 골랐으면 바로 선택 창, 이미 골랐으면 바꾸기·해제 메뉴를 띄운다.
|
||||||
|
늘 보이는 자리에서 '누르면 해제'로 두면 실수로 지운다.
|
||||||
-->
|
-->
|
||||||
<Button x:Name="PatientButton" Content="환자 선택..." Padding="10,3" Click="OnPickPatient"
|
<Button x:Name="PatientButton" Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0"
|
||||||
ToolTip="실제 환자·내원을 골라 태그에 값이 들어간 상태로 확인합니다"/>
|
Click="OnPickPatient"
|
||||||
<Button x:Name="PatientClearButton" Content="환자 해제" Padding="10,3" Click="OnClearPatient"
|
ToolTip="환자 선택 — 실제 환자·내원을 골라 태그에 값이 들어간 상태로 확인합니다"/>
|
||||||
IsEnabled="False" ToolTip="환자 없이 — 태그는 사유만 보입니다"/>
|
|
||||||
<!--
|
<!--
|
||||||
데이터소스 결과 — 종이에 값이 안 찍혔을 때 왜인지는 여기서만 알 수 있다.
|
데이터소스 결과 — 종이에 값이 안 찍혔을 때 왜인지는 여기서만 알 수 있다.
|
||||||
"[MDataTable1.ALGYON — 조회 결과가 0행입니다]" 한 줄로는 쿼리가 틀렸는지,
|
"[MDataTable1.ALGYON — 조회 결과가 0행입니다]" 한 줄로는 쿼리가 틀렸는지,
|
||||||
치환이 빈 값이 됐는지, 이 환자에게 정말 자료가 없는지 가릴 수 없다.
|
치환이 빈 값이 됐는지, 이 환자에게 정말 자료가 없는지 가릴 수 없다.
|
||||||
-->
|
-->
|
||||||
<Button x:Name="DataSourceButton" Content="데이터소스..." Padding="10,3" Click="OnShowDataSources"
|
<Button x:Name="DataSourceButton" Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0"
|
||||||
IsEnabled="False" ToolTip="MDataTable 조회 결과와 치환을 마친 SQL 을 봅니다"/>
|
Click="OnShowDataSources" IsEnabled="False"
|
||||||
<TextBlock x:Name="PatientText" VerticalAlignment="Center" Margin="8,0"
|
ToolTip="데이터소스 결과 — MDataTable 조회 결과와 치환을 마친 SQL 을 봅니다"
|
||||||
MaxWidth="360" TextTrimming="CharacterEllipsis"
|
Content="{Binding Source=table, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||||
Foreground="{DynamicResource B.Muted}" Text="환자 없음"/>
|
|
||||||
<!--
|
<!--
|
||||||
미리보기에서 눌러 본 체크·라디오를 원래대로. 문서는 처음부터 안 건드리므로
|
미리보기에서 눌러 본 체크·라디오를 원래대로. 문서는 처음부터 안 건드리므로
|
||||||
되돌릴 것이 없고, 여기서 지우는 것은 "눌러 본 흔적"뿐이다.
|
되돌릴 것이 없고, 여기서 지우는 것은 "눌러 본 흔적"뿐이다.
|
||||||
-->
|
-->
|
||||||
<Button x:Name="ResetChecksButton" Content="누른 것 초기화" Padding="10,3" Click="OnResetChecks"
|
<Button x:Name="ResetChecksButton" Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0"
|
||||||
IsEnabled="False" ToolTip="미리보기에서 눌러 본 체크·라디오를 서식의 기본 상태로 돌립니다"/>
|
Click="OnResetChecks" IsEnabled="False"
|
||||||
|
ToolTip="누른 것 초기화 — 미리보기에서 눌러 본 체크·라디오를 서식의 기본 상태로 돌립니다"
|
||||||
|
Content="{Binding Source=undo-2, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||||
|
<!-- 고른 환자는 <b>글자로</b> 남긴다 — 이름은 아이콘으로 대신할 수 없는 값이다 -->
|
||||||
|
<TextBlock x:Name="PatientText" VerticalAlignment="Center" Margin="8,0"
|
||||||
|
MaxWidth="360" TextTrimming="CharacterEllipsis"
|
||||||
|
Foreground="{DynamicResource B.Muted}" Text="환자 없음"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<Button Content="-" Padding="8,3" Click="OnZoomOut"/>
|
<Button Style="{StaticResource Subtle}" Width="30" Height="28" Padding="0"
|
||||||
|
Click="OnZoomOut" ToolTip="축소"
|
||||||
|
Content="{Binding Source=minus, Converter={StaticResource IconName}, ConverterParameter=14}"/>
|
||||||
<TextBlock x:Name="ZoomText" Text="100%" VerticalAlignment="Center" Margin="6,0" MinWidth="42" TextAlignment="Center"/>
|
<TextBlock x:Name="ZoomText" Text="100%" VerticalAlignment="Center" Margin="6,0" MinWidth="42" TextAlignment="Center"/>
|
||||||
<Button Content="+" Padding="8,3" Click="OnZoomIn"/>
|
<Button Style="{StaticResource Subtle}" Width="30" Height="28" Padding="0"
|
||||||
|
Click="OnZoomIn" ToolTip="확대"
|
||||||
|
Content="{Binding Source=plus, Converter={StaticResource IconName}, ConverterParameter=14}"/>
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
</ToolBarTray>
|
</ToolBarTray>
|
||||||
|
|
||||||
|
|||||||
@@ -303,21 +303,42 @@ public partial class PreviewWindow : Window
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnPickPatient(object sender, RoutedEventArgs e)
|
private void OnPickPatient(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var picked = PatientPickerDialogView.Pick(this, out var label);
|
// 안 골랐으면 바로 선택 창. 이미 골랐으면 <b>메뉴</b>다 —
|
||||||
if (picked is null)
|
// 늘 보이는 아이콘에서 '누르면 해제'로 두면 실수로 지운다.
|
||||||
|
// 셸 제목줄의 환자 아이콘과 같은 규범이다(버튼 두 개를 하나로 합쳤다).
|
||||||
|
if (!PatientSession.HasPatient)
|
||||||
{
|
{
|
||||||
|
Pick();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 세션에만 쓴다 — 그러면 Changed 가 이 창을 포함한 <b>모든</b> 화면을 갱신한다.
|
var menu = new System.Windows.Controls.ContextMenu
|
||||||
// 여기서 직접 UsePatient 도 부르면 두 번 그리고, 더 나쁘게는
|
{
|
||||||
// 세션과 창이 어긋나는 경로가 하나 더 생긴다.
|
PlacementTarget = PatientButton,
|
||||||
PatientSession.Set(picked, label);
|
Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom,
|
||||||
|
};
|
||||||
|
var header = new System.Windows.Controls.MenuItem { Header = PatientSession.Label, IsEnabled = false };
|
||||||
|
var change = new System.Windows.Controls.MenuItem { Header = "환자 바꾸기..." };
|
||||||
|
change.Click += (_, _) => Pick();
|
||||||
|
var clear = new System.Windows.Controls.MenuItem { Header = "환자 해제" };
|
||||||
|
clear.Click += (_, _) => PatientSession.Set(null, string.Empty);
|
||||||
|
menu.Items.Add(header);
|
||||||
|
menu.Items.Add(new System.Windows.Controls.Separator());
|
||||||
|
menu.Items.Add(change);
|
||||||
|
menu.Items.Add(clear);
|
||||||
|
menu.IsOpen = true;
|
||||||
|
|
||||||
|
void Pick()
|
||||||
|
{
|
||||||
|
// 권한 문·감사 기록·문맥 읽기는 전부 Pick 안에 있다 — 호출부마다 다시 확인하면 어긋난다.
|
||||||
|
// 세션에만 쓴다 — 그러면 Changed 가 이 창을 포함한 <b>모든</b> 화면을 갱신한다.
|
||||||
|
if (PatientPickerDialogView.Pick(this, out var label) is { } picked)
|
||||||
|
{
|
||||||
|
PatientSession.Set(picked, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>환자를 뗀다 — 종이에서 값이 사라지고 사유로 돌아가는 것까지 확인해야 한다</summary>
|
/// <summary>환자를 뗀다 — 종이에서 값이 사라지고 사유로 돌아가는 것까지 확인해야 한다</summary>
|
||||||
private void OnClearPatient(object sender, RoutedEventArgs e)
|
|
||||||
=> PatientSession.Set(null, string.Empty);
|
|
||||||
|
|
||||||
private void OnPatientSessionChanged(object? sender, EventArgs e)
|
private void OnPatientSessionChanged(object? sender, EventArgs e)
|
||||||
=> UsePatient(PatientSession.Current, PatientSession.Label);
|
=> UsePatient(PatientSession.Current, PatientSession.Label);
|
||||||
|
|
||||||
@@ -359,7 +380,11 @@ public partial class PreviewWindow : Window
|
|||||||
fields = new MDataTableRunner(designer.Document,
|
fields = new MDataTableRunner(designer.Document,
|
||||||
picked is null ? null : new PatientQueryVariableSource(picked, UserSession.Current.UidCod));
|
picked is null ? null : new PatientQueryVariableSource(picked, UserSession.Current.UidCod));
|
||||||
PatientText.Text = picked is null ? "환자 없음" : label;
|
PatientText.Text = picked is null ? "환자 없음" : label;
|
||||||
PatientClearButton.IsEnabled = picked is not null;
|
// 고른 상태는 <b>아이콘 색</b>으로 알린다 — 셸 제목줄과 같다.
|
||||||
|
// 안 고른 상태의 색은 B.Muted 여야 한다(null 을 넘기면 하드코딩 검정으로 칠해져
|
||||||
|
// 어두운 툴바에서 혼자 묻힌다 — 제목줄에서 실제로 겪은 결함이다).
|
||||||
|
PatientButton.Content = Controls.LucideIcons.Icon("user", 16,
|
||||||
|
picked is null ? "B.Muted" : "B.Accent");
|
||||||
DataSourceButton.IsEnabled = fields.Names.Count > 0;
|
DataSourceButton.IsEnabled = fields.Names.Count > 0;
|
||||||
BuildPages();
|
BuildPages();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,9 +323,33 @@
|
|||||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
|
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
|
||||||
<Button DockPanel.Dock="Right" Content="닫기"
|
<Button DockPanel.Dock="Right" Content="닫기"
|
||||||
Style="{StaticResource LinkBtn}" Click="OnCloseTrial"/>
|
Style="{StaticResource LinkBtn}" Click="OnCloseTrial"/>
|
||||||
|
<!--
|
||||||
|
무엇을 돌렸는지 보여 준다. 0행이 나왔을 때
|
||||||
|
'치환이 NULL 이라 조건이 사라진 건지, 이 환자에게 자료가 없는 건지'를
|
||||||
|
창을 나가지 않고 가르는 유일한 수단이다 — 치환을 마친 문장이 답을 갖고 있다.
|
||||||
|
-->
|
||||||
|
<Button x:Name="ShowTrialSqlButton" DockPanel.Dock="Right"
|
||||||
|
Content="돌린 SQL 보기" Margin="0,0,10,0"
|
||||||
|
Style="{StaticResource LinkBtn}" Click="OnToggleTrialSql"/>
|
||||||
<TextBlock x:Name="TrialSummary" FontSize="12.5" TextWrapping="Wrap"
|
<TextBlock x:Name="TrialSummary" FontSize="12.5" TextWrapping="Wrap"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
<!-- 돌린 SQL 원문 — 읽기 전용. 08c 데이터소스 결과 창과 같은 구성이다 -->
|
||||||
|
<Border x:Name="TrialSqlPane" DockPanel.Dock="Top" Visibility="Collapsed"
|
||||||
|
Margin="0,0,0,8" BorderThickness="1"
|
||||||
|
BorderBrush="{DynamicResource B.Line2}"
|
||||||
|
Background="{DynamicResource B.Input}">
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right" Content="SQL 복사" VerticalAlignment="Top"
|
||||||
|
Margin="6,4,6,4" Style="{StaticResource LinkBtn}"
|
||||||
|
Click="OnCopyTrialSql"/>
|
||||||
|
<TextBox x:Name="TrialSqlText" IsReadOnly="True" BorderThickness="0"
|
||||||
|
Background="Transparent" FontFamily="Consolas" FontSize="12"
|
||||||
|
TextWrapping="Wrap" MaxHeight="120" Padding="8,6"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
AutomationProperties.Name="돌린 SQL"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
<!-- 표를 감싸는 테두리 — 표 자체는 테두리를 그리지 않는다(위 스타일) -->
|
<!-- 표를 감싸는 테두리 — 표 자체는 테두리를 그리지 않는다(위 스타일) -->
|
||||||
<Border BorderThickness="1" BorderBrush="{DynamicResource B.Line2}">
|
<Border BorderThickness="1" BorderBrush="{DynamicResource B.Line2}">
|
||||||
<DataGrid x:Name="TrialGrid" Style="{StaticResource ResultGrid}" MaxHeight="200"
|
<DataGrid x:Name="TrialGrid" Style="{StaticResource ResultGrid}" MaxHeight="200"
|
||||||
|
|||||||
@@ -575,7 +575,11 @@ public partial class QueryEditorWindow : Window
|
|||||||
// 같은 정보를 두 군데 적으면 요약이 두 줄로 늘어 결과 표를 밀어내고,
|
// 같은 정보를 두 군데 적으면 요약이 두 줄로 늘어 결과 표를 밀어내고,
|
||||||
// 화면에 환자 이름이 필요 이상으로 여러 번 남는다.
|
// 화면에 환자 이름이 필요 이상으로 여러 번 남는다.
|
||||||
// 환자가 있을 때 필요한 말은 이미 버튼이 하고 있으므로 여기서는 침묵한다.
|
// 환자가 있을 때 필요한 말은 이미 버튼이 하고 있으므로 여기서는 침묵한다.
|
||||||
var basis = variables is not null ? string.Empty : "변수 NULL — 구문만 검사";
|
// 치환 대상이 <b>하나도 없는</b> 쿼리에 '변수 NULL' 을 적으면 거짓이다 —
|
||||||
|
// 치환할 것이 없으니 NULL 로 바뀐 것도 없다. 그런데 그 꼬리표가 늘 붙어 있어서
|
||||||
|
// 정작 진짜 경고가 붙은 날에도 눈에 안 들어왔다. 늘 뜨는 경고는 경고가 아니다.
|
||||||
|
var hasSubstitution = converted.Hits.Count > 0;
|
||||||
|
var basis = variables is null && hasSubstitution ? "변수 NULL — 구문만 검사" : string.Empty;
|
||||||
var notes = new List<string>();
|
var notes = new List<string>();
|
||||||
// 환자를 안 골랐으면 <b>모든</b> 변수가 Unknown 이다 — 그것을 "아직 옮기지 않았다"고 적으면
|
// 환자를 안 골랐으면 <b>모든</b> 변수가 Unknown 이다 — 그것을 "아직 옮기지 않았다"고 적으면
|
||||||
// 거짓이 된다(ChtNum 은 옮겨져 있다). 그 사유는 위 basis 줄이 이미 말한다.
|
// 거짓이 된다(ChtNum 은 옮겨져 있다). 그 사유는 위 basis 줄이 이미 말한다.
|
||||||
@@ -611,8 +615,11 @@ public partial class QueryEditorWindow : Window
|
|||||||
// 빈 조각은 버린다 — 안 버리면 "성공 · · 경고" 처럼 가운뎃점이 떠 있는다
|
// 빈 조각은 버린다 — 안 버리면 "성공 · · 경고" 처럼 가운뎃점이 떠 있는다
|
||||||
TrialSummary.Text = string.Join(" · ",
|
TrialSummary.Text = string.Join(" · ",
|
||||||
new[] { head, basis }.Concat(notes).Where(s => s.Length > 0));
|
new[] { head, basis }.Concat(notes).Where(s => s.Length > 0));
|
||||||
|
// 0행이어도 표를 띄운다 — <b>컬럼 이름</b>은 이미 알고 있고, 그것이 배선 필드 칸에
|
||||||
|
// 옮겨 적을 값이다. 표를 접으면 방금 조회한 컬럼명을 다시 알아내려고
|
||||||
|
// 쿼리를 또 돌려야 한다(0행이 배선 작업에서 드문 상태도 아니다).
|
||||||
TrialGrid.ItemsSource = ToGridRows(result);
|
TrialGrid.ItemsSource = ToGridRows(result);
|
||||||
TrialGrid.Visibility = result.Rows.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
TrialGrid.Visibility = result.Columns.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -621,6 +628,9 @@ public partial class QueryEditorWindow : Window
|
|||||||
new[] { "실패 — " + result.Error, basis }.Concat(notes).Where(s => s.Length > 0));
|
new[] { "실패 — " + result.Error, basis }.Concat(notes).Where(s => s.Length > 0));
|
||||||
TrialGrid.ItemsSource = null;
|
TrialGrid.ItemsSource = null;
|
||||||
TrialGrid.Visibility = Visibility.Collapsed;
|
TrialGrid.Visibility = Visibility.Collapsed;
|
||||||
|
// 실패했으면 표가 어차피 없다 — 그 자리에 돌린 문장을 바로 보여 준다.
|
||||||
|
// 오라클 오류 문구만으로는 어디가 틀렸는지 알 수 없고, 답은 치환된 문장에 있다.
|
||||||
|
SetTrialSqlVisible(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,6 +783,37 @@ public partial class QueryEditorWindow : Window
|
|||||||
/// <summary>검증 이후 SQL 이 바뀌었을 때 요약 앞에 붙는 표시</summary>
|
/// <summary>검증 이후 SQL 이 바뀌었을 때 요약 앞에 붙는 표시</summary>
|
||||||
private const string StaleMark = "(수정됨 — 다시 실행하세요) ";
|
private const string StaleMark = "(수정됨 — 다시 실행하세요) ";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// '돌린 SQL' 접기/펴기 — 치환을 마친 문장을 창 안에서 확인한다.
|
||||||
|
///
|
||||||
|
/// 0행이 나왔을 때 <b>치환이 NULL 이라 조건이 사라진 것</b>인지
|
||||||
|
/// <b>이 환자에게 자료가 없는 것</b>인지 가르는 유일한 수단이다. 그 답은 돌린 문장에 있다.
|
||||||
|
/// 전에는 그 문장을 필드에 담아 두기만 하고(진단용) 화면에는 내보내지 않았다.
|
||||||
|
/// </summary>
|
||||||
|
private void OnToggleTrialSql(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var show = TrialSqlPane.Visibility != Visibility.Visible;
|
||||||
|
SetTrialSqlVisible(show);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetTrialSqlVisible(bool show)
|
||||||
|
{
|
||||||
|
TrialSqlPane.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
ShowTrialSqlButton.Content = show ? "돌린 SQL 숨기기" : "돌린 SQL 보기";
|
||||||
|
if (show)
|
||||||
|
{
|
||||||
|
TrialSqlText.Text = lastTrialSql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCopyTrialSql(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (lastTrialSql.Length > 0)
|
||||||
|
{
|
||||||
|
Clipboard.SetText(lastTrialSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>줄·열 표시 — 오류 메시지의 위치를 찾아갈 때 쓴다</summary>
|
/// <summary>줄·열 표시 — 오류 메시지의 위치를 찾아갈 때 쓴다</summary>
|
||||||
private void UpdateCaretText()
|
private void UpdateCaretText()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,6 +66,22 @@
|
|||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
끄는 동안의 치수·좌표 — 편집 크롬이라 종이에는 없다.
|
||||||
|
줌이 걸린 층이라 역배율로 되돌려 글자 크기를 화면 고정으로 만든다
|
||||||
|
(간격 수치 칩이 이미 같은 방식을 쓴다).
|
||||||
|
-->
|
||||||
|
<Border Canvas.Left="{Binding DragBadgeX}" Canvas.Top="{Binding DragBadgeY}"
|
||||||
|
Background="{DynamicResource B.AccentFill}" CornerRadius="3" Padding="5,1"
|
||||||
|
RenderTransformOrigin="0,0"
|
||||||
|
Visibility="{Binding ShowDragBadge, Converter={StaticResource BoolToVisibility}}">
|
||||||
|
<Border.RenderTransform>
|
||||||
|
<ScaleTransform ScaleX="{Binding InverseZoom}" ScaleY="{Binding InverseZoom}"/>
|
||||||
|
</Border.RenderTransform>
|
||||||
|
<TextBlock Text="{Binding DragBadgeText}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource B.OnAccent}"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- 마퀴(러버밴드) -->
|
<!-- 마퀴(러버밴드) -->
|
||||||
<Rectangle Canvas.Left="{Binding MarX}" Canvas.Top="{Binding MarY}"
|
<Rectangle Canvas.Left="{Binding MarX}" Canvas.Top="{Binding MarY}"
|
||||||
Width="{Binding MarW}" Height="{Binding MarH}"
|
Width="{Binding MarW}" Height="{Binding MarH}"
|
||||||
|
|||||||
Reference in New Issue
Block a user