diff --git a/src/SheetMe.Designer/Controls/InstalledFontCatalog.cs b/src/SheetMe.Designer/Controls/InstalledFontCatalog.cs new file mode 100644 index 0000000..22e320d --- /dev/null +++ b/src/SheetMe.Designer/Controls/InstalledFontCatalog.cs @@ -0,0 +1,76 @@ +using System.Globalization; +using System.Windows.Markup; +using System.Windows.Media; + +namespace SheetMe.Designer.Controls; + +/// +/// 이 PC 에 설치된 글꼴 이름 목록 — 인스펙터 글꼴 칸의 선택지. +/// +/// 한글 이름을 낸다. 레거시가 저장하는 값이 그것이다 — 실사용 서식의 Font 속성은 +/// "굴림, 11.25pt, style=Bold" 처럼 GDI 가 주는 지역화된 이름을 담고 있다. +/// WPF 의 는 같은 글꼴을 "Gulim" 으로 주므로, +/// 그걸 그대로 목록에 쓰면 사용자가 고른 순간 서식의 글꼴 이름이 영문으로 바뀐다. +/// 레거시가 그 이름을 못 찾으면 기본 글꼴로 떨어지고, 그 사실은 EMR 에서만 드러난다. +/// +/// 목록에 없는 이름도 지운다는 뜻이 아니다. 병원 PC 와 이 PC 의 설치 글꼴은 다르다. +/// 서식이 쓰는 글꼴이 여기 없을 수 있고, 그건 서식이 틀렸다는 뜻이 아니라 +/// 지금 이 화면이 병원과 다르게 보인다는 뜻이다 — 그래서 값을 고치지 않고 알리기만 한다. +/// +public static class InstalledFontCatalog +{ + #region Member Fields + private static IReadOnlyList? cached; + private static HashSet? installed; + #endregion + + #region Methods + /// 설치된 글꼴 이름(한글 우선) — 가나다·알파벳 순, 한 번만 훑는다 + public static IReadOnlyList Families() + { + if (cached is not null) + { + return cached; + } + var names = new SortedSet(StringComparer.Create(CultureInfo.GetCultureInfo("ko-KR"), ignoreCase: true)); + foreach (var family in Fonts.SystemFontFamilies) + { + names.Add(DisplayNameOf(family)); + } + cached = names.ToList(); + installed = new HashSet(cached, StringComparer.OrdinalIgnoreCase); + return cached; + } + + /// 이 PC 에 있는 글꼴인가 — 없으면 화면과 병원 출력이 다르다는 신호다 + public static bool IsInstalled(string family) + { + if (family.Length == 0) + { + return true; // 미지정(상속)은 경고 대상이 아니다 + } + Families(); + return installed!.Contains(family); + } + + /// + /// 이 글꼴의 표시 이름 — 한국어 이름이 있으면 그것, 없으면 원래 이름. + /// + /// FamilyNames 는 글꼴이 스스로 담고 있는 언어별 이름표다. 한국어 글꼴은 + /// ko-KR 항목에 "굴림"·"맑은 고딕" 을 갖고 있고, 레거시(GDI)가 쓰는 이름이 바로 그것이다. + /// + private static string DisplayNameOf(FontFamily family) + { + var korean = XmlLanguage.GetLanguage("ko-KR"); + if (family.FamilyNames.TryGetValue(korean, out var name) && name.Length > 0) + { + return name; + } + // Source 는 "Gulim" 처럼 영문이거나, 조합 글꼴이면 "굴림, Arial" 같은 목록일 수 있다 — + // 목록이면 첫 이름만 쓴다(레거시 Font.Name 도 하나의 이름이다) + var source = family.Source; + var comma = source.IndexOf(','); + return comma < 0 ? source : source[..comma].Trim(); + } + #endregion +} diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index 5a71b9e..9e3917d 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -1095,6 +1095,55 @@ public static class EditSmoke $"ValueText='{choiceRow.ValueText}' IsMixed={choiceRow.IsMixed}"); } + // 15-bb) 글꼴은 목록에서 고른다 — 전에는 자유 입력뿐이라 매번 타이핑해야 했고, + // 한 글자만 틀려도 레거시가 아무 말 없이 기본 글꼴로 떨어뜨렸다. + // + // 이름의 형태가 핵심이다. 레거시가 저장하는 값은 GDI 가 주는 한글 이름 + // ("굴림, 11.25pt, style=Bold")인데, WPF 의 FontFamily.Source 는 같은 글꼴을 + // "Gulim" 으로 준다. 영문 이름을 목록에 쓰면 고르는 순간 서식의 글꼴 이름이 바뀌고, + // 레거시가 그것을 못 찾으면 조용히 기본 글꼴이 된다 — EMR 에서만 드러난다. + { + var families = Controls.InstalledFontCatalog.Families(); + Check("글꼴: 설치 글꼴 목록을 낸다", families.Count > 0, $"{families.Count}종"); + // 실사용 서식이 쓰는 이름이 목록에 그대로 있어야 한다(sheetdesign_sample.xml 실측: 굴림·돋움) + Check("글꼴: 레거시가 저장하는 한글 이름으로 낸다", + families.Contains("굴림", StringComparer.Ordinal) + || families.Contains("돋움", StringComparer.Ordinal) + || families.Contains("맑은 고딕", StringComparer.Ordinal), + string.Join(", ", families.Take(8))); + Check("글꼴: 설치 여부를 판정한다", + Controls.InstalledFontCatalog.IsInstalled(families[0]) + && !Controls.InstalledFontCatalog.IsInstalled("이런글꼴은없다"), + families[0]); + // 미지정(상속)은 경고 대상이 아니다 — 대부분의 컨트롤이 Font 키를 갖지 않는다 + Check("글꼴: 미지정은 경고하지 않는다", Controls.InstalledFontCatalog.IsInstalled(string.Empty)); + + var fontDoc = new DesignerViewModel(new FormDesignDataBusiness().CreateNew()); + fontDoc.AddControlAt("Label", new Point(60, 60)); + fontDoc.Selection.SetSingle(fontDoc.Pages[0].Controls[0]); + var familyRow = fontDoc.Inspector.Rows + .OfType().FirstOrDefault(r => r.Label == "글꼴"); + Check("글꼴: 인스펙터 글꼴 칸이 선택 목록을 가진다", + familyRow is not null && familyRow.Families.Count > 0, + familyRow is null ? "행 없음" : $"{familyRow.Families.Count}종"); + if (familyRow is not null) + { + familyRow.ValueText = "돋움"; + var saved = fontDoc.Pages[0].Controls[0].Model.Props.GetText("Font") ?? string.Empty; + Check("글꼴: 고른 이름이 레거시 형식으로 저장된다", + saved.StartsWith("돋움,", StringComparison.Ordinal), saved); + + // 이 PC 에 없는 글꼴은 값을 고치지 않고 알리기만 한다 — + // 병원 PC 와 설치 글꼴이 다른 것이 정상이고, 고치면 서식이 망가진다 + familyRow.ValueText = "이런글꼴은없다"; + Check("글꼴: 없는 글꼴도 값은 그대로 저장한다", + (fontDoc.Pages[0].Controls[0].Model.Props.GetText("Font") ?? string.Empty) + .StartsWith("이런글꼴은없다,", StringComparison.Ordinal), + fontDoc.Pages[0].Controls[0].Model.Props.GetText("Font") ?? "(없음)"); + Check("글꼴: 없는 글꼴이면 화면이 병원과 다르다고 알린다", familyRow.IsMissingFont); + } + } + // 15-c) 여러 개를 함께 고르고 공통 속성을 한 번에 바꾼다. // // 전에는 타입이 하나라도 다르면 타입 전용 섹션이 통째로 사라졌다. 그런데 여러 개를 @@ -1630,10 +1679,14 @@ public static class EditSmoke bare.Pages[0].PaperBrush is System.Windows.Media.SolidColorBrush { Color.R: 255, Color.G: 255, Color.B: 255 }); } - // 기본 글꼴 — 명시 Font 가 없는 컨트롤이 상속한다 - var fontRow = pg.Inspector.Rows.OfType() + // 기본 글꼴 — 명시 Font 가 없는 컨트롤이 상속한다. + // 컨트롤 글꼴과 같은 선택 목록을 쓴다(전에는 페이지 쪽만 자유 입력으로 남아 있었다). + var fontRow = pg.Inspector.Rows.OfType() .FirstOrDefault(r => r.Label == "글꼴"); Check("페이지 속성: 기본 글꼴 행 존재", fontRow is not null); + Check("페이지 속성: 기본 글꼴도 목록에서 고른다", + fontRow is not null && fontRow.Families.Count > 0, + fontRow is null ? "행 없음" : $"{fontRow.Families.Count}종"); if (fontRow is not null) { fontRow.ValueText = "돋움"; diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs index 238d384..c5118d9 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs @@ -575,7 +575,8 @@ public sealed class InspectorViewModel : ViewModelBase // 페이지 글꼴은 자식이 상속한다 — 여기를 바꾸면 Font 를 명시하지 않은 컨트롤이 전부 따라온다 AddSection("기본 글꼴 (컨트롤이 상속)"); - AddPageFontRow(page, "글꼴", f => f.Family, (f, v) => f.Family = v.Length == 0 ? f.Family : v); + AddPageFontRow(page, "글꼴", f => f.Family, (f, v) => f.Family = v.Length == 0 ? f.Family : v, + isFamily: true); AddPageFontRow(page, "크기(pt)", f => f.SizePt.ToString("0.##", CultureInfo.InvariantCulture), (f, v) => f.SizePt = double.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var size) && size > 0 @@ -673,9 +674,12 @@ public sealed class InspectorViewModel : ViewModelBase return row; } - /// 페이지 기본 글꼴 — 자식 상속 컨텍스트가 바뀌므로 문서 전체 시각을 다시 해석한다 + /// + /// 페이지 기본 글꼴 — 자식 상속 컨텍스트가 바뀌므로 문서 전체 시각을 다시 해석한다. + /// 면 이름 칸이라 설치 글꼴 목록에서 고를 수 있게 한다. + /// private void AddPageFontRow(PageViewModel page, string label, - Func get, Action set) + Func get, Action set, bool isFamily = false) { var root = page.Model.Root; LegacyFont Current() @@ -684,7 +688,8 @@ public sealed class InspectorViewModel : ViewModelBase return text is not null ? LegacyFormat.ParseFont(text) : new LegacyFont(); } - AddPageRow(page, new TextRowViewModel(label), () => get(Current()), value => + PropertyRowViewModel row = isFamily ? NewFontFamilyRow(label) : new TextRowViewModel(label); + AddPageRow(page, row, () => get(Current()), value => { var font = Current(); set(font, value); @@ -1034,9 +1039,16 @@ public sealed class InspectorViewModel : ViewModelBase return row; } + /// + /// 글꼴 이름 행 — 설치된 글꼴에서 고른다(직접 입력도 그대로 된다). + /// + /// 전에는 자유 입력 칸 하나뿐이라 "돋움"을 매번 타이핑해야 했다. 그리고 한 글자만 틀려도 + /// 레거시는 아무 말 없이 기본 글꼴로 떨어진다 — 디자이너에서는 멀쩡해 보이고 + /// EMR 에서만 다른 글꼴로 나온다. + /// private void AddFontRow(string label, Func get, Action set) { - AddRow(new TextRowViewModel(label), new RowBinding + AddRow(NewFontFamilyRow(label), new RowBinding { Get = vm => get(vm.EffectiveFont), Set = (vm, value) => @@ -1101,6 +1113,16 @@ public sealed class InspectorViewModel : ViewModelBase return new SegmentItem(icon, label, toggle); } + /// + /// 글꼴 이름 행 생성 — 컨트롤 글꼴과 페이지 기본 글꼴이 같은 것을 쓴다. + /// + /// 목록은 한글 이름이다. 레거시가 저장하는 값이 그것이라("굴림, 11.25pt") + /// 영문 이름을 쓰면 고르는 순간 서식의 글꼴 이름이 바뀌고, 레거시가 그것을 못 찾으면 + /// 조용히 기본 글꼴로 떨어진다. + /// + private static FontFamilyRowViewModel NewFontFamilyRow(string label) + => new(label, SheetMe.Designer.Controls.InstalledFontCatalog.Families(), SheetMe.Designer.Controls.InstalledFontCatalog.IsInstalled); + /// 편집 기준 폰트 — 명시 Font 있으면 그 값, 없으면 유효(상속) 폰트 복사본을 물화 private static LegacyFont CurrentFontOf(ControlViewModel vm) { diff --git a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs index 20b4d6c..08d9b59 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs @@ -402,6 +402,47 @@ public sealed class ToggleRowViewModel : PropertyRowViewModel protected override void OnValueApplied() => OnPropertyChanged(nameof(IsOn)); } +/// +/// 글꼴 이름 행 — 설치된 글꼴 목록에서 고르거나 직접 입력한다. +/// +/// 왜 자유 입력만으로는 안 되나. 전에는 그냥 텍스트 칸이라 "돋움"을 매번 타이핑해야 했고, +/// 한 글자만 틀려도 조용히 기본 글꼴로 떨어졌다(레거시는 못 찾은 이름에 대해 아무 말도 하지 않는다). +/// +/// 왜 목록만으로도 안 되나. 병원 PC 와 이 PC 의 설치 글꼴이 다르다. 목록에 가두면 +/// 여기 없는 글꼴을 쓰는 서식을 열었을 때 그 값을 고를 수 없게 되고, 손대는 순간 사라진다. +/// 그래서 고르기도 되고 쓰기도 되는 칸이다. +/// +public sealed class FontFamilyRowViewModel : PropertyRowViewModel +{ + private readonly Func isInstalled; + + /// 선택지 — 이 PC 에 설치된 글꼴 + public IReadOnlyList Families { get; } + + /// + /// 지금 값이 이 PC 에 없는 글꼴인가 — 그러면 이 화면이 병원과 다르게 보인다. + /// 서식이 틀린 것이 아니므로 값은 건드리지 않고 알리기만 한다. + /// + public bool IsMissingFont => !IsMixed && ValueText.Length > 0 && !isInstalled(ValueText); + + /// 경고 문구 — 툴팁으로 보인다 + public string MissingHint => $"'{ValueText}' 은(는) 이 PC 에 설치되어 있지 않습니다. " + + "저장되는 값은 그대로지만, 화면에는 대체 글꼴로 그려집니다."; + + public FontFamilyRowViewModel(string label, IReadOnlyList families, Func isInstalled) + : base(label) + { + Families = families; + this.isInstalled = isInstalled; + } + + protected override void OnValueApplied() + { + OnPropertyChanged(nameof(IsMissingFont)); + OnPropertyChanged(nameof(MissingHint)); + } +} + /// 선택지 행 public sealed class ChoiceRowViewModel : PropertyRowViewModel { diff --git a/src/SheetMe.Designer/Views/InspectorView.xaml b/src/SheetMe.Designer/Views/InspectorView.xaml index 1cbb3bb..c9133cf 100644 --- a/src/SheetMe.Designer/Views/InspectorView.xaml +++ b/src/SheetMe.Designer/Views/InspectorView.xaml @@ -391,6 +391,38 @@ + + + + + + + + + + + + + + + + + + + +