메인 화면을 게이트에 넣고, 최소 크기를 단일 출처로, 창 크기를 기억한다
재설계 1단계 계속. <b>메인 셸이 게이트에 아예 없었다.</b> 사용자가 하루 종일 보는 화면만 자동 검증 밖이었다. 넣고 나서 글자 31개만 검사되는 것을 보고 빈 껍데기를 찍고 있다는 것을 알았다 — 컨트롤을 놓고 하나를 골라 인스펙터·레이어를 채우니 81~95개가 된다. 세 크기·탭으로 찍는다: 기본 1440x920, 현장 최대화 1920x1032, 도구 상자 탭. Loaded 커맨드(서식 목록 DB 조회)는 진단 모드에서 돌지 않게 막았다 — 게이트 결과가 접속 상태에 따라 달라지면 안 된다. <b>게이트가 게이트의 오류를 잡았다.</b> 메인 화면을 넣자 세로잘림 30건이 떴는데 전부 오탐이었다. DesiredSize 는 Margin 을 포함하고 RenderSize 는 포함하지 않아서, 세로 Margin 이 있는 TextBlock 이 예외 없이 걸린 것이다(도구 상자 그룹 머리가 "필요 27 > 실제 14" 였고 그 13 이 Margin). Margin 을 빼고 비교하도록 고쳤다. 이 검사기에서 측정이 나를 정정한 세 번째다. <b>--maxrect 가 최소 크기를 베껴 두고 있었다.</b> ProbeMinWidth=940 / ProbeMinHeight=640 / ProbeBorder=6 은 MainView.xaml 의 복사본이라, 최소 크기를 바꾸면 이 검사가 <b>옛 값을 계속 검사하며 계속 통과한다</b>. 실제 MainView 를 만들어 XAML 이 정한 값을 읽는다. <b>창 크기·위치·상태·두 패널 폭을 기억한다.</b> UserPrefs 가 Dictionary<string,bool> 이라 숫자를 못 담던 것이 원인이었다. JsonElement 로 바꿔 이미 깔린 prefs.json 이 살아남게 했다 — string 이나 double 로 바꾸면 역직렬화가 터져 기존 취향이 통째로 초기화된다. 복원값은 반드시 클램프한다. 이 앱은 WindowStyle=None 이라 제목 표시줄을 직접 그리므로, 저장된 위치가 지금 화면에 없으면(회사에서 두 번째 모니터, 집에서 노트북) 메뉴·저장·닫기가 전부 손에 닿지 않고 마우스로 되돌릴 방법이 없다. 기본 크기로는 이 문제가 안 나지만, 저장을 시작하는 순간 복원되는 것은 사용자가 만든 값이다. 닫기를 물렀을 때는 저장하지 않는다 — 조작하지 않은 값이 남는다. 게이트: 테스트 277/277, --dialog-shots 대조군 4/4 · 글자 2,465개 검사 · 넘침 0건, --maxrect 0실패(MainView 실측 940x640 테두리 6), --cleartype 11/11, --edit-smoke 0실패, --modal-check 0실패, 빌드 경고 0, --db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
862657cf2f
commit
4150ebe1d7
@@ -41,6 +41,7 @@ public partial class App : Application
|
||||
if (Services.StartupArguments.IsDiagnostic(e.Args))
|
||||
{
|
||||
diagnosticMode = true;
|
||||
Services.StartupArguments.DiagnosticMode = true;
|
||||
// 기본 ShutdownMode 는 OnLastWindowClose 다. 창을 띄웠다 전부 닫는 진단
|
||||
// (--maxrect, --edit-smoke, --dialog-shots …)은 마지막 창을 닫는 순간 WPF 가 먼저
|
||||
// Shutdown(0) 을 걸어 버리고, 뒤이은 Shutdown(RunDiagnostic(...)) 인자는 무시된다 —
|
||||
|
||||
@@ -120,6 +120,13 @@ public static class DialogShots
|
||||
var designer = SampleDesigner();
|
||||
return new List<(string, Func<Window>)>
|
||||
{
|
||||
// 메인 셸 — 이 게이트에 <b>아예 없었다</b>. 사용자가 하루 종일 보는 화면만 자동 검증 밖이었다.
|
||||
// 두 크기로 찍는다: 기본 창(1440×920)과 현장 단말 최대화 크기(1920×1032).
|
||||
// 현장이 1920×1080 이상뿐이라는 답을 받았으므로 그 사각형이 실제 기준이다.
|
||||
("00-main-default", () => NewMainShell(1440, 920)),
|
||||
("00b-main-1920", () => NewMainShell(1920, 1032)),
|
||||
// 도구 상자 탭은 타일 라벨이 좁은 2열에 들어가므로 잘림이 가장 잘 나는 자리다
|
||||
("00c-main-toolbox", () => NewMainShell(1440, 920, leftTab: 2)),
|
||||
("01-sheet-open", () => new Views.SheetOpenDialogView(_ => SampleSheets())),
|
||||
("02-sheet-history", () => new Views.SheetHistoryDialogView("S999", "표본 서식", SampleVersions())),
|
||||
("03-query-editor", () => NewQueryEditor(designer,
|
||||
@@ -203,6 +210,58 @@ public static class DialogShots
|
||||
/// MainViewModel 을 통째로 만들면 DB 조회가 딸려 오므로, 패널이 읽는 <c>CurrentDesigner</c> 하나만
|
||||
/// 들고 있는 최소 DataContext 를 세워 준다.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 메인 셸을 주어진 크기로 만든다.
|
||||
///
|
||||
/// Loaded 커맨드(서식 목록 DB 조회)는 <see cref="StartupArguments.DiagnosticMode"/> 때문에 돌지 않으므로
|
||||
/// 접속 없이도 같은 결과가 나온다. 문서 한 장을 넣어 캔버스·레이어·인스펙터가 빈 상태가 아니게 한다 —
|
||||
/// 빈 껍데기를 찍으면 잘림을 볼 수 없다.
|
||||
/// </summary>
|
||||
private static Window NewMainShell(double width, double height, int leftTab = 1)
|
||||
{
|
||||
var window = new Views.MainView
|
||||
{
|
||||
Width = width,
|
||||
Height = height,
|
||||
Left = -10000,
|
||||
Top = -10000,
|
||||
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||
};
|
||||
if (window.DataContext is not MainViewModel shell)
|
||||
{
|
||||
return window;
|
||||
}
|
||||
shell.NewFileCommand?.Execute(null);
|
||||
shell.SelectedLeftTabIndex = leftTab;
|
||||
|
||||
// 컨트롤을 놓고 하나를 고른다 — 이걸 안 하면 인스펙터가 비어 있고 레이어에 행이 없어서
|
||||
// 검사할 글자가 31개밖에 안 나온다(빈 껍데기를 찍으면 잘림을 볼 수 없다).
|
||||
// 이름이 긴 컨트롤을 섞는다 — 좁은 칸에서 잘리는 것은 짧은 이름이 아니라 긴 이름이다.
|
||||
if (shell.CurrentDesigner is { } designer)
|
||||
{
|
||||
foreach (var type in new[] { "Label", "TextBox", "CheckBox", "MDataTable", "MPictureBox" })
|
||||
{
|
||||
designer.AddPaletteItemAtCenter(type);
|
||||
}
|
||||
foreach (var control in designer.Pages[0].Controls)
|
||||
{
|
||||
if (control.Type == "Label")
|
||||
{
|
||||
control.Model.Id = "아주긴이름을가진라벨컨트롤_LAB_00123";
|
||||
control.Model.Props.SetText("Text",
|
||||
"잘림을 보려면 충분히 긴 문구가 필요하다 — 간호·간병통합서비스 병동");
|
||||
control.NotifyAllChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (designer.Pages[0].Controls.FirstOrDefault() is { } first)
|
||||
{
|
||||
designer.Selection.SetSingle(first);
|
||||
}
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
private static Window HostLayerPanel(DesignerViewModel designer, LayerShotMode mode)
|
||||
{
|
||||
designer.Selection.Clear();
|
||||
|
||||
@@ -36,9 +36,20 @@ public static class MaximizeCheck
|
||||
private const uint AbmGetAutoHideBarEx = 0x0000000B;
|
||||
private const int AbsAutoHide = 0x00000001;
|
||||
|
||||
private const int ProbeMinWidth = 940;
|
||||
private const int ProbeMinHeight = 640;
|
||||
private const double ProbeBorder = 6;
|
||||
/// <summary>
|
||||
/// 검사에 쓰는 최소 크기·테두리는 <b>실제 MainView 에서 읽는다</b>.
|
||||
///
|
||||
/// 전에는 940 / 640 / 6 을 상수로 베껴 두었다. 그러면 MainView 의 최소 크기를 바꿨을 때
|
||||
/// 이 검사가 <b>옛 값을 계속 검사하며 계속 통과한다</b> — 게이트가 있는데 아무것도 지키지 않는 상태다.
|
||||
/// 창을 하나 만들어(띄우지 않는다) XAML 이 정한 값을 그대로 가져온다.
|
||||
/// </summary>
|
||||
private static (double MinWidth, double MinHeight, double Border) Real()
|
||||
{
|
||||
var main = new Views.MainView();
|
||||
var chrome = WindowChrome.GetWindowChrome(main);
|
||||
var border = chrome?.ResizeBorderThickness.Left ?? 6;
|
||||
return (main.MinWidth, main.MinHeight, border);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RECT
|
||||
@@ -152,7 +163,9 @@ public static class MaximizeCheck
|
||||
var failures = 0;
|
||||
|
||||
// 프로브는 WindowStyle=None 이어야 한다 — 기본값이면 Attach 의 가드에 걸려 아무 일도 안 일어난다
|
||||
var probe = NewProbe(ProbeMinWidth, ProbeMinHeight);
|
||||
var real = Real();
|
||||
report.AppendLine($"MainView 실측 — 최소 {real.MinWidth}x{real.MinHeight}, 리사이즈 테두리 {real.Border}");
|
||||
var probe = NewProbe(real.MinWidth, real.MinHeight);
|
||||
var handle = new WindowInteropHelper(probe).EnsureHandle();
|
||||
Services.MaximizeToWorkArea.Attach(probe);
|
||||
|
||||
@@ -226,8 +239,9 @@ public static class MaximizeCheck
|
||||
private static int Live(StringBuilder report, MonitorFacts monitor)
|
||||
{
|
||||
var failures = 0;
|
||||
var chrome = new WindowChrome { CaptionHeight = 40, ResizeBorderThickness = new Thickness(ProbeBorder) };
|
||||
var probe = NewProbe(ProbeMinWidth, ProbeMinHeight);
|
||||
var real = Real();
|
||||
var chrome = new WindowChrome { CaptionHeight = 40, ResizeBorderThickness = new Thickness(real.Border) };
|
||||
var probe = NewProbe(real.MinWidth, real.MinHeight);
|
||||
WindowChrome.SetWindowChrome(probe, chrome);
|
||||
probe.WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
probe.Left = monitor.Bounds.Left + 40;
|
||||
@@ -263,7 +277,7 @@ public static class MaximizeCheck
|
||||
var handle = new WindowInteropHelper(probe).Handle;
|
||||
probe.MinHeight = workHeight + 200;
|
||||
var cramped = Ask(handle);
|
||||
probe.MinHeight = ProbeMinHeight;
|
||||
probe.MinHeight = real.MinHeight;
|
||||
probe.Close();
|
||||
|
||||
report.AppendLine($"좁은 화면 흉내 — 최소 높이 {workHeight + 200} 요구, 작업영역 높이 {workHeight}");
|
||||
|
||||
@@ -181,10 +181,16 @@ internal static class OverflowCheck
|
||||
}
|
||||
|
||||
// ② 세로로 잘림 — 줄바꿈이 일어났는데 높이를 안 준 경우다.
|
||||
if (text.DesiredSize.Height - text.RenderSize.Height > Slack)
|
||||
//
|
||||
// <b>Margin 을 빼야 한다.</b> DesiredSize 는 Margin 을 포함하고 RenderSize 는 포함하지 않는다.
|
||||
// 그래서 그냥 비교하면 세로 Margin 이 있는 TextBlock 이 예외 없이 걸린다 —
|
||||
// 처음에 그렇게 만들어 30건이 떴고, 전부 오탐이었다
|
||||
// (도구 상자 그룹 머리가 "필요 27 > 실제 14" 로 나왔는데 그 13 이 Margin 이었다).
|
||||
var needHeight = text.DesiredSize.Height - text.Margin.Top - text.Margin.Bottom;
|
||||
if (needHeight - text.RenderSize.Height > Slack)
|
||||
{
|
||||
found.Add(new Violation("세로잘림", label,
|
||||
$"{owner} 필요 {text.DesiredSize.Height:0.#} > 실제 {text.RenderSize.Height:0.#}"));
|
||||
$"{owner} 필요 {needHeight:0.#} > 실제 {text.RenderSize.Height:0.#}"));
|
||||
}
|
||||
|
||||
// ③ 자기를 자르는 조상 밖으로 삐져나갔는가.
|
||||
|
||||
@@ -16,6 +16,14 @@ public static class StartupArguments
|
||||
{
|
||||
#region Methods
|
||||
/// <summary>진단 플래그 기동인지 — 첫 인자가 "--" 로 시작</summary>
|
||||
/// <summary>
|
||||
/// 이번 실행이 진단 모드인가. App 이 기동 때 한 번 정하고, 화면 쪽에서 읽는다.
|
||||
///
|
||||
/// 화면이 이것을 봐야 하는 이유: 게이트가 MainView 를 띄울 때 Loaded 커맨드가 DB 를 치면
|
||||
/// 검사 결과가 접속 상태에 따라 달라진다. 게이트는 DB 없이 같은 답을 내야 한다.
|
||||
/// </summary>
|
||||
public static bool DiagnosticMode { get; set; }
|
||||
|
||||
public static bool IsDiagnostic(string[] args)
|
||||
=> args.Length > 0 && args[0].StartsWith("--", StringComparison.Ordinal);
|
||||
|
||||
|
||||
@@ -16,7 +16,14 @@ public static class UserPrefs
|
||||
private static readonly string SettingsPath =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SheetMe", "prefs.json");
|
||||
|
||||
private static Dictionary<string, bool> values = new(StringComparer.Ordinal);
|
||||
/// <summary>
|
||||
/// 값 형식이 <see cref="JsonElement"/> 인 이유: 전에는 <c>Dictionary<string, bool></c> 였고
|
||||
/// 그래서 숫자를 담을 수 없었다(창 크기·패널 폭을 저장하지 못한 원인이다).
|
||||
/// 여기서 형식을 <c>string</c> 이나 <c>double</c> 로 바꾸면 이미 깔린 prefs.json 이
|
||||
/// 역직렬화에서 터져 <b>기존 취향이 통째로 초기화된다</b>.
|
||||
/// JsonElement 는 <c>false</c> 와 <c>640</c> 을 모두 그대로 받으므로 옛 파일이 살아남는다.
|
||||
/// </summary>
|
||||
private static Dictionary<string, JsonElement> values = new(StringComparer.Ordinal);
|
||||
private static bool loaded;
|
||||
#endregion
|
||||
|
||||
@@ -25,21 +32,58 @@ public static class UserPrefs
|
||||
public static bool GetBool(string key, bool fallback)
|
||||
{
|
||||
EnsureLoaded();
|
||||
return values.TryGetValue(key, out var value) ? value : fallback;
|
||||
return values.TryGetValue(key, out var value)
|
||||
&& value.ValueKind is JsonValueKind.True or JsonValueKind.False
|
||||
? value.GetBoolean()
|
||||
: fallback;
|
||||
}
|
||||
|
||||
/// <summary>불리언 취향 저장</summary>
|
||||
public static void SetBool(string key, bool value)
|
||||
{
|
||||
EnsureLoaded();
|
||||
if (values.TryGetValue(key, out var current) && current == value)
|
||||
if (GetBool(key, !value) == value && values.ContainsKey(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
values[key] = value;
|
||||
values[key] = JsonSerializer.SerializeToElement(value);
|
||||
Save();
|
||||
}
|
||||
|
||||
/// <summary>숫자 취향 읽기 — 미저장이거나 형식이 다르면 기본값</summary>
|
||||
public static double GetDouble(string key, double fallback)
|
||||
{
|
||||
EnsureLoaded();
|
||||
return values.TryGetValue(key, out var value)
|
||||
&& value.ValueKind == JsonValueKind.Number
|
||||
&& value.TryGetDouble(out var number)
|
||||
&& double.IsFinite(number)
|
||||
? number
|
||||
: fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 숫자 취향 여러 개를 한 번에 저장 — 창을 닫을 때 6개를 각각 저장하면 파일을 6번 쓴다.
|
||||
/// </summary>
|
||||
public static void SetDoubles(params (string Key, double Value)[] entries)
|
||||
{
|
||||
EnsureLoaded();
|
||||
var changed = false;
|
||||
foreach (var (key, value) in entries)
|
||||
{
|
||||
if (!double.IsFinite(value) || Math.Abs(GetDouble(key, double.NaN) - value) < 0.5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
values[key] = JsonSerializer.SerializeToElement(Math.Round(value, 1));
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureLoaded()
|
||||
{
|
||||
if (loaded)
|
||||
@@ -51,8 +95,8 @@ public static class UserPrefs
|
||||
{
|
||||
if (File.Exists(SettingsPath))
|
||||
{
|
||||
values = JsonSerializer.Deserialize<Dictionary<string, bool>>(File.ReadAllText(SettingsPath))
|
||||
?? new Dictionary<string, bool>(StringComparer.Ordinal);
|
||||
values = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(File.ReadAllText(SettingsPath))
|
||||
?? new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 창 크기·위치·패널 폭을 실행 사이에 기억한다.
|
||||
///
|
||||
/// <b>왜 필요한가.</b> 전에는 아무것도 기억하지 않아서, 창을 옮기거나 패널 폭을 조절해도
|
||||
/// 다음 실행에 원래대로 돌아갔다. 하루 종일 쓰는 사람에게 그 조작은 매일 반복하는 일이 된다.
|
||||
///
|
||||
/// <b>복원값을 반드시 클램프한다.</b> 저장된 사각형이 지금 화면에 없을 수 있다 —
|
||||
/// 회사에서 두 번째 모니터를 쓰다 집에서 노트북만 켜면, 저장된 위치가 존재하지 않는 화면을 가리킨다.
|
||||
/// 이 앱은 <c>WindowStyle="None"</c> 이라 OS 캡션이 없고 제목 표시줄을 직접 그리므로,
|
||||
/// 창이 화면 위쪽으로 나가면 <b>메뉴·저장·닫기가 전부 손에 닿지 않는다</b>(마우스로 되돌릴 방법이 없다).
|
||||
/// 그래서 복원 전에 작업영역과 교차 검사를 한다.
|
||||
///
|
||||
/// 현장 단말이 1920x1080 이상이라 기본 크기(1440x920)로는 이 문제가 나지 않는다.
|
||||
/// 그래도 클램프를 두는 이유는, 저장을 시작하는 순간 <b>사용자가 만든 값</b>이 복원되기 때문이다 —
|
||||
/// 기본값이 안전한 것과 저장된 값이 안전한 것은 다른 문제다.
|
||||
/// </summary>
|
||||
public static class WindowPlacement
|
||||
{
|
||||
#region Member Fields
|
||||
private const string KeyLeft = "Window.Left";
|
||||
private const string KeyTop = "Window.Top";
|
||||
private const string KeyWidth = "Window.Width";
|
||||
private const string KeyHeight = "Window.Height";
|
||||
private const string KeyMaximized = "Window.Maximized";
|
||||
private const string KeyLeftPanel = "Panel.Left.Width";
|
||||
private const string KeyRightPanel = "Panel.Right.Width";
|
||||
|
||||
/// <summary>제목 표시줄을 잡을 수 있어야 하는 최소 노출 폭·높이</summary>
|
||||
private const double Grip = 120;
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>
|
||||
/// 저장된 사각형을 복원한다 — 화면 밖이면 버리고 기본값을 쓴다.
|
||||
/// <c>Show()</c> 전에 불러야 한다(보인 뒤에 옮기면 한 프레임 번쩍인다).
|
||||
/// </summary>
|
||||
public static void Restore(Window window)
|
||||
{
|
||||
var width = UserPrefs.GetDouble(KeyWidth, window.Width);
|
||||
var height = UserPrefs.GetDouble(KeyHeight, window.Height);
|
||||
var left = UserPrefs.GetDouble(KeyLeft, double.NaN);
|
||||
var top = UserPrefs.GetDouble(KeyTop, double.NaN);
|
||||
|
||||
window.Width = Math.Max(window.MinWidth, width);
|
||||
window.Height = Math.Max(window.MinHeight, height);
|
||||
|
||||
if (double.IsNaN(left) || double.IsNaN(top))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var work = SystemParameters.WorkArea;
|
||||
var rect = new Rect(left, top, window.Width, window.Height);
|
||||
// 어느 화면에도 충분히 걸치지 않으면 저장값을 버린다. 여러 모니터를 정확히 다루려면
|
||||
// 모니터 열거가 필요하지만, 주 화면 작업영역과의 교차만으로도 '손에 닿지 않는 창'은 막힌다.
|
||||
if (rect.Right < work.Left + Grip || rect.Left > work.Right - Grip
|
||||
|| rect.Bottom < work.Top + Grip || rect.Top > work.Bottom - Grip)
|
||||
{
|
||||
return;
|
||||
}
|
||||
window.WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
window.Left = left;
|
||||
// 위로는 절대 넘기지 않는다 — 제목 표시줄이 화면 밖으로 나가면 되돌릴 수 없다
|
||||
window.Top = Math.Max(work.Top, top);
|
||||
|
||||
if (UserPrefs.GetBool(KeyMaximized, false))
|
||||
{
|
||||
window.WindowState = WindowState.Maximized;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>지금 상태를 저장한다 — 창을 닫을 때 한 번 부른다</summary>
|
||||
public static void Save(Window window, double leftPanel, double rightPanel)
|
||||
{
|
||||
UserPrefs.SetBool(KeyMaximized, window.WindowState == WindowState.Maximized);
|
||||
// 최대화 중이면 Left/Top/Width/Height 는 '복원했을 때의 값'이라 그대로 쓰는 것이 맞다.
|
||||
// 최소화 중이면 그 값이 쓰레기이므로 건드리지 않는다.
|
||||
if (window.WindowState == WindowState.Minimized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var bounds = window.WindowState == WindowState.Maximized
|
||||
? window.RestoreBounds
|
||||
: new Rect(window.Left, window.Top, window.ActualWidth, window.ActualHeight);
|
||||
UserPrefs.SetDoubles(
|
||||
(KeyLeft, bounds.Left), (KeyTop, bounds.Top),
|
||||
(KeyWidth, bounds.Width), (KeyHeight, bounds.Height),
|
||||
(KeyLeftPanel, leftPanel), (KeyRightPanel, rightPanel));
|
||||
}
|
||||
|
||||
/// <summary>패널 폭 복원 — 0 이면 XAML 기본값을 그대로 둔다는 뜻</summary>
|
||||
public static double LeftPanelWidth(double fallback) => UserPrefs.GetDouble(KeyLeftPanel, fallback);
|
||||
|
||||
public static double RightPanelWidth(double fallback) => UserPrefs.GetDouble(KeyRightPanel, fallback);
|
||||
#endregion
|
||||
}
|
||||
@@ -314,13 +314,13 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- [200] 기본 폭 — 좌 248(레이어 행에 타입 아이콘 + 토글 2개가 들어가 232 로는 이름이 잘린다),
|
||||
우 300(인스펙터 X|Y 쌍이 한 줄에 들어가는 최소치) -->
|
||||
<ColumnDefinition Width="248" MinWidth="180" MaxWidth="480"/>
|
||||
<ColumnDefinition x:Name="LeftColumn" Width="248" MinWidth="180" MaxWidth="480"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- 최소 250 — 글꼴 스타일 행이 라벨 92 + 토글 4칸 128 = 220px 를 요구한다
|
||||
(행 가용폭 = 폭 − 스크롤바 9 − 여백 20). 220 이면 마지막 토글이 잘린다. -->
|
||||
<ColumnDefinition Width="300" MinWidth="250" MaxWidth="560"/>
|
||||
<ColumnDefinition x:Name="RightColumn" Width="300" MinWidth="250" MaxWidth="560"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 좌: 알약 탭 — 서식 목록 / 레이어 / 도구 상자 -->
|
||||
|
||||
@@ -17,6 +17,16 @@ public partial class MainView : Window
|
||||
// StaysOpen=False 팝업은 바깥 클릭(버튼 포함)으로 먼저 닫힌다 — 토글 버튼 재클릭이 곧바로 다시 열지 않게 닫힌 시각 기록
|
||||
AddControlPopup.Closed += (_, _) => addControlPopupClosedAt = Environment.TickCount;
|
||||
ZoomPopup.Closed += (_, _) => zoomPopupClosedAt = Environment.TickCount;
|
||||
|
||||
// 진단(게이트)은 창 크기를 스스로 정한다 — 저장된 취향이 끼어들면 샷 크기가 PC 마다 달라진다
|
||||
if (StartupArguments.DiagnosticMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Show 전에 복원해야 한 프레임 번쩍이지 않는다
|
||||
WindowPlacement.Restore(this);
|
||||
LeftColumn.Width = new GridLength(WindowPlacement.LeftPanelWidth(LeftColumn.Width.Value));
|
||||
RightColumn.Width = new GridLength(WindowPlacement.RightPanelWidth(RightColumn.Width.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -36,6 +46,12 @@ public partial class MainView : Window
|
||||
private void OnWindowLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThemeMenuItem.IsChecked = !ThemeManager.IsLight;
|
||||
// 진단(게이트) 실행에서는 Loaded 커맨드를 돌리지 않는다 — 서식 목록 DB 조회가 붙어 있어서
|
||||
// 검사 결과가 접속 상태와 조회 결과에 따라 달라진다. 게이트는 DB 없이 같은 답을 내야 한다.
|
||||
if (StartupArguments.DiagnosticMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (DataContext is MainViewModel viewModel && viewModel.LoadedCommand?.CanExecute(null) == true)
|
||||
{
|
||||
viewModel.LoadedCommand.Execute(null);
|
||||
@@ -127,6 +143,12 @@ public partial class MainView : Window
|
||||
if (DataContext is MainViewModel viewModel && !viewModel.IsShuttingDown && !viewModel.ConfirmCloseAll())
|
||||
{
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
// 취소되지 않은 뒤에만 저장한다 — 닫기를 물렀는데 크기가 기억되면 조작하지 않은 값이 남는다
|
||||
if (!StartupArguments.DiagnosticMode)
|
||||
{
|
||||
WindowPlacement.Save(this, LeftColumn.ActualWidth, RightColumn.ActualWidth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user