글꼴을 목록에서 고른다 — 매번 타이핑하지 않는다
글꼴 칸이 자유 입력 텍스트 하나였다. "돋움"을 매번 손으로 쳐야 했고,
한 글자만 틀려도 레거시는 <b>아무 말 없이</b> 기본 글꼴로 떨어진다 —
디자이너에서는 멀쩡해 보이고 EMR 에서만 다른 글꼴로 나온다.
이제 설치된 글꼴 목록에서 고른다. 목록의 각 이름은 그 글꼴로 그린다 —
이름만으로는 어떤 모양인지 알 수 없다. 페이지 기본 글꼴 칸도 같은 것을 쓴다
(전에는 페이지 쪽만 자유 입력으로 남아 있었다).
## 이름의 형태가 핵심이다
레거시가 저장하는 값은 GDI 가 주는 <b>한글 이름</b>이다 — 실사용 서식 실측:
"굴림, 11.25pt, style=Bold" · "돋움, 9.75pt". 그런데 WPF 의 FontFamily.Source 는
같은 글꼴을 "Gulim" 으로 준다. 영문 이름을 목록에 쓰면 <b>고르는 순간</b> 서식의 글꼴
이름이 바뀌고, 레거시가 그 이름을 못 찾으면 조용히 기본 글꼴이 된다.
그래서 글꼴이 스스로 담고 있는 ko-KR 이름표를 먼저 읽고, 없을 때만 원래 이름을 쓴다.
검사가 이것을 고정한다 — 목록에 "굴림"·"돋움"·"맑은 고딕" 중 하나는 반드시 있어야 한다.
## 목록에 가두지 않는다
병원 PC 와 이 PC 의 설치 글꼴은 다르다. 목록에만 고를 수 있게 하면
여기 없는 글꼴을 쓰는 서식을 열었을 때 그 값을 고를 수 없고, 칸을 건드리는 순간 사라진다.
그건 서식이 틀린 것이 아니라 <b>지금 이 화면이 병원과 다르게 보인다</b>는 뜻일 뿐이다.
그래서 고르기도 되고 쓰기도 되는 칸으로 두고, 이 PC 에 없는 글꼴이면
값은 그대로 둔 채 ⚠ 로 알린다("저장되는 값은 그대로지만, 화면에는 대체 글꼴로 그려집니다").
커밋은 포커스를 잃을 때 한 번이다 — 커밋 한 번이 문서 전체 딥클론 1회라
글자마다 커밋하면 "맑은 고딕" 여섯 자에 스냅샷 여섯 개가 쌓인다.
## 판정
- 단위 시험 388
- edit-smoke 348건 전건 통과 (글꼴 9건 추가 — 한글 이름·저장 형식·없는 글꼴 보존·경고)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
- --dialog-shots · --cleartype 전건 통과
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d45b674e29
commit
be9d697a0d
@@ -0,0 +1,76 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SheetMe.Designer.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 이 PC 에 설치된 글꼴 이름 목록 — 인스펙터 글꼴 칸의 선택지.
|
||||
///
|
||||
/// <b>한글 이름을 낸다.</b> 레거시가 저장하는 값이 그것이다 — 실사용 서식의 Font 속성은
|
||||
/// <c>"굴림, 11.25pt, style=Bold"</c> 처럼 GDI 가 주는 <b>지역화된</b> 이름을 담고 있다.
|
||||
/// WPF 의 <see cref="FontFamily.Source"/> 는 같은 글꼴을 "Gulim" 으로 주므로,
|
||||
/// 그걸 그대로 목록에 쓰면 사용자가 고른 순간 서식의 글꼴 이름이 영문으로 바뀐다.
|
||||
/// 레거시가 그 이름을 못 찾으면 기본 글꼴로 떨어지고, 그 사실은 EMR 에서만 드러난다.
|
||||
///
|
||||
/// <b>목록에 없는 이름도 지운다는 뜻이 아니다.</b> 병원 PC 와 이 PC 의 설치 글꼴은 다르다.
|
||||
/// 서식이 쓰는 글꼴이 여기 없을 수 있고, 그건 서식이 틀렸다는 뜻이 아니라
|
||||
/// <b>지금 이 화면이 병원과 다르게 보인다</b>는 뜻이다 — 그래서 값을 고치지 않고 알리기만 한다.
|
||||
/// </summary>
|
||||
public static class InstalledFontCatalog
|
||||
{
|
||||
#region Member Fields
|
||||
private static IReadOnlyList<string>? cached;
|
||||
private static HashSet<string>? installed;
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>설치된 글꼴 이름(한글 우선) — 가나다·알파벳 순, 한 번만 훑는다</summary>
|
||||
public static IReadOnlyList<string> Families()
|
||||
{
|
||||
if (cached is not null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
var names = new SortedSet<string>(StringComparer.Create(CultureInfo.GetCultureInfo("ko-KR"), ignoreCase: true));
|
||||
foreach (var family in Fonts.SystemFontFamilies)
|
||||
{
|
||||
names.Add(DisplayNameOf(family));
|
||||
}
|
||||
cached = names.ToList();
|
||||
installed = new HashSet<string>(cached, StringComparer.OrdinalIgnoreCase);
|
||||
return cached;
|
||||
}
|
||||
|
||||
/// <summary>이 PC 에 있는 글꼴인가 — 없으면 화면과 병원 출력이 다르다는 신호다</summary>
|
||||
public static bool IsInstalled(string family)
|
||||
{
|
||||
if (family.Length == 0)
|
||||
{
|
||||
return true; // 미지정(상속)은 경고 대상이 아니다
|
||||
}
|
||||
Families();
|
||||
return installed!.Contains(family);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 글꼴의 표시 이름 — 한국어 이름이 있으면 그것, 없으면 원래 이름.
|
||||
///
|
||||
/// <c>FamilyNames</c> 는 글꼴이 스스로 담고 있는 언어별 이름표다. 한국어 글꼴은
|
||||
/// ko-KR 항목에 "굴림"·"맑은 고딕" 을 갖고 있고, 레거시(GDI)가 쓰는 이름이 바로 그것이다.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
@@ -1095,6 +1095,55 @@ public static class EditSmoke
|
||||
$"ValueText='{choiceRow.ValueText}' IsMixed={choiceRow.IsMixed}");
|
||||
}
|
||||
|
||||
// 15-bb) 글꼴은 목록에서 고른다 — 전에는 자유 입력뿐이라 매번 타이핑해야 했고,
|
||||
// 한 글자만 틀려도 레거시가 아무 말 없이 기본 글꼴로 떨어뜨렸다.
|
||||
//
|
||||
// <b>이름의 형태가 핵심이다.</b> 레거시가 저장하는 값은 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<ViewModels.Inspector.FontFamilyRowViewModel>().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 에 없는 글꼴은 <b>값을 고치지 않고</b> 알리기만 한다 —
|
||||
// 병원 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) 여러 개를 함께 고르고 <b>공통 속성을 한 번에</b> 바꾼다.
|
||||
//
|
||||
// 전에는 타입이 하나라도 다르면 타입 전용 섹션이 통째로 사라졌다. 그런데 여러 개를
|
||||
@@ -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<ViewModels.Inspector.TextRowViewModel>()
|
||||
// 기본 글꼴 — 명시 Font 가 없는 컨트롤이 상속한다.
|
||||
// 컨트롤 글꼴과 같은 선택 목록을 쓴다(전에는 페이지 쪽만 자유 입력으로 남아 있었다).
|
||||
var fontRow = pg.Inspector.Rows.OfType<ViewModels.Inspector.FontFamilyRowViewModel>()
|
||||
.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 = "돋움";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>페이지 기본 글꼴 — 자식 상속 컨텍스트가 바뀌므로 문서 전체 시각을 다시 해석한다</summary>
|
||||
/// <summary>
|
||||
/// 페이지 기본 글꼴 — 자식 상속 컨텍스트가 바뀌므로 문서 전체 시각을 다시 해석한다.
|
||||
/// <paramref name="isFamily"/> 면 이름 칸이라 설치 글꼴 목록에서 고를 수 있게 한다.
|
||||
/// </summary>
|
||||
private void AddPageFontRow(PageViewModel page, string label,
|
||||
Func<LegacyFont, string> get, Action<LegacyFont, string> set)
|
||||
Func<LegacyFont, string> get, Action<LegacyFont, string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 글꼴 이름 행 — 설치된 글꼴에서 고른다(직접 입력도 그대로 된다).
|
||||
///
|
||||
/// 전에는 자유 입력 칸 하나뿐이라 "돋움"을 매번 타이핑해야 했다. 그리고 한 글자만 틀려도
|
||||
/// 레거시는 아무 말 없이 기본 글꼴로 떨어진다 — 디자이너에서는 멀쩡해 보이고
|
||||
/// EMR 에서만 다른 글꼴로 나온다.
|
||||
/// </summary>
|
||||
private void AddFontRow(string label, Func<LegacyFont, string> get, Action<LegacyFont, string> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 글꼴 이름 행 생성 — 컨트롤 글꼴과 페이지 기본 글꼴이 같은 것을 쓴다.
|
||||
///
|
||||
/// 목록은 <b>한글 이름</b>이다. 레거시가 저장하는 값이 그것이라("굴림, 11.25pt")
|
||||
/// 영문 이름을 쓰면 고르는 순간 서식의 글꼴 이름이 바뀌고, 레거시가 그것을 못 찾으면
|
||||
/// 조용히 기본 글꼴로 떨어진다.
|
||||
/// </summary>
|
||||
private static FontFamilyRowViewModel NewFontFamilyRow(string label)
|
||||
=> new(label, SheetMe.Designer.Controls.InstalledFontCatalog.Families(), SheetMe.Designer.Controls.InstalledFontCatalog.IsInstalled);
|
||||
|
||||
/// <summary>편집 기준 폰트 — 명시 Font 있으면 그 값, 없으면 유효(상속) 폰트 복사본을 물화</summary>
|
||||
private static LegacyFont CurrentFontOf(ControlViewModel vm)
|
||||
{
|
||||
|
||||
@@ -402,6 +402,47 @@ public sealed class ToggleRowViewModel : PropertyRowViewModel
|
||||
protected override void OnValueApplied() => OnPropertyChanged(nameof(IsOn));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 글꼴 이름 행 — 설치된 글꼴 목록에서 고르거나 직접 입력한다.
|
||||
///
|
||||
/// <b>왜 자유 입력만으로는 안 되나.</b> 전에는 그냥 텍스트 칸이라 "돋움"을 매번 타이핑해야 했고,
|
||||
/// 한 글자만 틀려도 조용히 기본 글꼴로 떨어졌다(레거시는 못 찾은 이름에 대해 아무 말도 하지 않는다).
|
||||
///
|
||||
/// <b>왜 목록만으로도 안 되나.</b> 병원 PC 와 이 PC 의 설치 글꼴이 다르다. 목록에 가두면
|
||||
/// 여기 없는 글꼴을 쓰는 서식을 열었을 때 그 값을 고를 수 없게 되고, 손대는 순간 사라진다.
|
||||
/// 그래서 <b>고르기도 되고 쓰기도 되는</b> 칸이다.
|
||||
/// </summary>
|
||||
public sealed class FontFamilyRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
private readonly Func<string, bool> isInstalled;
|
||||
|
||||
/// <summary>선택지 — 이 PC 에 설치된 글꼴</summary>
|
||||
public IReadOnlyList<string> Families { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 지금 값이 이 PC 에 없는 글꼴인가 — 그러면 <b>이 화면이 병원과 다르게 보인다</b>.
|
||||
/// 서식이 틀린 것이 아니므로 값은 건드리지 않고 알리기만 한다.
|
||||
/// </summary>
|
||||
public bool IsMissingFont => !IsMixed && ValueText.Length > 0 && !isInstalled(ValueText);
|
||||
|
||||
/// <summary>경고 문구 — 툴팁으로 보인다</summary>
|
||||
public string MissingHint => $"'{ValueText}' 은(는) 이 PC 에 설치되어 있지 않습니다. "
|
||||
+ "저장되는 값은 그대로지만, 화면에는 대체 글꼴로 그려집니다.";
|
||||
|
||||
public FontFamilyRowViewModel(string label, IReadOnlyList<string> families, Func<string, bool> isInstalled)
|
||||
: base(label)
|
||||
{
|
||||
Families = families;
|
||||
this.isInstalled = isInstalled;
|
||||
}
|
||||
|
||||
protected override void OnValueApplied()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsMissingFont));
|
||||
OnPropertyChanged(nameof(MissingHint));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>선택지 행</summary>
|
||||
public sealed class ChoiceRowViewModel : PropertyRowViewModel
|
||||
{
|
||||
|
||||
@@ -391,6 +391,38 @@
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<!--
|
||||
글꼴 이름 — 고르기도 되고 쓰기도 된다(IsEditable).
|
||||
목록에 가두면 이 PC 에 없는 글꼴을 쓰는 서식을 열었을 때 그 값을 고를 수 없게 되고,
|
||||
칸을 건드리는 순간 사라진다. 병원 PC 와 설치 글꼴이 다른 것이 정상이다.
|
||||
|
||||
UpdateSourceTrigger=LostFocus 인 이유: 커밋 한 번이 문서 전체 딥클론 1회다.
|
||||
글자마다 커밋하면 "맑은 고딕" 여섯 자에 스냅샷 여섯 개가 쌓인다.
|
||||
-->
|
||||
<DataTemplate DataType="{x:Type ins:FontFamilyRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" ToolTip="{Binding Label}"/>
|
||||
<!-- 이 PC 에 없는 글꼴이면 알린다 — 값은 그대로 두고 '화면이 병원과 다르다'만 말한다 -->
|
||||
<TextBlock DockPanel.Dock="Right" Text="⚠" FontSize="13" VerticalAlignment="Center"
|
||||
Margin="4,0,0,0" Foreground="{DynamicResource B.Warning}"
|
||||
ToolTip="{Binding MissingHint}"
|
||||
Visibility="{Binding IsMissingFont, Converter={StaticResource BoolToVisibility}}"/>
|
||||
<Grid>
|
||||
<ComboBox Style="{StaticResource RowCombo}" IsEditable="True" IsTextSearchEnabled="True"
|
||||
ItemsSource="{Binding Families}"
|
||||
Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<!-- 이름을 그 글꼴로 그린다 — 이름만으로는 어떤 모양인지 알 수 없다 -->
|
||||
<TextBlock Text="{Binding}" FontFamily="{Binding}" FontSize="13"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock Style="{StaticResource MixedHintOverlay}" Margin="9,0,0,0"/>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:TextRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" ToolTip="{Binding Label}"/>
|
||||
|
||||
Reference in New Issue
Block a user