diff --git a/BuildTime.txt b/BuildTime.txt index ff38f66..6c7c2e8 100644 --- a/BuildTime.txt +++ b/BuildTime.txt @@ -1 +1 @@ -2026-04-04 14:22:16 \ No newline at end of file +2026-04-04 15:06:13 \ No newline at end of file diff --git a/GitCommit.txt b/GitCommit.txt index 80cc331..54d666b 100644 --- a/GitCommit.txt +++ b/GitCommit.txt @@ -1 +1 @@ -121d30f \ No newline at end of file +218ec19 \ No newline at end of file diff --git a/Gui/AppManager.cs b/Gui/AppManager.cs index 3484988..5003cba 100644 --- a/Gui/AppManager.cs +++ b/Gui/AppManager.cs @@ -145,6 +145,7 @@ namespace CMLeonOS.Gui RegisterApp(new AppMetadata("Function Grapher", () => { return new FunctionGrapher(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("CodeBlocks", () => { return new CodeBlocks(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("Contacts", () => { return new Contacts(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); + RegisterApp(new AppMetadata("Notes", () => { return new Notes(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("Sheet Editor", () => { return new SheetEditor(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("Tetris", () => { return new Tetris(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); diff --git a/Gui/Apps/Notes.cs b/Gui/Apps/Notes.cs new file mode 100644 index 0000000..faeabe2 --- /dev/null +++ b/Gui/Apps/Notes.cs @@ -0,0 +1,465 @@ +using CMLeonOS; +using CMLeonOS.Gui.UILib; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Text; + +namespace CMLeonOS.Gui.Apps +{ + internal class Notes : Process + { + internal Notes() : base("Notes", ProcessType.Application) { } + + private sealed class NoteItem + { + internal string Title = "Untitled"; + internal string Content = string.Empty; + internal DateTime UpdatedAt = DateTime.Now; + } + + private const string DataPath = @"0:\notes.dat"; + private const int ToolbarHeight = 34; + private const int HeaderHeight = 40; + private const int Padding = 8; + private const int ButtonWidth = 84; + private const int ButtonHeight = 24; + private const int TitleRowHeight = 24; + + private readonly WindowManager wm = ProcessManager.GetProcess(); + private readonly List notes = new List(); + + private AppWindow window; + private Window header; + + private Button newButton; + private Button saveButton; + private Button deleteButton; + private Button reloadButton; + + private Table noteTable; + private TextBox titleBox; + private TextBox contentBox; + + private TextBlock titleLabel; + private TextBlock contentLabel; + + private string status = @"Store: 0:\notes.dat"; + + private static string Escape(string value) + { + string v = value ?? string.Empty; + v = v.Replace("\\", "\\\\"); + v = v.Replace("\t", "\\t"); + v = v.Replace("\n", "\\n"); + v = v.Replace("\r", string.Empty); + return v; + } + + private static string Unescape(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + StringBuilder sb = new StringBuilder(value.Length); + bool escaped = false; + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + if (!escaped) + { + if (c == '\\') + { + escaped = true; + } + else + { + sb.Append(c); + } + continue; + } + + switch (c) + { + case 't': + sb.Append('\t'); + break; + case 'n': + sb.Append('\n'); + break; + case '\\': + sb.Append('\\'); + break; + default: + sb.Append(c); + break; + } + escaped = false; + } + + if (escaped) + { + sb.Append('\\'); + } + + return sb.ToString(); + } + + private static string[] SplitEscapedTabs(string line) + { + List parts = new List(); + StringBuilder current = new StringBuilder(); + bool escaped = false; + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + if (escaped) + { + current.Append('\\'); + current.Append(c); + escaped = false; + continue; + } + + if (c == '\\') + { + escaped = true; + continue; + } + + if (c == '\t') + { + parts.Add(current.ToString()); + current.Clear(); + continue; + } + + current.Append(c); + } + + if (escaped) + { + current.Append('\\'); + } + + parts.Add(current.ToString()); + return parts.ToArray(); + } + + private void SetStatus(string text) + { + status = text ?? string.Empty; + RenderHeader(); + } + + private void RenderHeader() + { + header.Clear(Color.FromArgb(244, 246, 250)); + header.DrawRectangle(0, 0, header.Width, header.Height, Color.FromArgb(186, 194, 206)); + header.DrawString("Notes", Color.FromArgb(40, 49, 64), 12, 6); + header.DrawString(status, Color.FromArgb(108, 117, 131), 12, 22); + wm.Update(header); + } + + private void LoadNotes() + { + notes.Clear(); + + if (!File.Exists(DataPath)) + { + SetStatus(@"No notes file yet. Save to create 0:\notes.dat"); + return; + } + + string[] lines = File.ReadAllLines(DataPath); + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i] ?? string.Empty; + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + string[] parts = SplitEscapedTabs(line); + NoteItem item = new NoteItem(); + item.Title = parts.Length > 0 ? Unescape(parts[0]) : "Untitled"; + item.Content = parts.Length > 1 ? Unescape(parts[1]) : string.Empty; + + long ticks = 0; + if (parts.Length > 2 && long.TryParse(parts[2], out ticks)) + { + item.UpdatedAt = new DateTime(ticks); + } + else + { + item.UpdatedAt = DateTime.Now; + } + + if (string.IsNullOrWhiteSpace(item.Title)) + { + item.Title = "Untitled"; + } + + notes.Add(item); + } + + SetStatus($"Loaded {notes.Count} note(s)"); + } + + private void SaveNotes() + { + string[] lines = new string[notes.Count]; + for (int i = 0; i < notes.Count; i++) + { + NoteItem note = notes[i]; + lines[i] = string.Join("\t", new string[] + { + Escape(note.Title), + Escape(note.Content), + note.UpdatedAt.Ticks.ToString() + }); + } + + File.WriteAllLines(DataPath, lines); + } + + private string BuildListLabel(NoteItem note) + { + string title = string.IsNullOrWhiteSpace(note.Title) ? "Untitled" : note.Title.Trim(); + if (title.Length > 22) + { + title = title.Substring(0, 22) + "..."; + } + + string date = note.UpdatedAt.ToString("MM-dd HH:mm"); + return $"{title} {date}"; + } + + private void PopulateList(int preferredSelection) + { + noteTable.Cells.Clear(); + noteTable.SelectedCellIndex = -1; + + for (int i = 0; i < notes.Count; i++) + { + noteTable.Cells.Add(new TableCell(BuildListLabel(notes[i]), i)); + } + + if (preferredSelection >= 0 && preferredSelection < notes.Count) + { + noteTable.SelectedCellIndex = preferredSelection; + } + else if (notes.Count > 0) + { + noteTable.SelectedCellIndex = 0; + } + + noteTable.Render(); + LoadCurrentSelection(); + } + + private void LoadCurrentSelection() + { + int idx = noteTable.SelectedCellIndex; + if (idx < 0 || idx >= notes.Count) + { + titleBox.Text = string.Empty; + contentBox.Text = string.Empty; + SetStatus($"Loaded {notes.Count} note(s)"); + return; + } + + NoteItem selected = notes[idx]; + titleBox.Text = selected.Title; + contentBox.Text = selected.Content; + SetStatus($"Selected: {selected.Title}"); + } + + private void NewClicked(int x, int y) + { + noteTable.SelectedCellIndex = -1; + titleBox.Text = "Untitled"; + contentBox.Text = string.Empty; + SetStatus("Create mode: edit and click Save"); + titleBox.Render(); + contentBox.Render(); + } + + private void SaveClicked(int x, int y) + { + string title = (titleBox.Text ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(title)) + { + title = "Untitled"; + } + + string content = contentBox.Text ?? string.Empty; + int idx = noteTable.SelectedCellIndex; + if (idx >= 0 && idx < notes.Count) + { + notes[idx].Title = title; + notes[idx].Content = content; + notes[idx].UpdatedAt = DateTime.Now; + SaveNotes(); + PopulateList(idx); + SetStatus($"Updated: {title}"); + return; + } + + NoteItem item = new NoteItem + { + Title = title, + Content = content, + UpdatedAt = DateTime.Now + }; + notes.Insert(0, item); + SaveNotes(); + PopulateList(0); + SetStatus($"Created: {title}"); + } + + private void DeleteClicked(int x, int y) + { + int idx = noteTable.SelectedCellIndex; + if (idx < 0 || idx >= notes.Count) + { + SetStatus("Select a note first"); + return; + } + + string title = notes[idx].Title; + notes.RemoveAt(idx); + SaveNotes(); + + int next = idx; + if (next >= notes.Count) + { + next = notes.Count - 1; + } + PopulateList(next); + SetStatus($"Deleted: {title}"); + } + + private void ReloadClicked(int x, int y) + { + LoadNotes(); + PopulateList(-1); + } + + private void Relayout() + { + int right = window.Width - Padding; + int topY = Padding; + + newButton.MoveAndResize(right - ((ButtonWidth * 4) + (Padding * 3)), topY, ButtonWidth, ButtonHeight); + saveButton.MoveAndResize(right - ((ButtonWidth * 3) + (Padding * 2)), topY, ButtonWidth, ButtonHeight); + deleteButton.MoveAndResize(right - ((ButtonWidth * 2) + Padding), topY, ButtonWidth, ButtonHeight); + reloadButton.MoveAndResize(right - ButtonWidth, topY, ButtonWidth, ButtonHeight); + + header.MoveAndResize(0, ToolbarHeight, window.Width, HeaderHeight); + + int contentY = ToolbarHeight + HeaderHeight + Padding; + int contentHeight = window.Height - contentY - Padding; + int listWidth = Math.Max(220, (window.Width * 38) / 100); + int editorX = listWidth + (Padding * 2); + int editorWidth = window.Width - editorX - Padding; + + noteTable.MoveAndResize(Padding, contentY, listWidth - Padding, contentHeight); + + titleLabel.MoveAndResize(editorX, contentY + 2, 48, 20); + titleBox.MoveAndResize(editorX + 48, contentY, editorWidth - 48, TitleRowHeight); + + int bodyY = contentY + TitleRowHeight + Padding; + contentLabel.MoveAndResize(editorX, bodyY + 2, 60, 20); + contentBox.MoveAndResize(editorX, bodyY + 22, editorWidth, Math.Max(80, contentY + contentHeight - (bodyY + 22))); + + newButton.Render(); + saveButton.Render(); + deleteButton.Render(); + reloadButton.Render(); + RenderHeader(); + noteTable.Render(); + titleLabel.Render(); + contentLabel.Render(); + titleBox.Render(); + contentBox.Render(); + } + + public override void Start() + { + base.Start(); + + window = new AppWindow(this, 180, 96, 930, 540); + window.Title = "Notes"; + window.Icon = AppManager.DefaultAppIcon; + window.CanResize = true; + window.UserResized = Relayout; + window.Closing = TryStop; + wm.AddWindow(window); + + newButton = new Button(window, 0, 0, 1, 1); + newButton.Text = "New"; + newButton.OnClick = NewClicked; + wm.AddWindow(newButton); + + saveButton = new Button(window, 0, 0, 1, 1); + saveButton.Text = "Save"; + saveButton.OnClick = SaveClicked; + wm.AddWindow(saveButton); + + deleteButton = new Button(window, 0, 0, 1, 1); + deleteButton.Text = "Delete"; + deleteButton.OnClick = DeleteClicked; + wm.AddWindow(deleteButton); + + reloadButton = new Button(window, 0, 0, 1, 1); + reloadButton.Text = "Reload"; + reloadButton.OnClick = ReloadClicked; + wm.AddWindow(reloadButton); + + header = new Window(this, window, 0, ToolbarHeight, window.Width, HeaderHeight); + wm.AddWindow(header); + + noteTable = new Table(window, 0, 0, 1, 1); + noteTable.CellHeight = 24; + noteTable.Background = Color.White; + noteTable.Foreground = Color.Black; + noteTable.Border = Color.FromArgb(185, 194, 207); + noteTable.SelectedBackground = Color.FromArgb(255, 240, 210); + noteTable.SelectedBorder = Color.FromArgb(223, 164, 70); + noteTable.SelectedForeground = Color.Black; + noteTable.TableCellSelected = _ => LoadCurrentSelection(); + wm.AddWindow(noteTable); + + titleLabel = new TextBlock(window, 0, 0, 1, 1); + titleLabel.Text = "Title"; + titleLabel.Foreground = UITheme.TextSecondary; + wm.AddWindow(titleLabel); + + contentLabel = new TextBlock(window, 0, 0, 1, 1); + contentLabel.Text = "Content"; + contentLabel.Foreground = UITheme.TextSecondary; + wm.AddWindow(contentLabel); + + titleBox = new TextBox(window, 0, 0, 1, 1); + wm.AddWindow(titleBox); + + contentBox = new TextBox(window, 0, 0, 1, 1); + contentBox.MultiLine = true; + wm.AddWindow(contentBox); + + LoadNotes(); + PopulateList(-1); + Relayout(); + wm.Update(window); + } + + public override void Run() + { + } + } +}