초기 커밋: SheetMe 서식생성기 (P0~P5 완료 상태)
레거시 서식생성기(VB.NET WinForms) 대체용 C#/.NET 10 WPF 디자이너. 기준선: 실DB 활성 디자인 1,271건 왕복 의미론 diff 0 / 예외 0, 단위 테스트 49/49. 이 커밋에 함께 포함된 자격증명 분리: - appsettings.json 을 __HOST__/__PASSWORD__ 플레이스홀더로 전환 - 실접속 정보는 appsettings.Development.json 으로 분리(.gitignore 제외, csproj Debug 조건부 복사라 Release 산출물에 실리지 않음) - ConfigLoader 를 환경변수 > Development > appsettings 순 레이어링으로 변경, 미치환 플레이스홀더는 '미설정'으로 간주 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using SheetMe.Core.Catalog;
|
||||
using SheetMe.Core.Models;
|
||||
using SheetMe.Core.Serialization;
|
||||
using SheetMe.Designer.Services;
|
||||
using SheetMe.Designer.ViewModels.Controls;
|
||||
|
||||
namespace SheetMe.Designer.ViewModels.Inspector;
|
||||
|
||||
/// <summary>
|
||||
/// 속성 인스펙터 — ControlRegistry 스키마 기반 행 구성.
|
||||
/// 선택 '집합' 변경 시에만 재구성(드래그 프레임 제외), 다중선택은 값 동일성 병합("여러 값").
|
||||
/// 커밋 규약: 값 변경 시 Undo 스냅샷 1회 → 전체 선택 대상 적용 → 시각 재해석.
|
||||
/// </summary>
|
||||
public sealed class InspectorViewModel : ViewModelBase
|
||||
{
|
||||
#region Member Fields
|
||||
private const string StringItemAqn =
|
||||
"System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
|
||||
private readonly DesignerViewModel designer;
|
||||
private readonly List<(PropertyRowViewModel Row, RowBinding Binding)> boundsRows = new();
|
||||
private bool showAdvanced;
|
||||
|
||||
/// <summary>공통/글꼴 섹션이 이미 다루는 키 — 고급 목록에서 제외</summary>
|
||||
private static readonly HashSet<string> HandledKeys = new(StringComparer.Ordinal)
|
||||
{
|
||||
"Location", "Size", "LocationOnBase", "Name", "Font",
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>행 목록(섹션 헤더 포함)</summary>
|
||||
public ObservableCollection<PropertyRowViewModel> Rows { get; } = new();
|
||||
|
||||
/// <summary>선택 요약 텍스트</summary>
|
||||
public string Summary
|
||||
=> designer.Selection.Items.Count switch
|
||||
{
|
||||
0 => "선택 없음",
|
||||
1 => $"{designer.Selection.Primary!.Type} — {designer.Selection.Primary!.Id}",
|
||||
var n => $"{n}개 선택",
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public InspectorViewModel(DesignerViewModel designer)
|
||||
{
|
||||
this.designer = designer;
|
||||
designer.Selection.SetChanged += Rebuild;
|
||||
designer.Selection.Changed += RefreshBoundsRows; // 드래그/리사이즈 중 X/Y/W/H 실시간 갱신
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>경계(X/Y/W/H) 행 값만 갱신 — 이동/리사이즈 프레임(재구성 없이 가볍게)</summary>
|
||||
private void RefreshBoundsRows()
|
||||
{
|
||||
var items = designer.Selection.Items;
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var (row, binding) in boundsRows)
|
||||
{
|
||||
var values = items.Select(binding.Get).Distinct(StringComparer.Ordinal).ToList();
|
||||
row.Initialize(values.Count == 1 ? values[0] : null, isMixed: values.Count > 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>행 재구성 — 선택 집합 변경 시</summary>
|
||||
public void Rebuild()
|
||||
{
|
||||
Rows.Clear();
|
||||
boundsRows.Clear();
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
|
||||
var items = designer.Selection.Items;
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 공통: 이름/위치/크기
|
||||
Rows.Add(new SectionRowViewModel("공통"));
|
||||
if (items.Count == 1)
|
||||
{
|
||||
AddRow(new TextRowViewModel("이름"), new RowBinding
|
||||
{
|
||||
Get = vm => vm.Id,
|
||||
Set = (vm, value) => RenameControl(vm, value),
|
||||
AffectsVisual = false,
|
||||
});
|
||||
}
|
||||
AddBoundsRow("X", vm => vm.X, (vm, v) => vm.X = v);
|
||||
AddBoundsRow("Y", vm => vm.Y, (vm, v) => vm.Y = v);
|
||||
AddBoundsRow("너비", vm => vm.Width, (vm, v) => vm.Width = Math.Max(1, v));
|
||||
AddBoundsRow("높이", vm => vm.Height, (vm, v) => vm.Height = Math.Max(1, v));
|
||||
|
||||
// 폰트(공통) — Font Property(상속 시 빈 값)
|
||||
Rows.Add(new SectionRowViewModel("글꼴"));
|
||||
AddFontRow("글꼴", f => f.Family, (f, v) => f.Family = v.Length == 0 ? f.Family : v);
|
||||
AddFontRow("크기(pt)", f => f.SizePt.ToString("0.##", CultureInfo.InvariantCulture),
|
||||
(f, v) => f.SizePt = double.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var size) && size > 0 ? size : f.SizePt);
|
||||
AddFontToggleRow("굵게", f => f.Bold, (f, v) => f.Bold = v);
|
||||
AddFontToggleRow("밑줄", f => f.Underline, (f, v) => f.Underline = v);
|
||||
|
||||
// 타입 전용 — 전체 선택이 같은 타입일 때만
|
||||
var type = items[0].Type;
|
||||
var curatedKeys = new HashSet<string>(HandledKeys, StringComparer.Ordinal);
|
||||
if (items.All(i => i.Type == type) && ControlRegistry.Find(type) is { } descriptor && descriptor.Properties.Count > 0)
|
||||
{
|
||||
Rows.Add(new SectionRowViewModel(descriptor.DisplayName));
|
||||
foreach (var def in descriptor.Properties)
|
||||
{
|
||||
AddDefRow(def);
|
||||
curatedKeys.Add(def.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// 전체 속성(고급) — 단일 선택 시 PropBag 의 나머지 레거시 속성 전부(레거시 PropertyGrid 등가)
|
||||
if (items.Count == 1)
|
||||
{
|
||||
BuildAdvancedRows(items[0], curatedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>고급 섹션 구성 — 접이식, raw 문자열 편집 + 속성 추가</summary>
|
||||
private void BuildAdvancedRows(ControlViewModel target, HashSet<string> curatedKeys)
|
||||
{
|
||||
var advancedKeys = target.Model.Props.Keys
|
||||
.Where(k => !curatedKeys.Contains(k))
|
||||
.ToList();
|
||||
|
||||
if (!showAdvanced)
|
||||
{
|
||||
Rows.Add(new ToggleAdvancedRowViewModel($"전체 속성 표시 ▾ ({advancedKeys.Count}개)", () =>
|
||||
{
|
||||
showAdvanced = true;
|
||||
Rebuild();
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
Rows.Add(new SectionRowViewModel("전체 속성 (고급 — 레거시 원문)"));
|
||||
Rows.Add(new ToggleAdvancedRowViewModel("전체 속성 숨기기 ▴", () =>
|
||||
{
|
||||
showAdvanced = false;
|
||||
Rebuild();
|
||||
}));
|
||||
|
||||
foreach (var key in advancedKeys)
|
||||
{
|
||||
var value = target.Model.Props.Get(key);
|
||||
switch (value)
|
||||
{
|
||||
case LegacyPropValue.TextValue or LegacyPropValue.NullValue or null:
|
||||
AddRow(new TextRowViewModel(key), new RowBinding
|
||||
{
|
||||
Get = vm => vm.Model.Props.GetText(key) ?? string.Empty,
|
||||
Set = (vm, newValue) => vm.Model.Props.SetText(key, newValue),
|
||||
});
|
||||
break;
|
||||
|
||||
case LegacyPropValue.ItemsValue items:
|
||||
AddRow(new MultilineTextRowViewModel(key + " (목록)"), new RowBinding
|
||||
{
|
||||
Get = vm => vm.Model.Props.Get(key) is LegacyPropValue.ItemsValue iv
|
||||
? string.Join("\n", iv.Items.Select(i => i.Value is LegacyPropValue.TextValue t ? t.Value : string.Empty))
|
||||
: string.Empty,
|
||||
Set = (vm, newValue) =>
|
||||
{
|
||||
var itemsValue = new LegacyPropValue.ItemsValue();
|
||||
var aqn = items.Items.FirstOrDefault()?.Aqn ?? StringItemAqn;
|
||||
foreach (var line in newValue.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
itemsValue.Items.Add(new LegacyItem { Aqn = aqn, Value = new LegacyPropValue.TextValue(line) });
|
||||
}
|
||||
vm.Model.Props.Set(key, itemsValue);
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case LegacyPropValue.NestedValue nested:
|
||||
Rows.Add(new ReadOnlyRowViewModel(key, $"(중첩 속성 {nested.Children.Count}개 — 보존됨)"));
|
||||
break;
|
||||
|
||||
case LegacyPropValue.BinaryValue:
|
||||
Rows.Add(new ReadOnlyRowViewModel(key, "(바이너리 — 원문 보존됨)"));
|
||||
break;
|
||||
|
||||
case LegacyPropValue.ReferenceValue reference:
|
||||
Rows.Add(new ReadOnlyRowViewModel(key, $"(참조: {reference.Name})"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 속성 추가 — 레거시 컨트롤의 임의 속성 지정(오타 주의: 로더가 모르는 키는 경고 처리)
|
||||
Rows.Add(new AddPropertyRowViewModel(key =>
|
||||
{
|
||||
if (target.Model.Props.Contains(key))
|
||||
{
|
||||
System.Windows.MessageBox.Show($"이미 존재하는 속성입니다: {key}", "속성 추가");
|
||||
return;
|
||||
}
|
||||
designer.Undo.Snapshot();
|
||||
target.Model.Props.SetText(key, string.Empty);
|
||||
Rebuild();
|
||||
}));
|
||||
}
|
||||
|
||||
private void AddDefRow(PropertyDef def)
|
||||
{
|
||||
var binding = def.Editor == PropEditorKind.StringList
|
||||
? new RowBinding
|
||||
{
|
||||
Get = vm => vm.Model.Props.Get(def.Key) is LegacyPropValue.ItemsValue items
|
||||
? string.Join("\n", items.Items.Select(i => i.Value is LegacyPropValue.TextValue t ? t.Value : string.Empty))
|
||||
: string.Empty,
|
||||
Set = (vm, value) =>
|
||||
{
|
||||
var itemsValue = new LegacyPropValue.ItemsValue();
|
||||
foreach (var line in value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
itemsValue.Items.Add(new LegacyItem { Aqn = StringItemAqn, Value = new LegacyPropValue.TextValue(line) });
|
||||
}
|
||||
vm.Model.Props.Set(def.Key, itemsValue);
|
||||
},
|
||||
}
|
||||
: new RowBinding
|
||||
{
|
||||
Get = vm => vm.Model.Props.GetText(def.Key),
|
||||
Set = (vm, value) =>
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
vm.Model.Props.Remove(def.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.Model.Props.SetText(def.Key, value);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
PropertyRowViewModel row = def.Editor switch
|
||||
{
|
||||
PropEditorKind.MultilineText or PropEditorKind.StringList => new MultilineTextRowViewModel(def.Label),
|
||||
PropEditorKind.Number => new NumberRowViewModel(def.Label),
|
||||
PropEditorKind.Toggle => new ToggleRowViewModel(def.Label),
|
||||
PropEditorKind.Choice => new ChoiceRowViewModel(def.Label, def.Choices ?? Array.Empty<string>()),
|
||||
PropEditorKind.Color => new ColorRowViewModel(def.Label),
|
||||
PropEditorKind.DataInterfaceTag => new TagPickerRowViewModel(def.Label,
|
||||
"데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", LegacyTagCatalog.DataInterfaceTags),
|
||||
PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label,
|
||||
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags),
|
||||
PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label),
|
||||
_ => new TextRowViewModel(def.Label),
|
||||
};
|
||||
AddRow(row, binding);
|
||||
}
|
||||
|
||||
private void AddBoundsRow(string label, Func<ControlViewModel, double> get, Action<ControlViewModel, double> set)
|
||||
{
|
||||
var row = new NumberRowViewModel(label);
|
||||
var binding = new RowBinding
|
||||
{
|
||||
Get = vm => Math.Round(get(vm)).ToString(CultureInfo.InvariantCulture),
|
||||
Set = (vm, value) =>
|
||||
{
|
||||
if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var number))
|
||||
{
|
||||
set(vm, Math.Round(number));
|
||||
}
|
||||
},
|
||||
AffectsVisual = false,
|
||||
};
|
||||
AddRow(row, binding, afterCommit: () => designer.Selection.NotifyBoundsChanged());
|
||||
boundsRows.Add((row, binding));
|
||||
}
|
||||
|
||||
private void AddFontRow(string label, Func<LegacyFont, string> get, Action<LegacyFont, string> set)
|
||||
{
|
||||
AddRow(new TextRowViewModel(label), new RowBinding
|
||||
{
|
||||
Get = vm => get(vm.EffectiveFont),
|
||||
Set = (vm, value) =>
|
||||
{
|
||||
var font = CurrentFontOf(vm);
|
||||
set(font, value);
|
||||
vm.Model.Props.SetText("Font", LegacyFormat.FormatFont(font));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private void AddFontToggleRow(string label, Func<LegacyFont, bool> get, Action<LegacyFont, bool> set)
|
||||
{
|
||||
AddRow(new ToggleRowViewModel(label), new RowBinding
|
||||
{
|
||||
Get = vm => get(vm.EffectiveFont) ? "True" : "False",
|
||||
Set = (vm, value) =>
|
||||
{
|
||||
var font = CurrentFontOf(vm);
|
||||
set(font, value == "True");
|
||||
vm.Model.Props.SetText("Font", LegacyFormat.FormatFont(font));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>편집 기준 폰트 — 명시 Font 있으면 그 값, 없으면 유효(상속) 폰트 복사본을 물화</summary>
|
||||
private static LegacyFont CurrentFontOf(ControlViewModel vm)
|
||||
{
|
||||
var explicitFont = vm.Model.Props.GetText("Font");
|
||||
if (explicitFont is not null)
|
||||
{
|
||||
return LegacyFormat.ParseFont(explicitFont);
|
||||
}
|
||||
var inherited = vm.EffectiveFont;
|
||||
return new LegacyFont
|
||||
{
|
||||
Family = inherited.Family,
|
||||
SizePt = inherited.SizePt,
|
||||
Bold = inherited.Bold,
|
||||
Italic = inherited.Italic,
|
||||
Underline = inherited.Underline,
|
||||
Strikeout = inherited.Strikeout,
|
||||
};
|
||||
}
|
||||
|
||||
private void AddRow(PropertyRowViewModel row, RowBinding binding, Action? afterCommit = null)
|
||||
{
|
||||
var items = designer.Selection.Items;
|
||||
var values = items.Select(binding.Get).Distinct(StringComparer.Ordinal).ToList();
|
||||
row.Initialize(values.Count == 1 ? values[0] : null, isMixed: values.Count > 1);
|
||||
|
||||
row.Commit = value =>
|
||||
{
|
||||
var targets = designer.Selection.Items.ToList();
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
designer.Undo.Snapshot();
|
||||
foreach (var target in targets)
|
||||
{
|
||||
binding.Set(target, value);
|
||||
if (binding.AffectsVisual)
|
||||
{
|
||||
target.RefreshVisual();
|
||||
}
|
||||
target.NotifyAllChanged();
|
||||
}
|
||||
afterCommit?.Invoke();
|
||||
};
|
||||
Rows.Add(row);
|
||||
}
|
||||
|
||||
private void RenameControl(ControlViewModel vm, string newId)
|
||||
{
|
||||
var trimmed = newId.Trim();
|
||||
if (trimmed.Length == 0 || trimmed == vm.Id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var used = IdGenerator.CollectUsed(designer.Document);
|
||||
used.Remove(vm.Id);
|
||||
if (used.Contains(trimmed))
|
||||
{
|
||||
System.Windows.MessageBox.Show($"이미 사용 중인 이름입니다: {trimmed}", "이름 변경");
|
||||
return;
|
||||
}
|
||||
vm.Model.Id = trimmed;
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
using System.Windows.Media;
|
||||
using SheetMe.Designer.ViewModels.Controls;
|
||||
|
||||
namespace SheetMe.Designer.ViewModels.Inspector;
|
||||
|
||||
/// <summary>
|
||||
/// 인스펙터 행 ViewModel — 라벨 + 문자열 정규화 값. 커밋 시(값 변경 시에만) 소유자 콜백으로
|
||||
/// Undo 스냅샷 → 전체 선택 대상 적용 → 시각 재해석이 수행된다.
|
||||
/// </summary>
|
||||
public abstract class PropertyRowViewModel : ViewModelBase
|
||||
{
|
||||
#region Member Fields
|
||||
private string valueText = string.Empty;
|
||||
private bool building;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>행 라벨(한글)</summary>
|
||||
public string Label { get; }
|
||||
|
||||
/// <summary>선택 대상들의 값이 서로 다른지 — "여러 값" 표시</summary>
|
||||
public bool IsMixed { get; private set; }
|
||||
|
||||
/// <summary>정규화 문자열 값 — 파생 편집기가 형 변환</summary>
|
||||
public string ValueText
|
||||
{
|
||||
get => valueText;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref valueText, value) || building)
|
||||
{
|
||||
return;
|
||||
}
|
||||
IsMixed = false;
|
||||
OnPropertyChanged(nameof(IsMixed));
|
||||
Commit?.Invoke(value);
|
||||
OnValueApplied();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>커밋 콜백 — InspectorViewModel 이 배선(스냅샷+적용)</summary>
|
||||
public Action<string>? Commit { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
protected PropertyRowViewModel(string label)
|
||||
{
|
||||
Label = label;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>초기값 세팅(커밋 미발생)</summary>
|
||||
public void Initialize(string? value, bool isMixed = false)
|
||||
{
|
||||
building = true;
|
||||
valueText = value ?? string.Empty;
|
||||
IsMixed = isMixed;
|
||||
OnPropertyChanged(nameof(ValueText));
|
||||
OnPropertyChanged(nameof(IsMixed));
|
||||
building = false;
|
||||
}
|
||||
|
||||
/// <summary>값 적용 후 파생 갱신 지점</summary>
|
||||
protected virtual void OnValueApplied() { }
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>구분 헤더 행</summary>
|
||||
public sealed class SectionRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
public SectionRowViewModel(string label) : base(label) { }
|
||||
}
|
||||
|
||||
/// <summary>한 줄 문자열 행</summary>
|
||||
public sealed class TextRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
public TextRowViewModel(string label) : base(label) { }
|
||||
}
|
||||
|
||||
/// <summary>여러 줄 문자열 행(Text/수식/항목 목록)</summary>
|
||||
public sealed class MultilineTextRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
public MultilineTextRowViewModel(string label) : base(label) { }
|
||||
}
|
||||
|
||||
/// <summary>숫자 행 — 문자열 바인딩, 커밋 시 숫자 검증은 소유자에서</summary>
|
||||
public sealed class NumberRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
public NumberRowViewModel(string label) : base(label) { }
|
||||
}
|
||||
|
||||
/// <summary>토글(참/거짓) 행</summary>
|
||||
public sealed class ToggleRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
/// <summary>체크 상태 — "True"/"False" 문자열과 동기</summary>
|
||||
public bool IsOn
|
||||
{
|
||||
get => ValueText == "True";
|
||||
set => ValueText = value ? "True" : "False";
|
||||
}
|
||||
|
||||
public ToggleRowViewModel(string label) : base(label) { }
|
||||
|
||||
protected override void OnValueApplied() => OnPropertyChanged(nameof(IsOn));
|
||||
}
|
||||
|
||||
/// <summary>선택지 행</summary>
|
||||
public sealed class ChoiceRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
/// <summary>선택지 목록</summary>
|
||||
public string[] Choices { get; }
|
||||
|
||||
public ChoiceRowViewModel(string label, string[] choices) : base(label)
|
||||
{
|
||||
Choices = choices;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 태그 피커 행 — 값 표시 + 찾아보기 버튼(검색 대화상자).
|
||||
/// 목록에 없는 사이트 커스텀 태그는 텍스트 직접 입력도 허용.
|
||||
/// </summary>
|
||||
public sealed class TagPickerRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
/// <summary>피커 선택지(레거시 카탈로그)</summary>
|
||||
public IReadOnlyList<string> Choices { get; }
|
||||
|
||||
/// <summary>피커 대화상자 제목</summary>
|
||||
public string PickerTitle { get; }
|
||||
|
||||
/// <summary>찾아보기 — 검색 대화상자 열기</summary>
|
||||
public M.Framework.WPF.ICustomCommand? BrowseCommand { get; set; }
|
||||
|
||||
public TagPickerRowViewModel(string label, string pickerTitle, IReadOnlyList<string> choices) : base(label)
|
||||
{
|
||||
PickerTitle = pickerTitle;
|
||||
Choices = choices;
|
||||
BrowseCommand = new M.Framework.WPF.Command((sender, e) => OnBrowse());
|
||||
}
|
||||
|
||||
private void OnBrowse()
|
||||
{
|
||||
var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText)
|
||||
{
|
||||
Owner = System.Windows.Application.Current.MainWindow,
|
||||
};
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
ValueText = dialog.SelectedTag ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>SQL 쿼리 행 — 요약 표시 + 전용 편집기 창(치환 변수 삽입)</summary>
|
||||
public sealed class QueryRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
/// <summary>요약 텍스트(한 줄)</summary>
|
||||
public string Summary
|
||||
{
|
||||
get
|
||||
{
|
||||
var oneLine = ValueText.Replace("\r", " ").Replace("\n", " ").Trim();
|
||||
return oneLine.Length == 0 ? "(쿼리 없음)" : oneLine.Length > 48 ? oneLine[..48] + "…" : oneLine;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>전용 편집기 열기</summary>
|
||||
public M.Framework.WPF.ICustomCommand? EditCommand { get; set; }
|
||||
|
||||
public QueryRowViewModel(string label) : base(label)
|
||||
{
|
||||
EditCommand = new M.Framework.WPF.Command((sender, e) => OnEdit());
|
||||
}
|
||||
|
||||
protected override void OnValueApplied() => OnPropertyChanged(nameof(Summary));
|
||||
|
||||
private void OnEdit()
|
||||
{
|
||||
var dialog = new Views.QueryEditorWindow(Label, ValueText)
|
||||
{
|
||||
Owner = System.Windows.Application.Current.MainWindow,
|
||||
};
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
ValueText = dialog.QueryText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>색 행 — 레거시 invariant 문자열("R, G, B"/명명색) + 미리보기 스와치</summary>
|
||||
public sealed class ColorRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
/// <summary>미리보기 브러시</summary>
|
||||
public Brush Preview
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ValueText.Length == 0)
|
||||
{
|
||||
return Brushes.Transparent;
|
||||
}
|
||||
var (a, r, g, b) = Core.Serialization.LegacyFormat.ParseColor(ValueText);
|
||||
var brush = new SolidColorBrush(Color.FromArgb(a, r, g, b));
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>색상 피커 열기 — 확정 시 레거시 invariant 형식("R, G, B")으로 반영</summary>
|
||||
public M.Framework.WPF.ICustomCommand? PickCommand { get; set; }
|
||||
|
||||
public ColorRowViewModel(string label) : base(label)
|
||||
{
|
||||
PickCommand = new M.Framework.WPF.Command((sender, e) => OnPick());
|
||||
}
|
||||
|
||||
protected override void OnValueApplied() => OnPropertyChanged(nameof(Preview));
|
||||
|
||||
private void OnPick()
|
||||
{
|
||||
var initialHex = (string?)null;
|
||||
if (ValueText.Length > 0)
|
||||
{
|
||||
var (_, r, g, b) = Core.Serialization.LegacyFormat.ParseColor(ValueText);
|
||||
initialHex = $"#{r:X2}{g:X2}{b:X2}";
|
||||
}
|
||||
var picked = Views.ColorPickerWindow.Pick(System.Windows.Application.Current.MainWindow, initialHex);
|
||||
if (picked is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var color = (Color)ColorConverter.ConvertFromString(picked);
|
||||
ValueText = Core.Serialization.LegacyFormat.FormatColor(255, color.R, color.G, color.B);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>읽기 전용 행 — 중첩/바이너리/참조 등 raw 편집 불가 값 표시</summary>
|
||||
public sealed class ReadOnlyRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
public ReadOnlyRowViewModel(string label, string display) : base(label)
|
||||
{
|
||||
Initialize(display);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>전체 속성(고급) 섹션 토글 행 — 표시/숨김 버튼</summary>
|
||||
public sealed class ToggleAdvancedRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
/// <summary>버튼 표시 텍스트</summary>
|
||||
public string ButtonText { get; }
|
||||
|
||||
/// <summary>토글 실행</summary>
|
||||
public M.Framework.WPF.ICustomCommand? ToggleCommand { get; set; }
|
||||
|
||||
public ToggleAdvancedRowViewModel(string buttonText, Action toggle) : base(string.Empty)
|
||||
{
|
||||
ButtonText = buttonText;
|
||||
ToggleCommand = new M.Framework.WPF.Command((sender, e) => toggle());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>속성 추가 행 — 키 입력 후 빈 속성 생성(고급)</summary>
|
||||
public sealed class AddPropertyRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
private string keyText = string.Empty;
|
||||
|
||||
/// <summary>추가할 속성 키(레거시 Property 이름)</summary>
|
||||
public string KeyText
|
||||
{
|
||||
get => keyText;
|
||||
set => SetProperty(ref keyText, value);
|
||||
}
|
||||
|
||||
/// <summary>추가 실행</summary>
|
||||
public M.Framework.WPF.ICustomCommand? AddCommand { get; set; }
|
||||
|
||||
public AddPropertyRowViewModel(Action<string> add) : base(string.Empty)
|
||||
{
|
||||
AddCommand = new M.Framework.WPF.Command((sender, e) =>
|
||||
{
|
||||
var key = KeyText.Trim();
|
||||
if (key.Length > 0)
|
||||
{
|
||||
add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>인스펙터 행 컨텍스트 — 대상 컨트롤 집합과 접근자</summary>
|
||||
public sealed class RowBinding
|
||||
{
|
||||
/// <summary>값 읽기</summary>
|
||||
public required Func<ControlViewModel, string?> Get { get; init; }
|
||||
|
||||
/// <summary>값 쓰기</summary>
|
||||
public required Action<ControlViewModel, string> Set { get; init; }
|
||||
|
||||
/// <summary>커밋 후 시각 재해석 필요 여부</summary>
|
||||
public bool AffectsVisual { get; init; } = true;
|
||||
}
|
||||
Reference in New Issue
Block a user