■ 한글을 치면 색칠이 글자와 어긋났다 (가장 중요) 색칠 레이어가 글자 위치를 <b>직접 재서</b> 잡고 있었다. 앞부분을 FormattedText 로 측정해 그 폭만큼 오른쪽으로 옮겨 그리는 방식이다. ASCII 에서는 맞았지만 한글에서 무너졌다 — 편집 글꼴(Consolas)에 한글 글리프가 없어 폰트 대체가 일어나는데, TextBox 의 내부 조판과 이쪽 측정이 같은 글꼴·같은 셰이핑을 고르리라는 보장이 없다. 조금만 달라도 뒤로 갈수록 벌어져 색과 글자가 따로 논다. 이제 <b>TextBox 에게 좌표를 묻는다</b>(GetRectFromCharacterIndex). 여백·스크롤·줄 높이·폰트 대체가 전부 TextBox 기준으로 이미 반영돼 오고, 조각마다 위치를 새로 물으므로 오차가 누적되지 않는다. 줄 높이를 손으로 계산하던 코드도 함께 사라졌다. 편집 글꼴 순서도 한글이 있는 것(D2Coding → 굴림체 → Consolas)으로 바꿔 대체 자체를 줄였다. --query-popup 진단에 정렬 검사 2건을 넣었다: 한글이 섞인 줄에서 글자 좌표가 뒤로 밀리지 않는가, 마지막 글자 다음 좌표가 캐럿 자리와 이어지는가. 현재 9/9. ■ 다크 모드인데 제목 표시줄만 밝게 남던 문제 ThemedWindow 스타일은 창 <b>안쪽</b>만 칠한다. 제목 표시줄은 OS 가 그리고 기본값은 Windows 의 테마를 따르므로, 앱을 다크로 바꿔도 Windows 가 라이트면 흰 띠가 남는다. DWM 속성(DWMWA_USE_IMMERSIVE_DARK_MODE)으로 창별로 지정한다. 커스텀 크롬을 직접 그리는 것보다 훨씬 싸고, 최소화·최대화·닫기의 동작과 접근성이 OS 것 그대로 남는다. 창마다 손으로 넣으면 새 대화상자에서 반드시 빠지므로 App 에서 Window 클래스 핸들러로 한 번만 걸었다 — 지금 있는 창과 앞으로 만들 창 모두 자동으로 적용된다. 테마를 바꾸는 순간 이미 떠 있는 창들도 함께 맞춘다(ThemeManager.ApplyChromeToOpenWindows). ■ 목업과 위치 맞춤 앞 배지(아이콘) · 검색칸의 돋보기와 안내 문구 · 안내 상자의 ⓘ 를 넣었다. 갈래 칩 순서는 개수순에서 <b>고정 순서</b>(환자·서식·작업자·컨트롤)로 바꿨다 — 컨트롤 수는 서식마다 달라서 개수순으로 두면 서식을 바꿀 때마다 칩이 자리를 바꿔 손이 기억하지 못한다. 회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬 점검 9/9, 검증 실행 점검 10/10, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
3.4 KiB
C#
97 lines
3.4 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Windows;
|
|
|
|
namespace SheetMe.Designer.Services;
|
|
|
|
/// <summary>
|
|
/// 라이트/다크 테마 전환 — App.Resources 의 토큰 사전(Tokens.Dark↔Light)을 교체하면
|
|
/// {DynamicResource B.*} 를 참조하는 모든 스타일이 라이브 리스킨된다([200]SheetMe SwapThemeTokens 이식).
|
|
/// 선택은 %LocalAppData%\SheetMe\theme.json 에 보존.
|
|
/// </summary>
|
|
public static class ThemeManager
|
|
{
|
|
#region Member Fields
|
|
private static readonly string SettingsPath =
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SheetMe", "theme.json");
|
|
#endregion
|
|
|
|
#region Properties
|
|
/// <summary>현재 라이트 테마 여부(기본 다크 — [200]SheetMe 기본값과 동일)</summary>
|
|
public static bool IsLight { get; private set; }
|
|
#endregion
|
|
|
|
#region Methods
|
|
/// <summary>시작 시 저장된 테마 적용</summary>
|
|
public static void LoadSaved()
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(SettingsPath) &&
|
|
JsonDocument.Parse(File.ReadAllText(SettingsPath)).RootElement.TryGetProperty("theme", out var theme) &&
|
|
theme.GetString() == "light")
|
|
{
|
|
Apply(light: true);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 설정 손상 시 기본(다크) 유지
|
|
}
|
|
}
|
|
|
|
/// <summary>라이트↔다크 토글</summary>
|
|
public static void Toggle() => Apply(!IsLight);
|
|
|
|
/// <summary>
|
|
/// 열려 있는 모든 창의 제목 표시줄을 지금 테마에 맞춘다.
|
|
///
|
|
/// 제목 표시줄은 창 안쪽 자원(B.*)이 아니라 OS 가 그리므로 사전을 바꿔도 따라오지 않는다.
|
|
/// 테마를 바꾼 순간 이미 떠 있는 창들도 함께 맞춰 줘야 위쪽만 밝은 창이 남지 않는다.
|
|
/// </summary>
|
|
public static void ApplyChromeToOpenWindows()
|
|
{
|
|
foreach (Window window in Application.Current.Windows)
|
|
{
|
|
WindowChromeTheme.Apply(window, !IsLight);
|
|
}
|
|
}
|
|
|
|
/// <summary>테마 적용 — App.Resources 병합 사전에서 토큰 dict 를 찾아 교체</summary>
|
|
public static void Apply(bool light)
|
|
{
|
|
IsLight = light;
|
|
var dictionaries = Application.Current.Resources.MergedDictionaries;
|
|
var tokensUri = new Uri($"/Themes/Tokens.{(light ? "Light" : "Dark")}.xaml", UriKind.Relative);
|
|
for (var i = 0; i < dictionaries.Count; i++)
|
|
{
|
|
var source = dictionaries[i].Source?.OriginalString ?? string.Empty;
|
|
if (source.EndsWith("Tokens.Dark.xaml", StringComparison.OrdinalIgnoreCase) ||
|
|
source.EndsWith("Tokens.Light.xaml", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
dictionaries[i] = new ResourceDictionary { Source = tokensUri };
|
|
Save();
|
|
ApplyChromeToOpenWindows();
|
|
return;
|
|
}
|
|
}
|
|
dictionaries.Add(new ResourceDictionary { Source = tokensUri });
|
|
Save();
|
|
ApplyChromeToOpenWindows();
|
|
}
|
|
|
|
private static void Save()
|
|
{
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
|
|
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(new { theme = IsLight ? "light" : "dark" }));
|
|
}
|
|
catch
|
|
{
|
|
// 저장 실패는 무시(다음 실행 기본 테마)
|
|
}
|
|
}
|
|
#endregion
|
|
}
|