2단계 — 신규 서식에 런타임 설정을 걸 수 있게 (선 3종·공통 속성·키 자동완성)
레거시로는 되는데 SheetMe 로는 못 만들던 것들을 메웠다. 어떤 속성을 올릴지는 취향으로 정하지 않고
--db-props 로 운영 디자인 1,271건 / 컨트롤 164,091개를 세고, 각 컨트롤 .vb 의 필드 선언을 읽어 정했다.
■ 선(Line) — 새로 그은 세로선이 EMR 에서 사라지던 문제
MLine 은 방향을 크기가 아니라 Orientation 으로 정한다(MLine.vb:181-190).
키가 없으면 Horizontal 이라 (0,0)→(Width,0) 을 그리므로, 폭 1·높이 210 으로 세로처럼 만든 선은
디자이너에만 보이고 기록지에서는 1px 점이 된다. 운영 선 32,599개 중 Orientation 보유 12,811개이고
그 중 Vertical 이 12,721개 — 세로선은 예외가 아니라 주류다.
Orientation·BorderWidth·DashStyle 을 인스펙터에 올렸다. 그리고 값만 올리지 않고
레거시 컨트롤이 스스로 지키는 불변식을 함께 옮겼다(LineGeometry):
· 두께 축은 항상 BorderWidth 다 — MLine.OnResize 가 매번 되돌린다. 그래서 선을 끌어
두껍게 만들 수 없다. 이걸 허용하면 '세로처럼 보이지만 EMR 에선 점'인 선을 다시 만들 수 있다.
· 방향 전환은 길이를 보존한 채 축을 바꾼다(레거시 Orientation 세터: Width = Height 후 OnResize).
· 가로로 되돌리면 Orientation 키를 지운다 — <DefaultValue(Horizontal)> 라 레거시 직렬화기도 생략한다.
--db-lines 를 새로 만들어 확인한 결과 운영 선 32,599개에서 경계 모양과 Orientation 불일치는 0건이다.
레거시가 구조적으로 막아 왔다는 뜻이고, 그래서 캔버스 렌더는 건드리지 않았다(P062 바이트 동일).
■ 공통 런타임 속성 — 서명란·인쇄 제외·필수입력·자동높이·재조회
신규 컨트롤은 Props 가 사실상 비어 있어 '전체 속성(0개)'이었고, 고급 편집도 이미 있는 키만 나열한다.
결과적으로 서명이 필요한 동의서나 점수를 합산하는 평가지를 SheetMe 만으로 새로 만들 수 없었다.
타입별 기술자에 다음을 추가했다(기본값은 각 컨트롤 .vb 필드 선언에서 확인한 값이다):
PrintOutPut(기본 True) · PreventEditing · IsRequiredValue(No/Yes) · Visible · AutoHeight ·
ReLoadData 3종 · MPictureBox 의 IsSignature/SignatureIndex.
PrintOutPut 을 True 로 표시하는 것이 중요하다 — 키가 없는 컨트롤이 '인쇄 안 함'으로 보이면
사용자가 껐다 켜는 순간 명시 False 가 기록돼 실제로 인쇄에서 빠진다
(런타임 필터: If Me.Visible = False OrElse mbPrintOutPut = False Then Return False).
■ 감사 권고와 다르게 한 것 3가지 — 근거가 반대였다
1. Score 를 5개 타입에 추가하라는 권고는 따르지 않았다. 실제로 저장값을 읽는 것은
CheckBox·RadioButton 뿐이다(게터가 mdScore 반환). TextBox·MaskedTextBox 의 Score 게터는
저장값을 무시하고 Me.Text 를 숫자로 읽으며, CalcBox 는 세터가 오히려 Text 를 덮어쓴다.
편집기를 붙였으면 사용자는 배점을 걸었다고 믿고 런타임은 무시하는 상태가 된다.
ComboBox 는 Score 가 아니라 ItemScore("/" 구분, 항목 순서와 1:1)를 노출했다.
실측이 먼저 신호를 줬다: ComboBox 의 Score 값 분포가 Text 와 정확히 같았다(-×625, ++×22).
2. Button 에는 PrintOutPut 을 붙이지 않았다 — MButton.vb 에 그 속성 자체가 없다.
3. 라벨의 표시 여부는 소문자 visible 이다. Label.vb:209-217 이 <Browsable(False)> Shadows 로
Control.Visible 을 가리고 직렬화기가 그 그림자를 쓴다. 운영에서도 소문자 58,864건(99.7%)
대 대문자 196건. 대문자로 쓰면 라벨은 그대로 보인다.
■ '속성 추가' 자동완성
큐레이션 밖의 값을 걸려면 키를 손으로 쳐야 했는데, 철자가 틀려도 경고가 없고
그 서식은 배포된 뒤에야 이상하게 동작한다. 타입별 실사용 키 목록(LegacyPropertyCatalog,
--db-props 집계 기반)을 제안 칩으로 깔았다. 이미 있는 키와 인스펙터가 이미 다루는 키는 빼고,
부분 일치로 좁히며 접두 일치를 앞에 둔다. 자유 입력은 그대로 열어 뒀다 — 제안이지 검증이 아니고
사이트가 추가한 키도 있다.
■ 1단계 잔여분: 선택 밖 컨트롤을 Ctrl+드래그하면 엉뚱한 것이 끌리던 문제
Ctrl/Shift 클릭은 선택을 바꾸지 않고 업에서 토글하므로, 그 상태로 드래그가 시작되면
잡은 컨트롤이 아니라 기존 선택분이 끌려갔다(선택이 비어 있으면 아무것도 안 끌렸다).
드래그가 실제로 시작되는 지점에서 잡은 것을 선택에 넣는다 — 기존 다중선택은 유지한다(업 토글과 같은 규칙).
편집 스모크 14건 추가(선 기하 6 · Ctrl+드래그 2 · 자동완성 5 · 라벨 소문자 함정 1).
회귀: 테스트 124/124, 편집 스모크 183건 실패 0,
DB 왕복 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
df1a29afa2
commit
59b0e457fe
@@ -7,6 +7,59 @@ namespace SheetMe.Core.Catalog;
|
|||||||
public static class ControlRegistry
|
public static class ControlRegistry
|
||||||
{
|
{
|
||||||
#region Member Fields
|
#region Member Fields
|
||||||
|
/// <summary>
|
||||||
|
/// 여러 타입이 공유하는 런타임 속성 — 레거시 컨트롤 필드 초기값을 그대로 옮긴 것.
|
||||||
|
///
|
||||||
|
/// <b>기본값(Default)은 지어내지 않았다.</b> 각 컨트롤 .vb 의 필드 선언을 읽어 확인한 값이다.
|
||||||
|
/// 예: <c>Private mbPrintOutPut As Boolean = True</c> (Label.vb:21, TextBox.vb:31, CheckBox.vb:31,
|
||||||
|
/// Panel.vb:20, MLine.vb:15, MPictureBox.vb:35 — 전부 동일). 기본값을 알려주지 않으면
|
||||||
|
/// 키가 없는 컨트롤이 '인쇄 안 함'으로 보이고, 사용자가 껐다 켜는 순간 명시 False 가 기록돼
|
||||||
|
/// 실제로 인쇄에서 빠진다(런타임 필터: <c>If Me.Visible = False OrElse mbPrintOutPut = False Then Return False</c>).
|
||||||
|
///
|
||||||
|
/// 이 목록에 없는 타입에는 붙이지 않는다 — 레거시가 노출하지 않는 속성을 우리가 만들어 주면
|
||||||
|
/// 사용자는 설정했다고 믿지만 런타임은 그 값을 읽지 않는다.
|
||||||
|
/// </summary>
|
||||||
|
private static class Runtime
|
||||||
|
{
|
||||||
|
/// <summary>인쇄 출력 여부 — 기본 True</summary>
|
||||||
|
public static PropertyDef PrintOutPut => new()
|
||||||
|
{
|
||||||
|
Key = "PrintOutPut", Label = "인쇄 출력", Editor = PropEditorKind.Toggle, Default = "True",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>읽기 전용 — TextBox 는 ReadOnly 와 같은 것(TextBox.vb PreventEditing → Me.ReadOnly)</summary>
|
||||||
|
public static PropertyDef PreventEditing => new()
|
||||||
|
{
|
||||||
|
Key = "PreventEditing", Label = "읽기 전용", Editor = PropEditorKind.Toggle, Default = "False",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>필수 입력 — 값은 True/False 가 아니라 No/Yes(EN_NoYes, 기본 No=0)</summary>
|
||||||
|
public static PropertyDef IsRequiredValue => new()
|
||||||
|
{
|
||||||
|
Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice,
|
||||||
|
Default = "No", Choices = new[] { "No", "Yes" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 아래 3종의 라벨은 짧게 잡았다 — 인스펙터 라벨 열은 92px 이라 8자를 넘기면 두 줄로 접힌다.
|
||||||
|
/// <summary>저장된 기록을 다시 열 때 최신 데이터를 다시 조회 — 기본 False</summary>
|
||||||
|
public static PropertyDef ReLoadDataOnSavedSheet => new()
|
||||||
|
{
|
||||||
|
Key = "ReLoadDataOnSavedSheet", Label = "저장본 재조회", Editor = PropEditorKind.Toggle, Default = "False",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>열람 모드에서도 재조회 — 기본 False</summary>
|
||||||
|
public static PropertyDef ReLoadDataOnViewMode => new()
|
||||||
|
{
|
||||||
|
Key = "ReLoadDataOnViewMode", Label = "열람 시 재조회", Editor = PropEditorKind.Toggle, Default = "False",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>재조회 시 확인 팝업 생략 — 기본 False</summary>
|
||||||
|
public static PropertyDef ReLoadDataMsgNoCheck => new()
|
||||||
|
{
|
||||||
|
Key = "ReLoadDataMsgNoCheck", Label = "재조회 무확인", Editor = PropEditorKind.Toggle, Default = "False",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly List<ControlDescriptor> all = new()
|
private static readonly List<ControlDescriptor> all = new()
|
||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
@@ -20,6 +73,11 @@ public static class ControlRegistry
|
|||||||
Choices = new[] { "TopLeft", "TopCenter", "TopRight", "MiddleLeft", "MiddleCenter", "MiddleRight", "BottomLeft", "BottomCenter", "BottomRight" } },
|
Choices = new[] { "TopLeft", "TopCenter", "TopRight", "MiddleLeft", "MiddleCenter", "MiddleRight", "BottomLeft", "BottomCenter", "BottomRight" } },
|
||||||
new() { Key = "ForeColor", Label = "글자색", Editor = PropEditorKind.Color },
|
new() { Key = "ForeColor", Label = "글자색", Editor = PropEditorKind.Color },
|
||||||
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
|
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
|
||||||
|
// 라벨만 소문자 visible 이다 — Label.vb:209-217 이 <Browsable(False)> Shadows Property visible 로
|
||||||
|
// Control.Visible 을 가리고, 직렬화기는 이 그림자 속성을 쓴다. 운영에서도 소문자 58,864건(99.7%)
|
||||||
|
// 대 대문자 196건이다. 대문자로 쓰면 라벨은 그대로 보인다.
|
||||||
|
new() { Key = "visible", Label = "표시", Editor = PropEditorKind.Toggle, Default = "True" },
|
||||||
|
Runtime.PrintOutPut,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -29,8 +87,10 @@ public static class ControlRegistry
|
|||||||
{
|
{
|
||||||
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
|
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
|
||||||
new() { Key = "Multiline", Label = "여러 줄", Editor = PropEditorKind.Toggle },
|
new() { Key = "Multiline", Label = "여러 줄", Editor = PropEditorKind.Toggle },
|
||||||
|
// 입력이 높이를 넘으면 내용에 맞춰 늘어난다(TextBox.vb:311 자동 속성 — 기본 False)
|
||||||
|
new() { Key = "AutoHeight", Label = "자동 높이", Editor = PropEditorKind.Toggle, Default = "False" },
|
||||||
new() { Key = "TextAlign", Label = "정렬", Editor = PropEditorKind.Choice, Default = "Left", Choices = new[] { "Left", "Center", "Right" } },
|
new() { Key = "TextAlign", Label = "정렬", Editor = PropEditorKind.Choice, Default = "Left", Choices = new[] { "Left", "Center", "Right" } },
|
||||||
new() { Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice, Choices = new[] { "No", "Yes" } },
|
Runtime.IsRequiredValue,
|
||||||
new() { Key = "InitialValue", Label = "초기값", Editor = PropEditorKind.Text },
|
new() { Key = "InitialValue", Label = "초기값", Editor = PropEditorKind.Text },
|
||||||
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
|
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
|
||||||
new() { Key = "DataActionTag", Label = "액션 태그", Editor = PropEditorKind.DataActionTag },
|
new() { Key = "DataActionTag", Label = "액션 태그", Editor = PropEditorKind.DataActionTag },
|
||||||
@@ -41,6 +101,12 @@ public static class ControlRegistry
|
|||||||
new() { Key = "RwdOrderAutYon", Label = "처방연동 상용구", Editor = PropEditorKind.Toggle },
|
new() { Key = "RwdOrderAutYon", Label = "처방연동 상용구", Editor = PropEditorKind.Toggle },
|
||||||
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
|
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
|
||||||
new() { Key = "BorderStyle", Label = "테두리", Editor = PropEditorKind.Choice, Default = "Fixed3D", Choices = new[] { "None", "FixedSingle", "Fixed3D" } },
|
new() { Key = "BorderStyle", Label = "테두리", Editor = PropEditorKind.Choice, Default = "Fixed3D", Choices = new[] { "None", "FixedSingle", "Fixed3D" } },
|
||||||
|
new() { Key = "Visible", Label = "표시", Editor = PropEditorKind.Toggle, Default = "True" },
|
||||||
|
Runtime.PrintOutPut,
|
||||||
|
Runtime.PreventEditing,
|
||||||
|
Runtime.ReLoadDataOnSavedSheet,
|
||||||
|
Runtime.ReLoadDataOnViewMode,
|
||||||
|
Runtime.ReLoadDataMsgNoCheck,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -49,8 +115,14 @@ public static class ControlRegistry
|
|||||||
Properties = new PropertyDef[]
|
Properties = new PropertyDef[]
|
||||||
{
|
{
|
||||||
new() { Key = "Mask", Label = "마스크", Editor = PropEditorKind.Mask },
|
new() { Key = "Mask", Label = "마스크", Editor = PropEditorKind.Mask },
|
||||||
new() { Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice, Choices = new[] { "No", "Yes" } },
|
Runtime.IsRequiredValue,
|
||||||
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
|
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
|
||||||
|
new() { Key = "AutoHeight", Label = "자동 높이", Editor = PropEditorKind.Toggle, Default = "False" },
|
||||||
|
Runtime.PrintOutPut,
|
||||||
|
Runtime.PreventEditing,
|
||||||
|
Runtime.ReLoadDataOnSavedSheet,
|
||||||
|
Runtime.ReLoadDataOnViewMode,
|
||||||
|
Runtime.ReLoadDataMsgNoCheck,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -60,8 +132,12 @@ public static class ControlRegistry
|
|||||||
{
|
{
|
||||||
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
|
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
|
||||||
new() { Key = "Checked", Label = "기본 체크", Editor = PropEditorKind.Toggle },
|
new() { Key = "Checked", Label = "기본 체크", Editor = PropEditorKind.Toggle },
|
||||||
new() { Key = "Score", Label = "점수", Editor = PropEditorKind.Number },
|
// 진짜 배점이다 — CheckBox.vb:352-367 게터가 저장값(mdScore)을 그대로 돌려준다(체크됐을 때).
|
||||||
new() { Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice, Choices = new[] { "No", "Yes" } },
|
new() { Key = "Score", Label = "점수", Editor = PropEditorKind.Number, Default = "0" },
|
||||||
|
Runtime.IsRequiredValue,
|
||||||
|
Runtime.PrintOutPut,
|
||||||
|
Runtime.ReLoadDataOnSavedSheet,
|
||||||
|
Runtime.ReLoadDataMsgNoCheck,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -71,6 +147,12 @@ public static class ControlRegistry
|
|||||||
{
|
{
|
||||||
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
|
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
|
||||||
new() { Key = "Checked", Label = "기본 선택", Editor = PropEditorKind.Toggle },
|
new() { Key = "Checked", Label = "기본 선택", Editor = PropEditorKind.Toggle },
|
||||||
|
// CheckBox 와 같은 구조(RadioButton.vb Score 게터가 mdScore 반환) — 낙상·욕창 평가지의 배점원
|
||||||
|
new() { Key = "Score", Label = "점수", Editor = PropEditorKind.Number, Default = "0" },
|
||||||
|
Runtime.IsRequiredValue,
|
||||||
|
Runtime.PrintOutPut,
|
||||||
|
Runtime.ReLoadDataOnSavedSheet,
|
||||||
|
Runtime.ReLoadDataMsgNoCheck,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -79,8 +161,13 @@ public static class ControlRegistry
|
|||||||
Properties = new PropertyDef[]
|
Properties = new PropertyDef[]
|
||||||
{
|
{
|
||||||
new() { Key = "Items", Label = "선택지(줄바꿈 구분)", Editor = PropEditorKind.StringList },
|
new() { Key = "Items", Label = "선택지(줄바꿈 구분)", Editor = PropEditorKind.StringList },
|
||||||
new() { Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice, Choices = new[] { "No", "Yes" } },
|
Runtime.IsRequiredValue,
|
||||||
new() { Key = "DataTableField", Label = "데이터 필드", Editor = PropEditorKind.Text },
|
new() { Key = "DataTableField", Label = "데이터 필드", Editor = PropEditorKind.Text },
|
||||||
|
// 콤보의 배점은 Score 가 아니라 ItemScore 다 — ComboBox.vb Score 게터는 저장값을 읽지 않고
|
||||||
|
// ItemScore 를 "/" 로 쪼개 SelectedIndex 번째를 돌려준다. 항목 순서와 1:1 로 맞춰야 한다.
|
||||||
|
new() { Key = "ItemScore", Label = "항목 배점", Editor = PropEditorKind.Text },
|
||||||
|
Runtime.PrintOutPut,
|
||||||
|
Runtime.PreventEditing,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -106,6 +193,13 @@ public static class ControlRegistry
|
|||||||
{
|
{
|
||||||
new() { Key = "Format", Label = "형식", Editor = PropEditorKind.Choice, Default = "Long", Choices = new[] { "Long", "Short", "Time", "Custom" } },
|
new() { Key = "Format", Label = "형식", Editor = PropEditorKind.Choice, Default = "Long", Choices = new[] { "Long", "Short", "Time", "Custom" } },
|
||||||
new() { Key = "CustomFormat", Label = "사용자 형식", Editor = PropEditorKind.Text },
|
new() { Key = "CustomFormat", Label = "사용자 형식", Editor = PropEditorKind.Text },
|
||||||
|
Runtime.IsRequiredValue,
|
||||||
|
new() { Key = "Visible", Label = "표시", Editor = PropEditorKind.Toggle, Default = "True" },
|
||||||
|
Runtime.PrintOutPut,
|
||||||
|
Runtime.PreventEditing,
|
||||||
|
Runtime.ReLoadDataOnSavedSheet,
|
||||||
|
Runtime.ReLoadDataOnViewMode,
|
||||||
|
Runtime.ReLoadDataMsgNoCheck,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -115,6 +209,9 @@ public static class ControlRegistry
|
|||||||
{
|
{
|
||||||
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
|
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
|
||||||
new() { Key = "BorderStyle", Label = "테두리", Editor = PropEditorKind.Choice, Default = "None", Choices = new[] { "None", "FixedSingle", "Fixed3D" } },
|
new() { Key = "BorderStyle", Label = "테두리", Editor = PropEditorKind.Choice, Default = "None", Choices = new[] { "None", "FixedSingle", "Fixed3D" } },
|
||||||
|
new() { Key = "Visible", Label = "표시", Editor = PropEditorKind.Toggle, Default = "True" },
|
||||||
|
Runtime.IsRequiredValue,
|
||||||
|
Runtime.PrintOutPut,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -123,14 +220,30 @@ public static class ControlRegistry
|
|||||||
Properties = new PropertyDef[]
|
Properties = new PropertyDef[]
|
||||||
{
|
{
|
||||||
new() { Key = "Text", Label = "제목", Editor = PropEditorKind.Text },
|
new() { Key = "Text", Label = "제목", Editor = PropEditorKind.Text },
|
||||||
|
new() { Key = "Visible", Label = "표시", Editor = PropEditorKind.Toggle, Default = "True" },
|
||||||
|
Runtime.IsRequiredValue,
|
||||||
|
Runtime.PrintOutPut,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
// 방향은 크기가 아니라 Orientation 이 정한다 — MLine.vb:181-190
|
||||||
|
// Horizontal → DrawLine(0,0,Width,0) / Vertical → DrawLine(0,0,0,Height), 인쇄 경로도 동일.
|
||||||
|
// Orientation 이 없으면 런타임은 Horizontal 로 보고 (0,0)→(Width,0) 을 그린다.
|
||||||
|
// 그래서 폭 1·높이 210 으로 '세로처럼' 만든 선은 EMR 에서 1px 점이 된다 — 디자이너에만 보인다.
|
||||||
|
// 운영 선 32,599개 중 Orientation 보유 12,811개이고 그 중 Vertical 이 12,721개다(--db-lines).
|
||||||
Type = "Line", DisplayName = "선", DefaultW = 200, DefaultH = 1,
|
Type = "Line", DisplayName = "선", DefaultW = 200, DefaultH = 1,
|
||||||
Properties = new PropertyDef[]
|
Properties = new PropertyDef[]
|
||||||
{
|
{
|
||||||
new() { Key = "LineColor", Label = "선 색", Editor = PropEditorKind.Color },
|
new() { Key = "LineColor", Label = "선 색", Editor = PropEditorKind.Color },
|
||||||
|
new() { Key = "Orientation", Label = "방향", Editor = PropEditorKind.Choice,
|
||||||
|
Default = "Horizontal", Choices = new[] { "Horizontal", "Vertical" } },
|
||||||
|
new() { Key = "BorderWidth", Label = "굵기", Editor = PropEditorKind.Number, Default = "1" },
|
||||||
|
// System.Drawing.Drawing2D.DashStyle. Custom 은 뺐다 — DashPattern 없이 Custom 을 고르면
|
||||||
|
// 런타임이 예외를 내거나 실선으로 떨어진다(운영에도 Custom 은 0건).
|
||||||
|
new() { Key = "DashStyle", Label = "선 모양", Editor = PropEditorKind.Choice,
|
||||||
|
Default = "Solid", Choices = new[] { "Solid", "Dash", "Dot", "DashDot", "DashDotDot" } },
|
||||||
|
Runtime.PrintOutPut,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -140,6 +253,17 @@ public static class ControlRegistry
|
|||||||
{
|
{
|
||||||
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
|
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
|
||||||
new() { Key = "SizeMode", Label = "크기 모드", Editor = PropEditorKind.Choice, Default = "Normal", Choices = new[] { "Normal", "StretchImage", "AutoSize", "CenterImage", "Zoom" } },
|
new() { Key = "SizeMode", Label = "크기 모드", Editor = PropEditorKind.Choice, Default = "Normal", Choices = new[] { "Normal", "StretchImage", "AutoSize", "CenterImage", "Zoom" } },
|
||||||
|
// 서명란 — 이 값이 True 여야 런타임이 서명 수집 대상으로 잡는다(MPictureBox.vb:1480, 사용처 :992/:1075/:1212…).
|
||||||
|
// 이걸 못 걸면 서명이 필요한 동의서를 SheetMe 만으로 새로 만들 수 없다.
|
||||||
|
new() { Key = "IsSignature", Label = "서명란", Editor = PropEditorKind.Toggle, Default = "False" },
|
||||||
|
// 서명이 여러 개인 서식에서 몇 번째 칸인지(MPictureBox.vb:28 miSignatureIndex)
|
||||||
|
new() { Key = "SignatureIndex", Label = "서명 순번", Editor = PropEditorKind.Number, Default = "0" },
|
||||||
|
new() { Key = "Visible", Label = "표시", Editor = PropEditorKind.Toggle, Default = "True" },
|
||||||
|
Runtime.PrintOutPut,
|
||||||
|
Runtime.PreventEditing,
|
||||||
|
Runtime.ReLoadDataOnSavedSheet,
|
||||||
|
Runtime.ReLoadDataOnViewMode,
|
||||||
|
Runtime.ReLoadDataMsgNoCheck,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
@@ -149,10 +273,20 @@ public static class ControlRegistry
|
|||||||
{
|
{
|
||||||
new() { Key = "Formula", Label = "수식", Editor = PropEditorKind.MultilineText },
|
new() { Key = "Formula", Label = "수식", Editor = PropEditorKind.MultilineText },
|
||||||
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
|
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
|
||||||
|
// 계산박스에는 Score 를 노출하지 않는다 — MCalcBox.vb Score 게터는 저장값이 아니라
|
||||||
|
// Me.Text 를 숫자로 읽어 돌려주고, 세터는 반대로 Text 를 덮어쓴다. 배점은 이 칸이 아니라
|
||||||
|
// 합산 대상인 CheckBox/RadioButton 쪽에 준다.
|
||||||
|
new() { Key = "Visible", Label = "표시", Editor = PropEditorKind.Toggle, Default = "True" },
|
||||||
|
Runtime.IsRequiredValue,
|
||||||
|
Runtime.PrintOutPut,
|
||||||
|
Runtime.PreventEditing,
|
||||||
|
Runtime.ReLoadDataOnSavedSheet,
|
||||||
|
Runtime.ReLoadDataMsgNoCheck,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
// MButton 에는 PrintOutPut 이 없다(MButton.vb 에 해당 속성 자체가 없음) — 붙이지 않는다
|
||||||
Type = "Button", DisplayName = "버튼", DefaultW = 90, DefaultH = 26,
|
Type = "Button", DisplayName = "버튼", DefaultW = 90, DefaultH = 26,
|
||||||
Properties = new PropertyDef[]
|
Properties = new PropertyDef[]
|
||||||
{
|
{
|
||||||
@@ -162,6 +296,8 @@ public static class ControlRegistry
|
|||||||
new() { Key = "GetDataActionTagControl", Label = "입력 파라미터 컨트롤", Editor = PropEditorKind.Text },
|
new() { Key = "GetDataActionTagControl", Label = "입력 파라미터 컨트롤", Editor = PropEditorKind.Text },
|
||||||
new() { Key = "KeyboardShortcut", Label = "단축키", Editor = PropEditorKind.Text },
|
new() { Key = "KeyboardShortcut", Label = "단축키", Editor = PropEditorKind.Text },
|
||||||
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
|
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
|
||||||
|
new() { Key = "Visible", Label = "표시", Editor = PropEditorKind.Toggle, Default = "True" },
|
||||||
|
Runtime.PreventEditing,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
namespace SheetMe.Core.Catalog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 타입별로 레거시가 실제로 저장하는 속성 키 목록 — '속성 추가' 자동완성의 원천.
|
||||||
|
///
|
||||||
|
/// <b>왜 필요한가.</b> 인스펙터가 큐레이션한 속성 밖의 값을 걸려면 '속성 추가'로 키를 손수 쳐야 하는데,
|
||||||
|
/// 키 이름을 외워서 정확히 맞혀야 한다. 철자가 틀리면 아무 경고 없이 무의미한 속성이 하나 생기고,
|
||||||
|
/// 그 서식은 배포된 뒤 임상 화면에서야 기대한 동작을 안 한다는 형태로 드러난다.
|
||||||
|
/// 함정이 실재한다 — 라벨은 소문자 <c>visible</c>, 나머지는 <c>Visible</c>,
|
||||||
|
/// 체크박스·라디오는 또 <c>ControlVisible</c> 을 쓴다. <c>PrintOutPut</c> 의 대문자 P 도 마찬가지다.
|
||||||
|
///
|
||||||
|
/// <b>출처.</b> 운영 DB 디자인 1,271건·컨트롤 164,091개를 훑어 실제로 저장돼 있는 키를 집계했다
|
||||||
|
/// (진단 <c>--db-props</c>). 레거시 소스의 Public Property 목록이 아니라 <b>실제 저장 결과</b>를 쓴 이유는,
|
||||||
|
/// 노출은 되지만 아무도 안 쓰는 키까지 제안하면 목록이 길어져 고르기 어려워지고,
|
||||||
|
/// 반대로 사이트가 추가한 키는 소스에 없어도 실데이터에는 있기 때문이다.
|
||||||
|
///
|
||||||
|
/// 여기 없는 키도 여전히 손으로 입력할 수 있다 — 이 목록은 <b>제안</b>이지 검증이 아니다.
|
||||||
|
/// 레이아웃 계열(Location/Size/Name/TabIndex 등)은 인스펙터가 따로 다루므로 제안에서 뺐다.
|
||||||
|
/// </summary>
|
||||||
|
public static class LegacyPropertyCatalog
|
||||||
|
{
|
||||||
|
#region Member Fields
|
||||||
|
// 모든 타입에 공통으로 붙는 뼈대 속성 — 인스펙터의 위치·크기·이름 행이 이미 담당한다.
|
||||||
|
// 제안 목록에 넣으면 '속성 추가'로 Location 을 만들어 Bounds 와 어긋나게 만들 수 있다.
|
||||||
|
private static readonly HashSet<string> Skeleton = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
"Location", "LocationOnBase", "Size", "Name", "TabIndex", "DataBindings",
|
||||||
|
"Margin", "DrawingLocation", "Font", "LinkToNewDesign",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string[]> byType = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["Label"] = new[]
|
||||||
|
{
|
||||||
|
"visible", "PrintOutPut", "PreventEditing", "IsRequiredValue", "DisplaySequence",
|
||||||
|
"UseCompatibleTextRendering", "PrintBackColor", "IsColorLabel", "ControlContextMenu",
|
||||||
|
"SMSWebControlName", "RightToLeft", "AutoSize", "FlatStyle", "BackgroundImageLayout",
|
||||||
|
// 문서 일련번호(MSequence 계열) — 라벨에 붙어 서식 번호를 찍는다
|
||||||
|
"SequenceCode", "SequenceType", "SequenceNumber", "CheckSequenceAtSaving",
|
||||||
|
"AssociatedControl", "Format_SequenceNumber", "Format_PrefixSequenceType", "Format_DelimeterChar",
|
||||||
|
},
|
||||||
|
["Line"] = new[]
|
||||||
|
{
|
||||||
|
"Orientation", "BorderWidth", "DashStyle", "PrintOutPut", "PreventEditing",
|
||||||
|
"IsRequiredValue", "DisplaySequence", "InitialValue", "SetXPoint", "SetYPoint",
|
||||||
|
"TabStop", "ControlContextMenu", "SMSWebControlName", "Anchor", "Dock",
|
||||||
|
},
|
||||||
|
["TextBox"] = new[]
|
||||||
|
{
|
||||||
|
"PrintOutPut", "PreventEditing", "IsRequiredValue", "AutoHeight", "TogetherAutoHeight",
|
||||||
|
"ReLoadDataOnSavedSheet", "ReLoadDataOnViewMode", "ReLoadDataMsgNoCheck",
|
||||||
|
"DisplaySequence", "PrintTextFITOption", "CalendarMode", "DeleteYon", "TextChang",
|
||||||
|
"EnterTabYon", "UpperWrite", "AdpDteLink", "LineCount", "LineCountExplanation",
|
||||||
|
"ScrollBars", "Enabled", "MaxLength", "PrintBackColor", "ControlContextMenu",
|
||||||
|
"SMSWebControlName", "OnValue", "OffValue", "LinkedLabelControlName",
|
||||||
|
"AfterChangeInvokeMDataTable", "CheckDataTablePrintOut", "DataTableField",
|
||||||
|
"Same_UseMode", "Same_TextValue", "Same_SetFont", "Same_SetTextColor", "Same_SetBackColor",
|
||||||
|
},
|
||||||
|
["CheckBox"] = new[]
|
||||||
|
{
|
||||||
|
"Score", "PrintOutPut", "PreventEditing", "IsRequiredValue", "ControlVisible",
|
||||||
|
"ReLoadDataOnSavedSheet", "ReLoadDataMsgNoCheck", "DisplaySequence",
|
||||||
|
"CheckColor", "CheckForeColor", "CheckAlign", "CheckState", "TextAlign",
|
||||||
|
"UseVisualStyleBackColor", "UseCompatibleTextRendering", "PrintBackColor",
|
||||||
|
"FocusedControl_AfterClick", "ToolTipText", "Enabled", "TabStop", "AutoSize",
|
||||||
|
"FlatStyle", "ControlContextMenu", "SMSWebControlName", "DataTableField",
|
||||||
|
},
|
||||||
|
["RadioButton"] = new[]
|
||||||
|
{
|
||||||
|
"Score", "PrintOutPut", "PreventEditing", "IsRequiredValue", "ControlVisible",
|
||||||
|
"ReLoadDataOnSavedSheet", "ReLoadDataMsgNoCheck", "DisplaySequence",
|
||||||
|
"CheckColor", "CheckForeColor", "CheckAlign", "TextAlign", "TabStop",
|
||||||
|
"UseVisualStyleBackColor", "UseCompatibleTextRendering",
|
||||||
|
"FocusedControl_AfterClick", "LinkedExpandablePanel_ToExpand", "LinkedExpandablePanel_ToClose",
|
||||||
|
"Enabled", "AutoSize", "FlatStyle", "ControlContextMenu", "SMSWebControlName", "DataTableField",
|
||||||
|
},
|
||||||
|
["ComboBox"] = new[]
|
||||||
|
{
|
||||||
|
"ItemScore", "PrintOutPut", "PreventEditing", "IsRequiredValue", "DisplaySequence",
|
||||||
|
"InitialValueDataTable", "DataTableItms", "DataTableField", "EventHandlerMappingTag",
|
||||||
|
"FormattingEnabled", "DropDownStyle", "FlatStyle", "AutoCompleteCustomSource",
|
||||||
|
"ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
["CalcBox"] = new[]
|
||||||
|
{
|
||||||
|
"PrintOutPut", "PreventEditing", "IsRequiredValue", "Visible", "DisplaySequence",
|
||||||
|
"ReLoadDataOnSavedSheet", "ReLoadDataMsgNoCheck", "TimeMode", "QueryMode",
|
||||||
|
"ZeroScore_Blank", "FormulaResultCheck", "CalendarMode", "EnterTabYon",
|
||||||
|
"Multiline", "MaxLength", "ReadOnly", "DataTableField",
|
||||||
|
"ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
["PictureBox"] = new[]
|
||||||
|
{
|
||||||
|
"IsSignature", "SignatureIndex", "PrintOutPut", "PreventEditing", "IsRequiredValue",
|
||||||
|
"ReLoadDataOnSavedSheet", "ReLoadDataOnViewMode", "ReLoadDataMsgNoCheck",
|
||||||
|
"DisplaySequence", "CoordinateYon", "NoImageSave", "IsTemplate",
|
||||||
|
"VisibleText", "TextFont", "DataTableField", "Enabled", "TabStop", "Cursor",
|
||||||
|
"ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
["DateTimePicker"] = new[]
|
||||||
|
{
|
||||||
|
"PrintOutPut", "PreventEditing", "IsRequiredValue", "DisplaySequence",
|
||||||
|
"ReLoadDataOnSavedSheet", "ReLoadDataOnViewMode", "ReLoadDataMsgNoCheck",
|
||||||
|
"TimePrint", "DayPrint", "BringEmdTime", "ShowUpDown", "AdpDteLink",
|
||||||
|
"DataTableField", "TabStop", "CalendarFont", "CalendarMonthBackground",
|
||||||
|
"CalendarTitleBackColor", "CalendarTitleForeColor", "CalendarForeColor",
|
||||||
|
"CalendarTrailingForeColor", "ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
["MaskedTextBox"] = new[]
|
||||||
|
{
|
||||||
|
"PrintOutPut", "PreventEditing", "IsRequiredValue", "AutoHeight", "DisplaySequence",
|
||||||
|
"ReLoadDataOnSavedSheet", "ReLoadDataOnViewMode", "ReLoadDataMsgNoCheck",
|
||||||
|
"CalendarMode", "TimePadMode", "ValidatingType", "PrintTextFITOption",
|
||||||
|
"DeleteYon", "TextChang", "AdpDteLink", "AfterChangeInvokeMDataTable",
|
||||||
|
"DataTableField", "PrintBackColor", "Enabled", "ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
["Panel"] = new[]
|
||||||
|
{
|
||||||
|
"PrintOutPut", "PreventEditing", "IsRequiredValue", "Visible", "DisplaySequence",
|
||||||
|
"PrintBackColor", "BackgroundImageLayout", "Dock", "TabStop", "AutoHeight",
|
||||||
|
// 접이식 패널(MExpandablePanel) 계열
|
||||||
|
"HeaderText", "TitleHeight", "ExpandHeight", "IsExpanded", "ExpandButtonPosition", "InitiallyClose",
|
||||||
|
"ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
["GroupBox"] = new[]
|
||||||
|
{
|
||||||
|
"PrintOutPut", "PreventEditing", "IsRequiredValue", "Visible", "DisplaySequence",
|
||||||
|
"UseCompatibleTextRendering", "TabStop", "ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
["Button"] = new[]
|
||||||
|
{
|
||||||
|
"PreventEditing", "IsRequiredValue", "Visible", "DisplaySequence", "InitialValue",
|
||||||
|
"EventHandlerMappingTag", "GetDataActionTagControlChange", "CalendarMode",
|
||||||
|
"UseVisualStyleBackColor", "UseCompatibleTextRendering", "TextAlign", "ImageAlign",
|
||||||
|
"BackgroundImage", "BackgroundImageLayout", "FlatStyle", "Cursor",
|
||||||
|
"ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
["DataTable"] = new[]
|
||||||
|
{
|
||||||
|
"PreventEditing", "IsRequiredValue", "Visible", "DisplaySequence", "InitialValue",
|
||||||
|
"BackgroundImage", "BackgroundImageLayout", "ControlContextMenu", "SMSWebControlName",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
/// <summary>
|
||||||
|
/// 해당 타입에서 쓰이는 속성 키 제안 — 이미 있는 키와 인스펙터가 이미 다루는 키는 뺀다.
|
||||||
|
/// 알 수 없는 타입이면 빈 목록(제안 없이 자유 입력).
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<string> SuggestFor(string type, IEnumerable<string> existingKeys)
|
||||||
|
{
|
||||||
|
if (!byType.TryGetValue(type, out var keys))
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
var taken = new HashSet<string>(existingKeys, StringComparer.Ordinal);
|
||||||
|
var curated = ControlRegistry.Find(type)?.Properties.Select(p => p.Key) ?? Enumerable.Empty<string>();
|
||||||
|
foreach (var key in curated)
|
||||||
|
{
|
||||||
|
taken.Add(key);
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
.Where(k => !taken.Contains(k) && !Skeleton.Contains(k))
|
||||||
|
.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>타입이 제안 목록을 갖고 있는지(진단용)</summary>
|
||||||
|
public static bool Knows(string type) => byType.ContainsKey(type);
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using SheetMe.Core.Models;
|
||||||
|
|
||||||
|
namespace SheetMe.Core.Catalog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 선(MLine)의 모양 규칙 — 레거시 컨트롤이 스스로 강제하는 불변식을 그대로 옮긴 것.
|
||||||
|
///
|
||||||
|
/// 레거시 MLine 은 크기를 자유롭게 두지 않는다. <c>OnResize</c> 가 매번 두께 축을 BorderWidth 로 되돌리고
|
||||||
|
/// (MLine.vb: Horizontal 이면 <c>Me.Height = miBorderWidth</c>, Vertical 이면 <c>Me.Width = miBorderWidth</c>),
|
||||||
|
/// Orientation 세터는 길이를 보존한 채 축을 바꾼다(<c>Horizontal → Me.Width = Me.Height</c>, 반대도 대칭).
|
||||||
|
/// 그래서 레거시 디자이너에서는 가로선을 세로로 '늘려서' 만들 수 없다 — 늘리면 즉시 되돌아온다.
|
||||||
|
///
|
||||||
|
/// 이 규칙을 옮기지 않으면 디자이너와 EMR 이 서로 다른 그림을 보여 준다. 폭 1·높이 210 으로
|
||||||
|
/// 세로처럼 만든 선은 우리 캔버스에선 세로선이지만, 저장 XML 에 Orientation 이 없으므로
|
||||||
|
/// 런타임은 Horizontal 로 보고 (0,0)→(1,0) 즉 1px 점을 그린다 — 기록지에서 선이 사라진다.
|
||||||
|
///
|
||||||
|
/// 운영 데이터 32,599개 선에서 경계 모양과 Orientation 이 어긋나는 건 0건이다(--db-lines).
|
||||||
|
/// 레거시가 구조적으로 막아 왔다는 뜻이고, 우리도 같은 방식으로 막는다.
|
||||||
|
/// </summary>
|
||||||
|
public static class LineGeometry
|
||||||
|
{
|
||||||
|
#region Methods
|
||||||
|
/// <summary>대상이 선인지</summary>
|
||||||
|
public static bool IsLine(ControlElement element)
|
||||||
|
=> string.Equals(element.Type, "Line", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Orientation 이 Vertical 인지 — 키가 없으면 Horizontal(MLine.vb 필드 초기값)</summary>
|
||||||
|
public static bool IsVertical(ControlElement element)
|
||||||
|
=> string.Equals(element.Props.GetText("Orientation"), "Vertical", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>선 두께 — BorderWidth, 키가 없으면 1(MLine.vb 생성자 miBorderWidth = 1)</summary>
|
||||||
|
public static double ThicknessOf(ControlElement element)
|
||||||
|
{
|
||||||
|
var text = element.Props.GetText("BorderWidth");
|
||||||
|
if (text is not null
|
||||||
|
&& double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
|
||||||
|
&& value >= 1)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 두께 축을 BorderWidth 로 맞춘다(레거시 OnResize 이식) — 바뀌었으면 true.
|
||||||
|
///
|
||||||
|
/// 길이 축은 건드리지 않는다. 사용자가 끄는 것은 길이이고, 두께는 속성이 정한다.
|
||||||
|
/// </summary>
|
||||||
|
public static bool NormalizeThickness(ControlElement element)
|
||||||
|
{
|
||||||
|
if (!IsLine(element))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var thickness = ThicknessOf(element);
|
||||||
|
if (IsVertical(element))
|
||||||
|
{
|
||||||
|
if (Math.Abs(element.Bounds.W - thickness) < 0.001)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
element.Bounds.W = thickness;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (Math.Abs(element.Bounds.H - thickness) < 0.001)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
element.Bounds.H = thickness;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 방향 전환 — 길이를 보존한 채 축을 바꾼다(레거시 Orientation 세터 이식).
|
||||||
|
///
|
||||||
|
/// 레거시는 <c>Me.Width = Me.Height</c> (또는 반대)로 길이를 옮긴 뒤 OnResize 가 두께를 되돌린다.
|
||||||
|
/// 두 단계를 합치면 '가로 200×1 ↔ 세로 1×200' 이 된다.
|
||||||
|
/// </summary>
|
||||||
|
public static void ApplyOrientation(ControlElement element, bool vertical)
|
||||||
|
{
|
||||||
|
if (!IsLine(element))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var thickness = ThicknessOf(element);
|
||||||
|
// 전환 전 길이 = 지금 방향의 긴 축
|
||||||
|
var length = IsVertical(element) ? element.Bounds.H : element.Bounds.W;
|
||||||
|
if (vertical)
|
||||||
|
{
|
||||||
|
element.Props.SetText("Orientation", "Vertical");
|
||||||
|
element.Bounds.W = thickness;
|
||||||
|
element.Bounds.H = length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Horizontal 은 MLine 의 기본값이라 레거시 직렬화기도 키를 생략한다
|
||||||
|
// (<DefaultValue(Orientation.Horizontal)>) — 우리도 지워서 원문 모양을 맞춘다.
|
||||||
|
element.Props.Remove("Orientation");
|
||||||
|
element.Bounds.W = length;
|
||||||
|
element.Bounds.H = thickness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -755,6 +755,71 @@ public static class EditSmoke
|
|||||||
designer.Selection.Clear();
|
designer.Selection.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 20-5e) 선 기하 — 레거시 MLine 은 두께 축을 BorderWidth 로 강제하고,
|
||||||
|
// 방향 전환은 길이를 보존한 채 축을 바꾼다. 이걸 안 지키면 디자이너와 EMR 이 다른 그림을 그린다.
|
||||||
|
{
|
||||||
|
var beforeLine = designer.Pages[0].Controls.ToList();
|
||||||
|
designer.AddControlAt("Line", new Point(60, 300));
|
||||||
|
var line = designer.Pages[0].Controls.Except(beforeLine).Single();
|
||||||
|
Check("선: 기본은 가로 200×1", Math.Abs(line.Width - 200) < 0.5 && Math.Abs(line.Height - 1) < 0.5,
|
||||||
|
$"{line.Width}×{line.Height}");
|
||||||
|
Check("선: 기본에는 Orientation 키가 없다(레거시 DefaultValue 와 동일)",
|
||||||
|
line.Model.Props.GetText("Orientation") is null);
|
||||||
|
|
||||||
|
// 세로로 끌어 늘려도 두께는 되돌아온다 — '세로처럼 보이지만 EMR 에선 점' 상태를 만들 수 없다
|
||||||
|
line.Height = 210;
|
||||||
|
SheetMe.Core.Catalog.LineGeometry.NormalizeThickness(line.Model);
|
||||||
|
Check("선: 두께는 끌어서 못 바꾼다", Math.Abs(line.Height - 1) < 0.5, $"높이 {line.Height}");
|
||||||
|
|
||||||
|
// 방향 전환 — 길이 보존
|
||||||
|
SheetMe.Core.Catalog.LineGeometry.ApplyOrientation(line.Model, vertical: true);
|
||||||
|
Check("선: 세로 전환은 길이를 보존", Math.Abs(line.Model.Bounds.W - 1) < 0.5
|
||||||
|
&& Math.Abs(line.Model.Bounds.H - 200) < 0.5,
|
||||||
|
$"{line.Model.Bounds.W}×{line.Model.Bounds.H}");
|
||||||
|
Check("선: 세로면 Orientation=Vertical 이 기록된다",
|
||||||
|
line.Model.Props.GetText("Orientation") == "Vertical");
|
||||||
|
|
||||||
|
// 굵기를 키우면 두께 축만 따라간다
|
||||||
|
line.Model.Props.SetText("BorderWidth", "3");
|
||||||
|
SheetMe.Core.Catalog.LineGeometry.NormalizeThickness(line.Model);
|
||||||
|
Check("선: 굵기는 두께 축만 바꾼다", Math.Abs(line.Model.Bounds.W - 3) < 0.5
|
||||||
|
&& Math.Abs(line.Model.Bounds.H - 200) < 0.5,
|
||||||
|
$"{line.Model.Bounds.W}×{line.Model.Bounds.H}");
|
||||||
|
|
||||||
|
// 가로로 되돌리면 Orientation 키가 지워진다(레거시 직렬화기가 기본값을 생략하는 것과 같다)
|
||||||
|
SheetMe.Core.Catalog.LineGeometry.ApplyOrientation(line.Model, vertical: false);
|
||||||
|
Check("선: 가로로 되돌리면 Orientation 키가 지워진다",
|
||||||
|
line.Model.Props.GetText("Orientation") is null
|
||||||
|
&& Math.Abs(line.Model.Bounds.W - 200) < 0.5
|
||||||
|
&& Math.Abs(line.Model.Bounds.H - 3) < 0.5,
|
||||||
|
$"{line.Model.Bounds.W}×{line.Model.Bounds.H}");
|
||||||
|
|
||||||
|
designer.Selection.SetSingle(line);
|
||||||
|
designer.DeleteSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20-5f) 선택 밖 컨트롤을 Ctrl+드래그하면 그것이 끌려야 한다(기존 선택은 유지)
|
||||||
|
{
|
||||||
|
var anchor = Find("CheckBox");
|
||||||
|
var grabbed = Find("Label");
|
||||||
|
designer.Selection.SetSingle(anchor);
|
||||||
|
var anchorAt = new Point(anchor.X, anchor.Y);
|
||||||
|
var box = designer.WorldBoundsOf(grabbed);
|
||||||
|
var from = new Point(box.X + box.Width / 2, box.Y + box.Height / 2);
|
||||||
|
var grabbedX = grabbed.X;
|
||||||
|
// Alt 로 정렬 스냅을 끈다 — 스냅이 걸리면 이동량이 가이드에 붙어 델타 단언이 흔들린다
|
||||||
|
var ctrlAlt = new PointerContext(true, false, true, 1);
|
||||||
|
designer.Interaction.PointerDown(from, ctrlAlt);
|
||||||
|
designer.Interaction.PointerMove(new Point(from.X + 30, from.Y), ctrlAlt);
|
||||||
|
designer.Interaction.PointerUp(new Point(from.X + 30, from.Y), ctrlAlt);
|
||||||
|
Check("Ctrl+드래그: 잡은 컨트롤이 움직인다", Math.Abs(grabbed.X - (grabbedX + 30)) < 2,
|
||||||
|
$"{grabbedX} → {grabbed.X}");
|
||||||
|
Check("Ctrl+드래그: 기존 선택도 함께 움직인다(합집합)", Math.Abs(anchor.X - (anchorAt.X + 30)) < 2,
|
||||||
|
$"{anchorAt.X} → {anchor.X}");
|
||||||
|
designer.Undo.Undo();
|
||||||
|
designer.Selection.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
// 20-5d) 태그 카탈로그 — 레거시에서 주석 처리된 태그를 고를 수 있으면 안 된다
|
// 20-5d) 태그 카탈로그 — 레거시에서 주석 처리된 태그를 고를 수 있으면 안 된다
|
||||||
Check("태그 카탈로그: 미구현 3종 제외",
|
Check("태그 카탈로그: 미구현 3종 제외",
|
||||||
!SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_BMI")
|
!SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_BMI")
|
||||||
@@ -779,6 +844,22 @@ public static class EditSmoke
|
|||||||
var addRow = designer.Inspector.Rows
|
var addRow = designer.Inspector.Rows
|
||||||
.OfType<ViewModels.Inspector.AddPropertyRowViewModel>().FirstOrDefault();
|
.OfType<ViewModels.Inspector.AddPropertyRowViewModel>().FirstOrDefault();
|
||||||
Check("고급: 속성 추가 행 존재", addRow is not null);
|
Check("고급: 속성 추가 행 존재", addRow is not null);
|
||||||
|
// 자동완성 — 타입별 유효 키를 제안한다(철자를 외워 맞히지 않도록)
|
||||||
|
Check("고급: 제안 목록 존재", addRow!.Suggestions.Count > 0, $"{addRow.Suggestions.Count}개");
|
||||||
|
Check("고급: 이미 있는 키와 큐레이션된 키는 제안에서 빠진다",
|
||||||
|
!addRow.Suggestions.Contains("AutoHeight") && !addRow.Suggestions.Contains("Text"),
|
||||||
|
"AutoHeight/Text 는 TextBox 인스펙터가 이미 다룬다");
|
||||||
|
addRow.KeyText = "Print";
|
||||||
|
Check("고급: 부분 일치로 좁혀진다", addRow.Matches.Count > 0
|
||||||
|
&& addRow.Matches.All(m => m.Contains("Print", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
string.Join(",", addRow.Matches));
|
||||||
|
Check("고급: 접두 일치가 앞에 온다",
|
||||||
|
addRow.Matches[0].StartsWith("Print", StringComparison.OrdinalIgnoreCase), addRow.Matches[0]);
|
||||||
|
addRow.KeyText = string.Empty;
|
||||||
|
// 라벨의 소문자 visible — 대문자로 쓰면 라벨이 그대로 보인다. 제안이 이 함정을 덮어야 한다.
|
||||||
|
var labelSuggest = SheetMe.Core.Catalog.LegacyPropertyCatalog.SuggestFor("Label", Array.Empty<string>());
|
||||||
|
Check("고급: 라벨 제안에는 대문자 Visible 이 없다",
|
||||||
|
!labelSuggest.Contains("Visible"), string.Join(",", labelSuggest.Where(s => s.Contains("isible"))));
|
||||||
addRow!.KeyText = "EnterTabYon";
|
addRow!.KeyText = "EnterTabYon";
|
||||||
addRow.AddCommand!.Execute(null, EventArgs.Empty);
|
addRow.AddCommand!.Execute(null, EventArgs.Empty);
|
||||||
Check("고급: 속성 추가됨", advTarget.Model.Props.Contains("EnterTabYon"));
|
Check("고급: 속성 추가됨", advTarget.Model.Props.Contains("EnterTabYon"));
|
||||||
|
|||||||
@@ -134,6 +134,14 @@ public sealed class InteractionController
|
|||||||
mode = Mode.Move;
|
mode = Mode.Move;
|
||||||
toggleOnUp = false;
|
toggleOnUp = false;
|
||||||
collapseOnUp = false;
|
collapseOnUp = false;
|
||||||
|
// Ctrl/Shift 클릭은 선택을 바꾸지 않고 업에서 토글한다(:97). 그 상태로 드래그가 시작되면
|
||||||
|
// 잡은 컨트롤이 아니라 기존 선택분이 끌려간다 — 선택이 비어 있으면 아무것도 안 끌린다.
|
||||||
|
// 드래그가 실제로 시작되는 이 지점에서 잡은 것을 선택에 넣는다(기존 다중선택은 유지 — 업 토글과 같은 규칙).
|
||||||
|
if (!downControl.IsSelected)
|
||||||
|
{
|
||||||
|
var mates = designer.GroupMatesOf(downControl);
|
||||||
|
designer.Selection.Set(designer.Selection.Items.Union(mates).ToList(), downControl);
|
||||||
|
}
|
||||||
BeginMove();
|
BeginMove();
|
||||||
DoMove(world, ctx); // 전환 프레임부터 즉시 추종
|
DoMove(world, ctx); // 전환 프레임부터 즉시 추종
|
||||||
}
|
}
|
||||||
@@ -358,6 +366,12 @@ public sealed class InteractionController
|
|||||||
vm.Y = Math.Round(newRect.Y + (orig.Y - groupBounds0.Y) * sy - offset.Y);
|
vm.Y = Math.Round(newRect.Y + (orig.Y - groupBounds0.Y) * sy - offset.Y);
|
||||||
vm.Width = Math.Max(1, Math.Round(orig.Width * sx));
|
vm.Width = Math.Max(1, Math.Round(orig.Width * sx));
|
||||||
vm.Height = Math.Max(1, Math.Round(orig.Height * sy));
|
vm.Height = Math.Max(1, Math.Round(orig.Height * sy));
|
||||||
|
// 선은 두께를 끌어서 바꿀 수 없다 — 레거시 MLine.OnResize 가 매번 BorderWidth 로 되돌린다.
|
||||||
|
// 이걸 허용하면 세로로 늘린 가로선이 디자이너에만 보이고 EMR 에서는 1px 점이 된다.
|
||||||
|
if (SheetMe.Core.Catalog.LineGeometry.NormalizeThickness(vm.Model))
|
||||||
|
{
|
||||||
|
vm.NotifyAllChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
designer.Selection.NotifyBoundsChanged();
|
designer.Selection.NotifyBoundsChanged();
|
||||||
|
|||||||
@@ -131,6 +131,35 @@
|
|||||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}" />
|
<Setter Property="Foreground" Value="{DynamicResource B.Muted}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- 제안 칩: 눌러서 고르는 짧은 토큰(속성 키 자동완성). 기본 Button 보다 작고 촘촘하다 -->
|
||||||
|
<Style x:Key="SuggestChip" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource B.Ink}" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource B.Input}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource B.InputBorder}" />
|
||||||
|
<Setter Property="FontFamily" Value="Consolas" />
|
||||||
|
<Setter Property="FontSize" Value="11" />
|
||||||
|
<Setter Property="Padding" Value="6,2" />
|
||||||
|
<Setter Property="Margin" Value="0,0,4,4" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="chip" CornerRadius="6" Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="chip" Property="Background" Value="{DynamicResource B.Hover}" />
|
||||||
|
<Setter TargetName="chip" Property="BorderBrush" Value="{DynamicResource B.Accent}" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- ===== 텍스트박스 ===== -->
|
<!-- ===== 텍스트박스 ===== -->
|
||||||
<!-- Framer 식 입력칩: 채움만(평소 테두리 없음)·라운드8·높이30·세로 가운데 -->
|
<!-- Framer 식 입력칩: 채움만(평소 테두리 없음)·라운드8·높이30·세로 가운데 -->
|
||||||
<Style TargetType="TextBox">
|
<Style TargetType="TextBox">
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ public static class InspectorTabCatalog
|
|||||||
["Format"] = InspectorTab.Design, // 날짜 표시 형식
|
["Format"] = InspectorTab.Design, // 날짜 표시 형식
|
||||||
["CustomFormat"] = InspectorTab.Design,
|
["CustomFormat"] = InspectorTab.Design,
|
||||||
["Multiline"] = InspectorTab.Design, // 여러 줄로 '보이는가'
|
["Multiline"] = InspectorTab.Design, // 여러 줄로 '보이는가'
|
||||||
|
["AutoHeight"] = InspectorTab.Design, // 내용에 맞춰 높이가 늘어나는가
|
||||||
|
// 표시 여부는 '어떻게 보이는가'의 일부다. 라벨만 소문자 키를 쓴다(Label.vb 의 Shadows 속성).
|
||||||
|
["Visible"] = InspectorTab.Design,
|
||||||
|
["visible"] = InspectorTab.Design,
|
||||||
|
// 화면이 아니라 종이에 나오는가 — 성격은 표시 여부와 같은 축이다
|
||||||
|
["PrintOutPut"] = InspectorTab.Design,
|
||||||
|
// 선 3종
|
||||||
|
["Orientation"] = InspectorTab.Design,
|
||||||
|
["BorderWidth"] = InspectorTab.Design,
|
||||||
|
["DashStyle"] = InspectorTab.Design,
|
||||||
|
|
||||||
// ── 데이터: 무슨 값이 채워지는가 ──
|
// ── 데이터: 무슨 값이 채워지는가 ──
|
||||||
["DataInterfaceTag"] = InspectorTab.Data, // 자동 채움 원천
|
["DataInterfaceTag"] = InspectorTab.Data, // 자동 채움 원천
|
||||||
@@ -53,7 +63,14 @@ public static class InspectorTabCatalog
|
|||||||
["Items"] = InspectorTab.Data, // 고를 수 있는 값 목록
|
["Items"] = InspectorTab.Data, // 고를 수 있는 값 목록
|
||||||
["Checked"] = InspectorTab.Data, // 기본 체크 = 초기값
|
["Checked"] = InspectorTab.Data, // 기본 체크 = 초기값
|
||||||
["Score"] = InspectorTab.Data, // 설문 배점
|
["Score"] = InspectorTab.Data, // 설문 배점
|
||||||
|
["ItemScore"] = InspectorTab.Data, // 콤보 항목별 배점("/" 구분, 항목 순서와 1:1)
|
||||||
["Formula"] = InspectorTab.Data, // 다른 컨트롤의 Score 를 합산해 이 칸의 값을 만든다
|
["Formula"] = InspectorTab.Data, // 다른 컨트롤의 Score 를 합산해 이 칸의 값을 만든다
|
||||||
|
["IsSignature"] = InspectorTab.Data, // 이 칸에 채워지는 것이 서명 이미지인가
|
||||||
|
["SignatureIndex"] = InspectorTab.Data,
|
||||||
|
// 저장된 기록을 다시 열 때 값을 어디서 가져오는가 — 저장된 값인가, 지금 조회한 값인가
|
||||||
|
["ReLoadDataOnSavedSheet"] = InspectorTab.Data,
|
||||||
|
["ReLoadDataOnViewMode"] = InspectorTab.Data,
|
||||||
|
["ReLoadDataMsgNoCheck"] = InspectorTab.Data,
|
||||||
|
|
||||||
// ── 동작: 조작하면 무슨 일이 일어나는가 ──
|
// ── 동작: 조작하면 무슨 일이 일어나는가 ──
|
||||||
["DataActionTag"] = InspectorTab.Behavior,
|
["DataActionTag"] = InspectorTab.Behavior,
|
||||||
@@ -65,6 +82,8 @@ public static class InspectorTabCatalog
|
|||||||
// RwdOrderAutYon 은 상용구를 고른 결과로 OCS 처방을 전송하는 게이트다.
|
// RwdOrderAutYon 은 상용구를 고른 결과로 OCS 처방을 전송하는 게이트다.
|
||||||
["RwdRsvWrdYon"] = InspectorTab.Behavior,
|
["RwdRsvWrdYon"] = InspectorTab.Behavior,
|
||||||
["RwdOrderAutYon"] = InspectorTab.Behavior,
|
["RwdOrderAutYon"] = InspectorTab.Behavior,
|
||||||
|
// 읽기 전용은 '입력을 받는가'라서 동작이다(TextBox 에서는 곧 ReadOnly 다)
|
||||||
|
["PreventEditing"] = InspectorTab.Behavior,
|
||||||
};
|
};
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
@@ -527,7 +527,9 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 속성 추가 — 레거시 컨트롤의 임의 속성 지정(오타 주의: 로더가 모르는 키는 경고 처리)
|
// 속성 추가 — 타입별 실사용 키를 제안한다(자유 입력도 그대로 열어 둔다).
|
||||||
|
// 제안이 없으면 사용자가 철자를 외워 맞혀야 하고, 틀려도 경고가 없다 —
|
||||||
|
// 라벨의 소문자 visible, PrintOutPut 의 대문자 P 같은 함정이 실재한다.
|
||||||
AddPlain(new AddPropertyRowViewModel(key =>
|
AddPlain(new AddPropertyRowViewModel(key =>
|
||||||
{
|
{
|
||||||
if (target.Model.Props.Contains(key))
|
if (target.Model.Props.Contains(key))
|
||||||
@@ -538,7 +540,7 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
designer.Undo.Snapshot();
|
designer.Undo.Snapshot();
|
||||||
target.Model.Props.SetText(key, string.Empty);
|
target.Model.Props.SetText(key, string.Empty);
|
||||||
Rebuild();
|
Rebuild();
|
||||||
}));
|
}, LegacyPropertyCatalog.SuggestFor(target.Type, target.Model.Props.Keys)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDefRow(PropertyDef def)
|
private void AddDefRow(PropertyDef def)
|
||||||
@@ -566,6 +568,13 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
Get = vm => vm.Model.Props.GetText(def.Key) ?? def.Default,
|
Get = vm => vm.Model.Props.GetText(def.Key) ?? def.Default,
|
||||||
Set = (vm, value) =>
|
Set = (vm, value) =>
|
||||||
{
|
{
|
||||||
|
// 선의 방향·굵기는 값만 바꿔서는 안 된다 — 레거시 컨트롤이 크기까지 함께 정한다.
|
||||||
|
// 값만 쓰면 디자이너는 가로선을 보여 주는데 EMR 은 세로로 그리는 어긋남이 생긴다.
|
||||||
|
if (LineGeometry.IsLine(vm.Model) && def.Key is "Orientation" or "BorderWidth")
|
||||||
|
{
|
||||||
|
ApplyLineChange(vm, def.Key, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (value.Length == 0)
|
if (value.Length == 0)
|
||||||
{
|
{
|
||||||
vm.Model.Props.Remove(def.Key);
|
vm.Model.Props.Remove(def.Key);
|
||||||
@@ -598,6 +607,33 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
AddRow(row, binding);
|
AddRow(row, binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 선의 방향·굵기 변경 — 속성과 크기를 함께 맞춘다(레거시 MLine 의 세터/OnResize 이식).
|
||||||
|
///
|
||||||
|
/// 굵기를 바꾸면 두께 축만 따라가고 길이는 그대로다. 방향을 바꾸면 길이를 보존한 채 축이 바뀐다.
|
||||||
|
/// 크기가 바뀌므로 뷰모델 경계도 다시 읽혀야 한다(NotifyBoundsChanged).
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyLineChange(ControlViewModel vm, string key, string value)
|
||||||
|
{
|
||||||
|
if (key == "Orientation")
|
||||||
|
{
|
||||||
|
LineGeometry.ApplyOrientation(vm.Model, string.Equals(value, "Vertical", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (value.Length == 0)
|
||||||
|
{
|
||||||
|
vm.Model.Props.Remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
vm.Model.Props.SetText(key, value);
|
||||||
|
}
|
||||||
|
LineGeometry.NormalizeThickness(vm.Model);
|
||||||
|
}
|
||||||
|
vm.NotifyAllChanged();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 경계 값 두 개를 한 줄에 — X|Y, 너비|높이.
|
/// 경계 값 두 개를 한 줄에 — X|Y, 너비|높이.
|
||||||
/// 자식 행은 boundsRows 에 그대로 등록되므로 드래그·리사이즈 중 실시간 갱신
|
/// 자식 행은 boundsRows 에 그대로 등록되므로 드래그·리사이즈 중 실시간 갱신
|
||||||
|
|||||||
@@ -661,7 +661,12 @@ public sealed class ToggleAdvancedRowViewModel : PropertyRowViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>속성 추가 행 — 키 입력 후 빈 속성 생성(고급)</summary>
|
/// <summary>
|
||||||
|
/// 속성 추가 행 — 키 입력 후 빈 속성 생성(고급).
|
||||||
|
///
|
||||||
|
/// 제안 목록은 <see cref="SheetMe.Core.Catalog.LegacyPropertyCatalog"/> 가 타입별로 준다.
|
||||||
|
/// 자유 입력은 그대로 열어 둔다 — 목록은 제안이지 검증이 아니고, 사이트가 추가한 키도 있다.
|
||||||
|
/// </summary>
|
||||||
public sealed class AddPropertyRowViewModel : PropertyRowViewModel
|
public sealed class AddPropertyRowViewModel : PropertyRowViewModel
|
||||||
{
|
{
|
||||||
private string keyText = string.Empty;
|
private string keyText = string.Empty;
|
||||||
@@ -670,14 +675,66 @@ public sealed class AddPropertyRowViewModel : PropertyRowViewModel
|
|||||||
public string KeyText
|
public string KeyText
|
||||||
{
|
{
|
||||||
get => keyText;
|
get => keyText;
|
||||||
set => SetProperty(ref keyText, value);
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref keyText, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Matches));
|
||||||
|
OnPropertyChanged(nameof(HasMatches));
|
||||||
|
OnPropertyChanged(nameof(MoreCount));
|
||||||
|
OnPropertyChanged(nameof(MoreText));
|
||||||
|
OnPropertyChanged(nameof(HasMore));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>이 타입에서 쓰이는 속성 키 전체(실DB 집계 기반)</summary>
|
||||||
|
public IReadOnlyList<string> Suggestions { get; }
|
||||||
|
|
||||||
|
/// <summary>한 번에 보여 주는 제안 수 — 인스펙터는 좁아서 더 깔면 목록이 화면을 먹는다</summary>
|
||||||
|
private const int MaxShown = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 입력 중인 글자로 좁힌 제안 — 빈 입력이면 전체에서 앞쪽 몇 개를 보여 준다.
|
||||||
|
/// 부분 일치(Contains)까지 받는다: 사용자가 'Signature' 만 기억하고 'IsSignature' 는 모를 수 있다.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> Matches => Filtered().Take(MaxShown).ToList();
|
||||||
|
|
||||||
|
/// <summary>보여 주지 못하고 남은 제안 수(0 이면 표시 안 함)</summary>
|
||||||
|
public int MoreCount => Math.Max(0, Filtered().Count() - MaxShown);
|
||||||
|
|
||||||
|
/// <summary>남은 제안 안내 문구</summary>
|
||||||
|
public string MoreText => MoreCount > 0 ? $"+{MoreCount}개 — 더 입력해 좁히세요" : string.Empty;
|
||||||
|
|
||||||
|
/// <summary>가려진 제안이 있는지</summary>
|
||||||
|
public bool HasMore => MoreCount > 0;
|
||||||
|
|
||||||
|
/// <summary>보여 줄 제안이 있는지</summary>
|
||||||
|
public bool HasMatches => Filtered().Any();
|
||||||
|
|
||||||
|
private IEnumerable<string> Filtered()
|
||||||
|
{
|
||||||
|
var query = keyText.Trim();
|
||||||
|
if (query.Length == 0)
|
||||||
|
{
|
||||||
|
return Suggestions;
|
||||||
|
}
|
||||||
|
return Suggestions
|
||||||
|
.Where(s => s.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
// 접두 일치를 앞에 — 'Print' 를 치면 PrintOutPut 이 PrintBackColor 보다 먼저 와야 자연스럽다
|
||||||
|
.OrderByDescending(s => s.StartsWith(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ThenBy(s => s, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>추가 실행</summary>
|
/// <summary>추가 실행</summary>
|
||||||
public M.Framework.WPF.ICustomCommand? AddCommand { get; set; }
|
public M.Framework.WPF.ICustomCommand? AddCommand { get; set; }
|
||||||
|
|
||||||
public AddPropertyRowViewModel(Action<string> add) : base(string.Empty)
|
/// <summary>제안 선택 — 키를 채워 넣고 바로 추가한다</summary>
|
||||||
|
public M.Framework.WPF.ICustomCommand? PickCommand { get; set; }
|
||||||
|
|
||||||
|
public AddPropertyRowViewModel(Action<string> add, IReadOnlyList<string>? suggestions = null) : base(string.Empty)
|
||||||
{
|
{
|
||||||
|
Suggestions = suggestions ?? Array.Empty<string>();
|
||||||
AddCommand = new M.Framework.WPF.Command((sender, e) =>
|
AddCommand = new M.Framework.WPF.Command((sender, e) =>
|
||||||
{
|
{
|
||||||
var key = KeyText.Trim();
|
var key = KeyText.Trim();
|
||||||
@@ -686,6 +743,13 @@ public sealed class AddPropertyRowViewModel : PropertyRowViewModel
|
|||||||
add(key);
|
add(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
PickCommand = new M.Framework.WPF.Command((object parameter) =>
|
||||||
|
{
|
||||||
|
if (parameter is string picked && picked.Length > 0)
|
||||||
|
{
|
||||||
|
add(picked);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -382,13 +382,37 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate DataType="{x:Type ins:AddPropertyRowViewModel}">
|
<DataTemplate DataType="{x:Type ins:AddPropertyRowViewModel}">
|
||||||
<DockPanel Margin="0,6,0,2">
|
<StackPanel Margin="0,6,0,2">
|
||||||
<Button DockPanel.Dock="Right" Content="추가" Padding="8,2" Margin="4,0,0,0"
|
<DockPanel>
|
||||||
Command="{Binding AddCommand}"
|
<Button DockPanel.Dock="Right" Content="추가" Padding="8,2" Margin="4,0,0,0"
|
||||||
ToolTip="레거시 컨트롤의 속성 이름을 정확히 입력하세요 (예: EnterTabYon, AutoHeight)"/>
|
Command="{Binding AddCommand}"
|
||||||
<TextBox Text="{Binding KeyText, UpdateSourceTrigger=PropertyChanged}" FontSize="12"
|
ToolTip="레거시 컨트롤의 속성 이름을 정확히 입력하세요 (예: EnterTabYon, AutoHeight)"/>
|
||||||
ToolTip="새 속성 키 입력"/>
|
<TextBox Text="{Binding KeyText, UpdateSourceTrigger=PropertyChanged}" FontSize="12"
|
||||||
</DockPanel>
|
ToolTip="새 속성 키 입력 — 아래 제안을 눌러도 됩니다"/>
|
||||||
|
</DockPanel>
|
||||||
|
<!-- 이 타입에서 실제로 쓰이는 키 제안(운영 DB 집계). 누르면 바로 추가된다.
|
||||||
|
철자를 외워 맞히게 두면 라벨의 소문자 visible 같은 함정에서 조용히 틀린다. -->
|
||||||
|
<ItemsControl ItemsSource="{Binding Matches}" Margin="0,6,0,0"
|
||||||
|
Visibility="{Binding HasMatches, Converter={StaticResource BoolToVisibility}}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button Style="{StaticResource SuggestChip}" Content="{Binding}"
|
||||||
|
Command="{Binding DataContext.PickCommand,
|
||||||
|
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
ToolTip="{Binding StringFormat='{}{0} 속성을 추가합니다'}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="{Binding MoreText}" FontSize="10.5" Margin="0,0,0,2"
|
||||||
|
Foreground="{DynamicResource B.Muted}"
|
||||||
|
Visibility="{Binding HasMore, Converter={StaticResource BoolToVisibility}}"/>
|
||||||
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate DataType="{x:Type ins:QueryRowViewModel}">
|
<DataTemplate DataType="{x:Type ins:QueryRowViewModel}">
|
||||||
|
|||||||
Reference in New Issue
Block a user