Skip to content

Commit cc826a5

Browse files
committed
feat: implement window state persistence and restore functionality
- Added PersistedWindowState record to manage window properties. - Implemented methods to save and load main window position and compact state. - Enhanced MainWindow to save its position and state on close and restore on open. - Introduced functionality to save and restore open secondary windows. - Updated WindowHelper with methods to position windows within the visible area.
1 parent 4c10ed9 commit cc826a5

4 files changed

Lines changed: 434 additions & 79 deletions

File tree

‎AppSettings.cs‎

Lines changed: 168 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ public record BackdropSettings(
1414
string FallbackColor = "#000000",
1515
string Kind = "Base");
1616

17+
public record PersistedWindowState(
18+
string Type,
19+
string NoteId,
20+
int X,
21+
int Y,
22+
bool IsCompact = false);
23+
1724
public static class AppSettings
1825
{
1926
private static readonly string SettingsDir =
@@ -294,6 +301,47 @@ public static string LoadThemeSetting()
294301
return "system";
295302
}
296303

304+
public static (int X, int Y)? LoadMainWindowPosition()
305+
{
306+
try
307+
{
308+
if (!File.Exists(SettingsPath))
309+
return null;
310+
311+
var json = File.ReadAllText(SettingsPath);
312+
var doc = JsonDocument.Parse(json);
313+
var root = doc.RootElement;
314+
if (root.TryGetProperty("mainWindowX", out var xProp) &&
315+
root.TryGetProperty("mainWindowY", out var yProp) &&
316+
xProp.TryGetInt32(out var x) &&
317+
yProp.TryGetInt32(out var y))
318+
{
319+
return (x, y);
320+
}
321+
}
322+
catch { }
323+
return null;
324+
}
325+
326+
public static bool LoadMainWindowCompact()
327+
{
328+
try
329+
{
330+
if (!File.Exists(SettingsPath))
331+
return false;
332+
333+
var json = File.ReadAllText(SettingsPath);
334+
var doc = JsonDocument.Parse(json);
335+
if (doc.RootElement.TryGetProperty("mainWindowCompact", out var compactProp) &&
336+
(compactProp.ValueKind == JsonValueKind.True || compactProp.ValueKind == JsonValueKind.False))
337+
{
338+
return compactProp.GetBoolean();
339+
}
340+
}
341+
catch { }
342+
return false;
343+
}
344+
297345
public static void SaveBackdropSettings(BackdropSettings settings)
298346
{
299347
MergeAndSaveSettings(new Dictionary<string, object>
@@ -312,6 +360,107 @@ public static void SaveThemeSetting(string theme)
312360
MergeAndSaveSettings(new Dictionary<string, object> { ["theme"] = theme });
313361
}
314362

363+
public static void SaveMainWindowPosition(Windows.Graphics.PointInt32 position)
364+
{
365+
MergeAndSaveSettings(new Dictionary<string, object>
366+
{
367+
["mainWindowX"] = position.X,
368+
["mainWindowY"] = position.Y
369+
});
370+
}
371+
372+
public static void SaveMainWindowCompact(bool isCompact)
373+
{
374+
MergeAndSaveSettings(new Dictionary<string, object>
375+
{
376+
["mainWindowCompact"] = isCompact
377+
});
378+
}
379+
380+
public static List<PersistedWindowState> LoadOpenWindows()
381+
{
382+
var result = new List<PersistedWindowState>();
383+
try
384+
{
385+
if (!File.Exists(SettingsPath))
386+
return result;
387+
388+
var json = File.ReadAllText(SettingsPath);
389+
var doc = JsonDocument.Parse(json);
390+
if (!doc.RootElement.TryGetProperty("openWindows", out var windowsProp))
391+
{
392+
return result;
393+
}
394+
395+
var arrayElement = windowsProp;
396+
JsonDocument? nestedDoc = null;
397+
if (windowsProp.ValueKind == JsonValueKind.String)
398+
{
399+
var raw = windowsProp.GetString();
400+
if (!string.IsNullOrWhiteSpace(raw))
401+
{
402+
nestedDoc = JsonDocument.Parse(raw);
403+
arrayElement = nestedDoc.RootElement;
404+
}
405+
}
406+
407+
if (arrayElement.ValueKind != JsonValueKind.Array)
408+
{
409+
nestedDoc?.Dispose();
410+
return result;
411+
}
412+
413+
foreach (var item in arrayElement.EnumerateArray())
414+
{
415+
if (item.ValueKind != JsonValueKind.Object)
416+
continue;
417+
418+
var type = item.TryGetProperty("type", out var typeProp)
419+
? typeProp.GetString() ?? ""
420+
: "";
421+
var noteId = item.TryGetProperty("noteId", out var noteIdProp)
422+
? noteIdProp.GetString() ?? ""
423+
: "";
424+
if (!item.TryGetProperty("x", out var xProp) ||
425+
!item.TryGetProperty("y", out var yProp) ||
426+
!xProp.TryGetInt32(out var x) ||
427+
!yProp.TryGetInt32(out var y) ||
428+
string.IsNullOrWhiteSpace(type))
429+
{
430+
continue;
431+
}
432+
433+
var isCompact =
434+
item.TryGetProperty("isCompact", out var compactProp) &&
435+
(compactProp.ValueKind == JsonValueKind.True || compactProp.ValueKind == JsonValueKind.False) &&
436+
compactProp.GetBoolean();
437+
438+
result.Add(new PersistedWindowState(type, noteId, x, y, isCompact));
439+
}
440+
441+
nestedDoc?.Dispose();
442+
}
443+
catch { }
444+
return result;
445+
}
446+
447+
public static void SaveOpenWindows(IEnumerable<PersistedWindowState> windows)
448+
{
449+
var payload = windows.Select(w => new Dictionary<string, object>
450+
{
451+
["type"] = w.Type,
452+
["noteId"] = w.NoteId,
453+
["x"] = w.X,
454+
["y"] = w.Y,
455+
["isCompact"] = w.IsCompact
456+
}).ToList();
457+
458+
MergeAndSaveSettings(new Dictionary<string, object>
459+
{
460+
["openWindows"] = payload
461+
});
462+
}
463+
315464
private static void MergeAndSaveSettings(Dictionary<string, object> newValues)
316465
{
317466
try
@@ -324,14 +473,7 @@ private static void MergeAndSaveSettings(Dictionary<string, object> newValues)
324473
foreach (var prop in doc.RootElement.EnumerateObject())
325474
{
326475
if (newValues.ContainsKey(prop.Name)) continue;
327-
existing[prop.Name] = prop.Value.ValueKind switch
328-
{
329-
JsonValueKind.Number => prop.Value.TryGetInt64(out var l) ? l : prop.Value.GetDouble(),
330-
JsonValueKind.String => prop.Value.GetString()!,
331-
JsonValueKind.True => true,
332-
JsonValueKind.False => false,
333-
_ => prop.Value.GetRawText()
334-
};
476+
existing[prop.Name] = ConvertJsonElement(prop.Value) ?? "";
335477
}
336478
}
337479

@@ -344,6 +486,24 @@ private static void MergeAndSaveSettings(Dictionary<string, object> newValues)
344486
catch { }
345487
}
346488

489+
private static object? ConvertJsonElement(JsonElement value)
490+
{
491+
return value.ValueKind switch
492+
{
493+
JsonValueKind.Object => value.EnumerateObject()
494+
.ToDictionary(p => p.Name, p => ConvertJsonElement(p.Value)),
495+
JsonValueKind.Array => value.EnumerateArray()
496+
.Select(ConvertJsonElement)
497+
.ToList(),
498+
JsonValueKind.String => value.GetString(),
499+
JsonValueKind.Number => value.TryGetInt64(out var l) ? l : value.GetDouble(),
500+
JsonValueKind.True => true,
501+
JsonValueKind.False => false,
502+
JsonValueKind.Null => null,
503+
_ => value.GetRawText()
504+
};
505+
}
506+
347507
public static Windows.UI.Color ParseColor(string hex)
348508
{
349509
hex = hex.TrimStart('#');

‎MainWindow.xaml.cs‎

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ public MainWindow()
6060

6161
WindowHelper.RemoveWindowBorder(this);
6262
AppWindow.Resize(new Windows.Graphics.SizeInt32(380, 650));
63-
WindowHelper.CenterOnScreen(this);
63+
var savedMainPos = AppSettings.LoadMainWindowPosition();
64+
if (savedMainPos is { } pos)
65+
WindowHelper.MoveToVisibleArea(this, pos.X, pos.Y);
66+
else
67+
WindowHelper.MoveToBottomRight(this);
6468
WindowShadow.Apply(this);
6569

6670
// Set window icon (for taskbar / Alt+Tab)
@@ -93,15 +97,18 @@ public MainWindow()
9397
// Hide to tray instead of closing
9498
AppWindow.Closing += (_, args) =>
9599
{
100+
SaveMainWindowPosition();
96101
if (!_isExiting)
97102
{
103+
SaveOpenedSecondaryWindows();
98104
args.Cancel = true;
99105
AppWindow.Hide();
100106
}
101107
};
102108

103109
this.Closed += (_, _) =>
104110
{
111+
SaveMainWindowPosition();
105112
_textExpansion?.Stop();
106113
_reminderService?.Dispose();
107114
ReminderService.Shutdown();
@@ -117,6 +124,8 @@ public MainWindow()
117124
_textExpansion.Start();
118125
ApplyLocalization();
119126
RefreshNotesList();
127+
SetCompactState(AppSettings.LoadMainWindowCompact(), animate: false);
128+
RestorePersistedWindows();
120129

121130
_reminderService = new ReminderService(_notes);
122131
_reminderService.ReminderFired += () => DispatcherQueue.TryEnqueue(() => RefreshCurrentView());
@@ -147,6 +156,8 @@ private async Task InitCloudSync()
147156
private void ExitApplication()
148157
{
149158
_isExiting = true;
159+
SaveMainWindowPosition();
160+
SaveOpenedSecondaryWindows();
150161
foreach (var w in _openNoteWindows.ToList())
151162
{
152163
try { w.Close(); } catch { }
@@ -177,6 +188,61 @@ private void RefreshNotesList(string? search = null)
177188
}
178189
}
179190

191+
private void SaveMainWindowPosition()
192+
{
193+
AppSettings.SaveMainWindowPosition(AppWindow.Position);
194+
AppSettings.SaveMainWindowCompact(_isCompact);
195+
}
196+
197+
private void SaveOpenedSecondaryWindows()
198+
{
199+
var windows = new List<PersistedWindowState>();
200+
foreach (var noteWindow in _openNoteWindows.ToList())
201+
{
202+
var pos = noteWindow.AppWindow.Position;
203+
windows.Add(new PersistedWindowState("note", noteWindow.NoteId, pos.X, pos.Y, noteWindow.IsCompact));
204+
}
205+
206+
if (_notepadWindow != null)
207+
{
208+
var pos = _notepadWindow.AppWindow.Position;
209+
windows.Add(new PersistedWindowState("notepad", "", pos.X, pos.Y));
210+
}
211+
212+
AppSettings.SaveOpenWindows(windows);
213+
}
214+
215+
private void RestorePersistedWindows()
216+
{
217+
var windows = AppSettings.LoadOpenWindows();
218+
if (windows.Count == 0)
219+
return;
220+
221+
var notepadRestored = false;
222+
foreach (var window in windows)
223+
{
224+
if (window.Type == "note" && !string.IsNullOrWhiteSpace(window.NoteId))
225+
{
226+
OpenNote(window.NoteId);
227+
var opened = _openNoteWindows.FirstOrDefault(w => w.NoteId == window.NoteId);
228+
if (opened != null)
229+
{
230+
opened.SetCompactState(window.IsCompact, animate: false);
231+
WindowHelper.MoveToVisibleArea(opened, window.X, window.Y);
232+
}
233+
continue;
234+
}
235+
236+
if (window.Type == "notepad" && !notepadRestored)
237+
{
238+
OpenNotepad();
239+
if (_notepadWindow != null)
240+
WindowHelper.MoveToVisibleArea(_notepadWindow, window.X, window.Y);
241+
notepadRestored = true;
242+
}
243+
}
244+
}
245+
180246
private UIElement CreateNoteCard(NoteEntry note)
181247
{
182248
var color = NoteColors.Get(note.Color);
@@ -978,24 +1044,57 @@ private void Pin_Click(object sender, RoutedEventArgs e)
9781044

9791045
private void Compact_Click(object sender, RoutedEventArgs e)
9801046
{
981-
_isCompact = !_isCompact;
1047+
SetCompactState(!_isCompact);
1048+
}
1049+
1050+
private void SetCompactState(bool compact, bool animate = true)
1051+
{
1052+
if (_isCompact == compact)
1053+
return;
1054+
1055+
_isCompact = compact;
9821056
_targetHeight = _isCompact ? CompactHeight : FullHeight;
9831057
CompactIcon.Glyph = _isCompact ? "\uE70D" : "\uE70E";
9841058

9851059
if (_isCompact)
9861060
{
987-
AnimationHelper.FadeOut(TitleLabel, 100);
988-
AnimationHelper.FadeOut(NotesScroll, 120);
1061+
if (animate)
1062+
{
1063+
AnimationHelper.FadeOut(TitleLabel, 100);
1064+
AnimationHelper.FadeOut(NotesScroll, 120);
1065+
}
1066+
else
1067+
{
1068+
TitleLabel.Opacity = 0;
1069+
NotesScroll.Opacity = 0;
1070+
}
9891071
}
9901072
else
9911073
{
992-
AnimationHelper.FadeIn(TitleLabel, 200, 80);
993-
AnimationHelper.FadeIn(SearchGrid, 200, 120);
994-
AnimationHelper.FadeIn(NotesScroll, 250, 160);
1074+
if (animate)
1075+
{
1076+
AnimationHelper.FadeIn(TitleLabel, 200, 80);
1077+
AnimationHelper.FadeIn(SearchGrid, 200, 120);
1078+
AnimationHelper.FadeIn(NotesScroll, 250, 160);
1079+
}
1080+
else
1081+
{
1082+
TitleLabel.Opacity = 1;
1083+
SearchGrid.Opacity = 1;
1084+
NotesScroll.Opacity = 1;
1085+
}
9951086
}
9961087

997-
_currentAnimHeight = AppWindow.Size.Height;
9981088
_animTimer?.Stop();
1089+
_animTimer = null;
1090+
if (!animate)
1091+
{
1092+
_currentAnimHeight = _targetHeight;
1093+
AppWindow.Resize(new Windows.Graphics.SizeInt32(380, _targetHeight));
1094+
return;
1095+
}
1096+
1097+
_currentAnimHeight = AppWindow.Size.Height;
9991098
_animTimer = new Microsoft.UI.Xaml.DispatcherTimer
10001099
{
10011100
Interval = TimeSpan.FromMilliseconds(8)

0 commit comments

Comments
 (0)