From 1e59eff7245800a992a8806b26fbff6c674ed277 Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Fri, 3 Apr 2026 21:55:43 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E8=AE=AF=E5=BD=95&=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E8=BD=AF=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BuildTime.txt | 2 +- GitCommit.txt | 2 +- Gui/AppManager.cs | 2 + Gui/Apps/Contacts.cs | 515 +++++++++++++++++++++++++++++++++++ Gui/Apps/SheetEditor.cs | 579 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1098 insertions(+), 2 deletions(-) create mode 100644 Gui/Apps/Contacts.cs create mode 100644 Gui/Apps/SheetEditor.cs diff --git a/BuildTime.txt b/BuildTime.txt index 713a8d4..b9b1431 100644 --- a/BuildTime.txt +++ b/BuildTime.txt @@ -1 +1 @@ -2026-03-31 22:16:42 \ No newline at end of file +2026-04-03 21:32:07 \ No newline at end of file diff --git a/GitCommit.txt b/GitCommit.txt index d2000ac..ac1ebf7 100644 --- a/GitCommit.txt +++ b/GitCommit.txt @@ -1 +1 @@ -cd2e1e5 \ No newline at end of file +1d41885 \ No newline at end of file diff --git a/Gui/AppManager.cs b/Gui/AppManager.cs index c103d3c..1046529 100644 --- a/Gui/AppManager.cs +++ b/Gui/AppManager.cs @@ -144,6 +144,8 @@ namespace CMLeonOS.Gui RegisterApp(new AppMetadata("Music Editor", () => { return new MusicEditor(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); 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("Sheet Editor", () => { return new SheetEditor(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); Logger.Logger.Instance.Info("AppManager", $"{AppMetadatas.Count} apps were registered."); diff --git a/Gui/Apps/Contacts.cs b/Gui/Apps/Contacts.cs new file mode 100644 index 0000000..afba63e --- /dev/null +++ b/Gui/Apps/Contacts.cs @@ -0,0 +1,515 @@ +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 Contacts : Process + { + internal Contacts() : base("Contacts", ProcessType.Application) { } + + private sealed class ContactEntry + { + internal string Name = string.Empty; + internal string Phone = string.Empty; + internal string Email = string.Empty; + internal string Address = string.Empty; + internal string Notes = string.Empty; + } + + private const string DataPath = @"0:\contacts.dat"; + private const int ToolbarHeight = 34; + private const int HeaderHeight = 52; + private const int Padding = 8; + private const int ButtonWidth = 82; + private const int ButtonHeight = 24; + private const int LabelWidth = 64; + private const int RowHeight = 24; + + private readonly WindowManager wm = ProcessManager.GetProcess(); + private readonly List contacts = new List(); + + private AppWindow window; + private Window header; + private Table contactTable; + + private Button newButton; + private Button saveButton; + private Button deleteButton; + private Button refreshButton; + + private TextBlock nameLabel; + private TextBlock phoneLabel; + private TextBlock emailLabel; + private TextBlock addressLabel; + private TextBlock notesLabel; + + private TextBox nameBox; + private TextBox phoneBox; + private TextBox emailBox; + private TextBox addressBox; + private TextBox notesBox; + + private string statusText = @"Store file: 0:\contacts.dat"; + + private void SetStatus(string text) + { + statusText = text ?? string.Empty; + RenderHeader(); + } + + private void RenderHeader() + { + header.Clear(Color.FromArgb(235, 241, 248)); + header.DrawRectangle(0, 0, header.Width, header.Height, Color.FromArgb(180, 192, 208)); + header.DrawString("Contacts", Color.FromArgb(28, 38, 52), 12, 10); + header.DrawString(statusText, Color.FromArgb(97, 110, 126), 12, 30); + wm.Update(header); + } + + 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 LoadContacts() + { + contacts.Clear(); + + if (!File.Exists(DataPath)) + { + SetStatus(@"No contact file yet. Click Save to create 0:\contacts.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[] fields = SplitEscapedTabs(line); + ContactEntry entry = new ContactEntry(); + entry.Name = fields.Length > 0 ? Unescape(fields[0]) : string.Empty; + entry.Phone = fields.Length > 1 ? Unescape(fields[1]) : string.Empty; + entry.Email = fields.Length > 2 ? Unescape(fields[2]) : string.Empty; + entry.Address = fields.Length > 3 ? Unescape(fields[3]) : string.Empty; + entry.Notes = fields.Length > 4 ? Unescape(fields[4]) : string.Empty; + + if (!string.IsNullOrWhiteSpace(entry.Name)) + { + contacts.Add(entry); + } + } + + SetStatus($"Loaded {contacts.Count} contact(s)."); + } + + private void SaveContacts() + { + string[] lines = new string[contacts.Count]; + for (int i = 0; i < contacts.Count; i++) + { + ContactEntry c = contacts[i]; + lines[i] = string.Join("\t", new string[] + { + Escape(c.Name), + Escape(c.Phone), + Escape(c.Email), + Escape(c.Address), + Escape(c.Notes) + }); + } + + File.WriteAllLines(DataPath, lines); + } + + private void PopulateTable(int preferredSelection = -1) + { + contactTable.Cells.Clear(); + contactTable.SelectedCellIndex = -1; + + for (int i = 0; i < contacts.Count; i++) + { + ContactEntry c = contacts[i]; + string display = string.IsNullOrWhiteSpace(c.Phone) ? c.Name : $"{c.Name} ({c.Phone})"; + contactTable.Cells.Add(new TableCell(display, c.Name)); + } + + if (preferredSelection >= 0 && preferredSelection < contacts.Count) + { + contactTable.SelectedCellIndex = preferredSelection; + } + + contactTable.Render(); + ContactSelected(contactTable.SelectedCellIndex); + } + + private void ClearEditor() + { + nameBox.Text = string.Empty; + phoneBox.Text = string.Empty; + emailBox.Text = string.Empty; + addressBox.Text = string.Empty; + notesBox.Text = string.Empty; + } + + private ContactEntry ReadEditor() + { + ContactEntry entry = new ContactEntry(); + entry.Name = (nameBox.Text ?? string.Empty).Trim(); + entry.Phone = (phoneBox.Text ?? string.Empty).Trim(); + entry.Email = (emailBox.Text ?? string.Empty).Trim(); + entry.Address = (addressBox.Text ?? string.Empty).Trim(); + entry.Notes = notesBox.Text ?? string.Empty; + return entry; + } + + private void LoadEditor(ContactEntry entry) + { + if (entry == null) + { + ClearEditor(); + return; + } + + nameBox.Text = entry.Name; + phoneBox.Text = entry.Phone; + emailBox.Text = entry.Email; + addressBox.Text = entry.Address; + notesBox.Text = entry.Notes; + } + + private void ContactSelected(int index) + { + if (index < 0 || index >= contacts.Count) + { + SetStatus($"Loaded {contacts.Count} contact(s)."); + ClearEditor(); + return; + } + + LoadEditor(contacts[index]); + SetStatus($"Selected: {contacts[index].Name}"); + } + + private void NewClicked(int x, int y) + { + contactTable.SelectedCellIndex = -1; + ClearEditor(); + SetStatus("Create mode: fill fields and click Save."); + } + + private void SaveClicked(int x, int y) + { + ContactEntry updated = ReadEditor(); + if (string.IsNullOrWhiteSpace(updated.Name)) + { + new MessageBox(this, "Contacts", "Name cannot be empty.").Show(); + return; + } + + int selected = contactTable.SelectedCellIndex; + int targetIndex; + if (selected >= 0 && selected < contacts.Count) + { + contacts[selected] = updated; + targetIndex = selected; + SetStatus($"Updated: {updated.Name}"); + } + else + { + contacts.Add(updated); + targetIndex = contacts.Count - 1; + SetStatus($"Added: {updated.Name}"); + } + + SaveContacts(); + PopulateTable(targetIndex); + } + + private void DeleteClicked(int x, int y) + { + int selected = contactTable.SelectedCellIndex; + if (selected < 0 || selected >= contacts.Count) + { + SetStatus("Select a contact first."); + return; + } + + string name = contacts[selected].Name; + contacts.RemoveAt(selected); + SaveContacts(); + + int next = selected; + if (next >= contacts.Count) next = contacts.Count - 1; + PopulateTable(next); + SetStatus($"Deleted: {name}"); + } + + private void RefreshClicked(int x, int y) + { + LoadContacts(); + PopulateTable(-1); + } + + private void Relayout() + { + int topY = Padding; + int right = window.Width - 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); + refreshButton.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 * 2) / 5); + int editorX = listWidth + (Padding * 2); + int editorWidth = window.Width - editorX - Padding; + + contactTable.MoveAndResize(Padding, contentY, listWidth - Padding, contentHeight); + + int y = contentY; + nameLabel.MoveAndResize(editorX, y + 2, LabelWidth, 20); + nameBox.MoveAndResize(editorX + LabelWidth, y, editorWidth - LabelWidth, RowHeight); + + y += RowHeight + Padding; + phoneLabel.MoveAndResize(editorX, y + 2, LabelWidth, 20); + phoneBox.MoveAndResize(editorX + LabelWidth, y, editorWidth - LabelWidth, RowHeight); + + y += RowHeight + Padding; + emailLabel.MoveAndResize(editorX, y + 2, LabelWidth, 20); + emailBox.MoveAndResize(editorX + LabelWidth, y, editorWidth - LabelWidth, RowHeight); + + y += RowHeight + Padding; + addressLabel.MoveAndResize(editorX, y + 2, LabelWidth, 20); + addressBox.MoveAndResize(editorX + LabelWidth, y, editorWidth - LabelWidth, RowHeight); + + y += RowHeight + Padding; + notesLabel.MoveAndResize(editorX, y + 2, LabelWidth, 20); + notesBox.MoveAndResize(editorX + LabelWidth, y, editorWidth - LabelWidth, Math.Max(72, contentY + contentHeight - y)); + + newButton.Render(); + saveButton.Render(); + deleteButton.Render(); + refreshButton.Render(); + RenderHeader(); + contactTable.Render(); + nameLabel.Render(); + phoneLabel.Render(); + emailLabel.Render(); + addressLabel.Render(); + notesLabel.Render(); + nameBox.Render(); + phoneBox.Render(); + emailBox.Render(); + addressBox.Render(); + notesBox.Render(); + } + + public override void Start() + { + base.Start(); + + window = new AppWindow(this, 190, 100, 900, 520); + window.Title = "Contacts"; + 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); + + refreshButton = new Button(window, 0, 0, 1, 1); + refreshButton.Text = "Refresh"; + refreshButton.OnClick = RefreshClicked; + wm.AddWindow(refreshButton); + + header = new Window(this, window, 0, ToolbarHeight, window.Width, HeaderHeight); + wm.AddWindow(header); + + contactTable = new Table(window, 0, 0, 1, 1); + contactTable.CellHeight = 24; + contactTable.Background = Color.White; + contactTable.Foreground = Color.Black; + contactTable.Border = Color.FromArgb(185, 194, 207); + contactTable.SelectedBackground = Color.FromArgb(216, 231, 255); + contactTable.SelectedBorder = Color.FromArgb(94, 138, 216); + contactTable.SelectedForeground = Color.Black; + contactTable.TableCellSelected = ContactSelected; + wm.AddWindow(contactTable); + + nameLabel = new TextBlock(window, 0, 0, LabelWidth, 20); + nameLabel.Text = "Name"; + nameLabel.Foreground = UITheme.TextSecondary; + wm.AddWindow(nameLabel); + + phoneLabel = new TextBlock(window, 0, 0, LabelWidth, 20); + phoneLabel.Text = "Phone"; + phoneLabel.Foreground = UITheme.TextSecondary; + wm.AddWindow(phoneLabel); + + emailLabel = new TextBlock(window, 0, 0, LabelWidth, 20); + emailLabel.Text = "Email"; + emailLabel.Foreground = UITheme.TextSecondary; + wm.AddWindow(emailLabel); + + addressLabel = new TextBlock(window, 0, 0, LabelWidth, 20); + addressLabel.Text = "Address"; + addressLabel.Foreground = UITheme.TextSecondary; + wm.AddWindow(addressLabel); + + notesLabel = new TextBlock(window, 0, 0, LabelWidth, 20); + notesLabel.Text = "Notes"; + notesLabel.Foreground = UITheme.TextSecondary; + wm.AddWindow(notesLabel); + + nameBox = new TextBox(window, 0, 0, 1, 1); + wm.AddWindow(nameBox); + + phoneBox = new TextBox(window, 0, 0, 1, 1); + wm.AddWindow(phoneBox); + + emailBox = new TextBox(window, 0, 0, 1, 1); + wm.AddWindow(emailBox); + + addressBox = new TextBox(window, 0, 0, 1, 1); + wm.AddWindow(addressBox); + + notesBox = new TextBox(window, 0, 0, 1, 1); + notesBox.MultiLine = true; + wm.AddWindow(notesBox); + + LoadContacts(); + PopulateTable(-1); + Relayout(); + wm.Update(window); + } + + public override void Run() + { + } + } +} diff --git a/Gui/Apps/SheetEditor.cs b/Gui/Apps/SheetEditor.cs new file mode 100644 index 0000000..7ba4ebd --- /dev/null +++ b/Gui/Apps/SheetEditor.cs @@ -0,0 +1,579 @@ +using CMLeonOS; +using CMLeonOS.Gui.UILib; +using System; +using System.Drawing; +using System.IO; +using System.Text; + +namespace CMLeonOS.Gui.Apps +{ + internal class SheetEditor : Process + { + internal SheetEditor() : base("Sheet Editor", ProcessType.Application) { } + + internal SheetEditor(string path) : base("Sheet Editor", ProcessType.Application) + { + startupPath = path; + } + + private const int MaxRows = 200; + private const int MaxCols = 52; + private const int ToolbarHeight = 30; + private const int StatusHeight = 24; + private const int Padding = 6; + private const int ButtonWidth = 82; + private const int RowHeaderWidth = 44; + private const int ColHeaderHeight = 22; + private const int CellWidth = 92; + private const int CellHeight = 22; + private const string FileMagic = "CMSHEET1"; + + private readonly WindowManager wm = ProcessManager.GetProcess(); + + private AppWindow window; + private Window grid; + private TextBox editorBox; + private TextBlock cellLabel; + private StatusBar statusBar; + + private Button newButton; + private Button openButton; + private Button saveButton; + private Button saveAsButton; + private Button applyButton; + private Button leftButton; + private Button rightButton; + private Button upButton; + private Button downButton; + + private FileBrowser fileBrowser; + + private readonly string[] cells = new string[MaxRows * MaxCols]; + private int selectedRow = 0; + private int selectedCol = 0; + private int startRow = 0; + private int startCol = 0; + private string currentPath = null; + private bool modified = false; + private readonly string startupPath = null; + + private static int Index(int row, int col) + { + return (row * MaxCols) + col; + } + + 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 esc = false; + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + if (!esc) + { + if (c == '\\') + { + esc = true; + } + else + { + sb.Append(c); + } + } + else + { + switch (c) + { + case 't': sb.Append('\t'); break; + case 'n': sb.Append('\n'); break; + case '\\': sb.Append('\\'); break; + default: sb.Append(c); break; + } + esc = false; + } + } + if (esc) sb.Append('\\'); + return sb.ToString(); + } + + private static string[] SplitTabs(string line) + { + string[] parts = new string[3]; + int part = 0; + StringBuilder sb = new StringBuilder(); + bool esc = false; + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + if (!esc) + { + if (c == '\\') + { + esc = true; + continue; + } + if (c == '\t' && part < 2) + { + parts[part++] = sb.ToString(); + sb.Clear(); + continue; + } + sb.Append(c); + } + else + { + sb.Append('\\'); + sb.Append(c); + esc = false; + } + } + parts[part] = sb.ToString(); + if (parts[0] == null) parts[0] = string.Empty; + if (parts[1] == null) parts[1] = string.Empty; + if (parts[2] == null) parts[2] = string.Empty; + return parts; + } + + private void SetStatus(string text) + { + statusBar.Text = text; + statusBar.DetailText = currentPath ?? "Unsaved (.cws)"; + statusBar.Render(); + } + + private void UpdateTitle() + { + string baseName = currentPath == null ? "Untitled.cws" : Path.GetFileName(currentPath); + window.Title = modified ? (baseName + "* - Sheet Editor") : (baseName + " - Sheet Editor"); + cellLabel.Text = CellName(selectedRow, selectedCol); + cellLabel.Render(); + } + + private static string ColumnName(int col) + { + col = Math.Max(0, Math.Min(MaxCols - 1, col)); + if (col < 26) + { + return ((char)('A' + col)).ToString(); + } + return ((char)('A' + ((col / 26) - 1))).ToString() + ((char)('A' + (col % 26))).ToString(); + } + + private static string CellName(int row, int col) + { + return ColumnName(col) + (row + 1).ToString(); + } + + private static string ClipText(string text, int maxChars) + { + if (string.IsNullOrEmpty(text)) return string.Empty; + string singleLine = text.Replace('\n', ' '); + if (singleLine.Length <= maxChars) return singleLine; + if (maxChars <= 1) return "."; + return singleLine.Substring(0, maxChars - 1) + "."; + } + + private int VisibleCols() + { + int v = (grid.Width - RowHeaderWidth) / CellWidth; + return Math.Max(1, v); + } + + private int VisibleRows() + { + int v = (grid.Height - ColHeaderHeight) / CellHeight; + return Math.Max(1, v); + } + + private void EnsureSelectionVisible() + { + int visCols = VisibleCols(); + int visRows = VisibleRows(); + if (selectedCol < startCol) startCol = selectedCol; + if (selectedCol >= startCol + visCols) startCol = selectedCol - visCols + 1; + if (selectedRow < startRow) startRow = selectedRow; + if (selectedRow >= startRow + visRows) startRow = selectedRow - visRows + 1; + if (startCol < 0) startCol = 0; + if (startRow < 0) startRow = 0; + if (startCol > MaxCols - 1) startCol = MaxCols - 1; + if (startRow > MaxRows - 1) startRow = MaxRows - 1; + } + + private void RenderGrid() + { + grid.Clear(Color.White); + + int visCols = VisibleCols(); + int visRows = VisibleRows(); + int maxCol = Math.Min(MaxCols, startCol + visCols); + int maxRow = Math.Min(MaxRows, startRow + visRows); + + grid.DrawFilledRectangle(0, 0, grid.Width, ColHeaderHeight, Color.FromArgb(240, 245, 252)); + grid.DrawFilledRectangle(0, 0, RowHeaderWidth, grid.Height, Color.FromArgb(240, 245, 252)); + grid.DrawRectangle(0, 0, grid.Width, grid.Height, Color.FromArgb(182, 194, 210)); + + for (int c = startCol; c < maxCol; c++) + { + int x = RowHeaderWidth + ((c - startCol) * CellWidth); + string label = ColumnName(c); + grid.DrawRectangle(x, 0, CellWidth, ColHeaderHeight, Color.FromArgb(198, 208, 223)); + grid.DrawString(label, Color.FromArgb(44, 60, 82), x + (CellWidth / 2) - (label.Length * 4), 3); + } + + for (int r = startRow; r < maxRow; r++) + { + int y = ColHeaderHeight + ((r - startRow) * CellHeight); + string label = (r + 1).ToString(); + grid.DrawRectangle(0, y, RowHeaderWidth, CellHeight, Color.FromArgb(198, 208, 223)); + grid.DrawString(label, Color.FromArgb(44, 60, 82), RowHeaderWidth - 6 - (label.Length * 8), y + 3); + } + + for (int r = startRow; r < maxRow; r++) + { + int y = ColHeaderHeight + ((r - startRow) * CellHeight); + for (int c = startCol; c < maxCol; c++) + { + int x = RowHeaderWidth + ((c - startCol) * CellWidth); + bool selected = (r == selectedRow && c == selectedCol); + Color border = selected ? Color.FromArgb(94, 138, 216) : Color.FromArgb(204, 214, 226); + Color back = selected ? Color.FromArgb(227, 238, 255) : Color.White; + + grid.DrawFilledRectangle(x, y, CellWidth, CellHeight, border); + grid.DrawFilledRectangle(x + 1, y + 1, CellWidth - 2, CellHeight - 2, back); + + string cellText = cells[Index(r, c)] ?? string.Empty; + if (!string.IsNullOrWhiteSpace(cellText)) + { + grid.DrawString(ClipText(cellText, 10), Color.FromArgb(28, 38, 52), x + 4, y + 3); + } + } + } + + wm.Update(grid); + } + + private void SelectCell(int row, int col) + { + selectedRow = Math.Max(0, Math.Min(MaxRows - 1, row)); + selectedCol = Math.Max(0, Math.Min(MaxCols - 1, col)); + EnsureSelectionVisible(); + editorBox.Text = cells[Index(selectedRow, selectedCol)] ?? string.Empty; + UpdateTitle(); + RenderGrid(); + } + + private void GridDown(int x, int y) + { + if (x < RowHeaderWidth || y < ColHeaderHeight) + { + return; + } + + int col = startCol + ((x - RowHeaderWidth) / CellWidth); + int row = startRow + ((y - ColHeaderHeight) / CellHeight); + if (row < 0 || row >= MaxRows || col < 0 || col >= MaxCols) + { + return; + } + SelectCell(row, col); + } + + private void ApplyEdit() + { + int idx = Index(selectedRow, selectedCol); + string before = cells[idx] ?? string.Empty; + string after = editorBox.Text ?? string.Empty; + if (before == after) return; + cells[idx] = after; + modified = true; + UpdateTitle(); + RenderGrid(); + SetStatus("Cell updated: " + CellName(selectedRow, selectedCol)); + } + + private void ClearSheet() + { + for (int i = 0; i < cells.Length; i++) + { + cells[i] = string.Empty; + } + selectedRow = 0; + selectedCol = 0; + startRow = 0; + startCol = 0; + editorBox.Text = string.Empty; + modified = false; + currentPath = null; + UpdateTitle(); + RenderGrid(); + SetStatus("New sheet."); + } + + private void SaveToPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + StringBuilder sb = new StringBuilder(); + sb.AppendLine(FileMagic); + sb.AppendLine(MaxRows.ToString() + "\t" + MaxCols.ToString()); + + for (int r = 0; r < MaxRows; r++) + { + for (int c = 0; c < MaxCols; c++) + { + string val = cells[Index(r, c)] ?? string.Empty; + if (val.Length == 0) continue; + sb.Append(r.ToString()); + sb.Append('\t'); + sb.Append(c.ToString()); + sb.Append('\t'); + sb.AppendLine(Escape(val)); + } + } + + File.WriteAllText(path, sb.ToString()); + currentPath = path; + modified = false; + UpdateTitle(); + SetStatus("Saved."); + } + + private void Save() + { + if (currentPath == null) + { + SaveAs(); + return; + } + SaveToPath(currentPath); + } + + private void SaveAs() + { + fileBrowser = new FileBrowser(this, wm, (selectedPath) => + { + if (string.IsNullOrWhiteSpace(selectedPath)) return; + SaveToPath(selectedPath); + }, selectDirectoryOnly: true); + fileBrowser.Show(); + } + + private void OpenFromPath(string path) + { + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + { + new MessageBox(this, "Sheet Editor", "File does not exist.").Show(); + return; + } + + string[] lines = File.ReadAllLines(path); + if (lines.Length < 2 || lines[0] != FileMagic) + { + new MessageBox(this, "Sheet Editor", "Unsupported file format.").Show(); + return; + } + + for (int i = 0; i < cells.Length; i++) + { + cells[i] = string.Empty; + } + + for (int i = 2; i < lines.Length; i++) + { + string line = lines[i] ?? string.Empty; + if (string.IsNullOrWhiteSpace(line)) continue; + string[] parts = SplitTabs(line); + + if (!int.TryParse(parts[0], out int r)) continue; + if (!int.TryParse(parts[1], out int c)) continue; + if (r < 0 || r >= MaxRows || c < 0 || c >= MaxCols) continue; + + cells[Index(r, c)] = Unescape(parts[2]); + } + + currentPath = path; + modified = false; + selectedRow = 0; + selectedCol = 0; + startRow = 0; + startCol = 0; + editorBox.Text = string.Empty; + UpdateTitle(); + RenderGrid(); + SetStatus("Opened."); + } + + private void OpenPrompt() + { + fileBrowser = new FileBrowser(this, wm, (selectedPath) => + { + if (string.IsNullOrWhiteSpace(selectedPath)) return; + OpenFromPath(selectedPath); + }); + fileBrowser.Show(); + } + + private void Layout() + { + int topY = Padding; + int x = Padding; + newButton.MoveAndResize(x, topY, ButtonWidth, 22); x += ButtonWidth + 4; + openButton.MoveAndResize(x, topY, ButtonWidth, 22); x += ButtonWidth + 4; + saveButton.MoveAndResize(x, topY, ButtonWidth, 22); x += ButtonWidth + 4; + saveAsButton.MoveAndResize(x, topY, ButtonWidth, 22); x += ButtonWidth + 6; + + cellLabel.MoveAndResize(x, topY + 3, 52, 20); x += 54; + editorBox.MoveAndResize(x, topY, Math.Max(180, window.Width - x - 210), 22); + applyButton.MoveAndResize(window.Width - 198, topY, 70, 22); + + leftButton.MoveAndResize(window.Width - 122, topY, 26, 22); + rightButton.MoveAndResize(window.Width - 92, topY, 26, 22); + upButton.MoveAndResize(window.Width - 62, topY, 26, 22); + downButton.MoveAndResize(window.Width - 32, topY, 26, 22); + + int gridY = ToolbarHeight; + grid.MoveAndResize(0, gridY, window.Width, window.Height - gridY - StatusHeight); + statusBar.MoveAndResize(0, window.Height - StatusHeight, window.Width, StatusHeight); + + EnsureSelectionVisible(); + RenderGrid(); + statusBar.Render(); + cellLabel.Render(); + editorBox.Render(); + newButton.Render(); + openButton.Render(); + saveButton.Render(); + saveAsButton.Render(); + applyButton.Render(); + leftButton.Render(); + rightButton.Render(); + upButton.Render(); + downButton.Render(); + } + + public override void Start() + { + base.Start(); + + window = new AppWindow(this, 150, 80, 980, 560); + window.Title = "Sheet Editor - Untitled.cws"; + window.Icon = AppManager.DefaultAppIcon; + window.CanResize = true; + window.UserResized = Layout; + window.Closing = TryStop; + wm.AddWindow(window); + + newButton = new Button(window, 0, 0, 1, 1); + newButton.Text = "New"; + newButton.OnClick = (_, _) => ClearSheet(); + wm.AddWindow(newButton); + + openButton = new Button(window, 0, 0, 1, 1); + openButton.Text = "Open"; + openButton.OnClick = (_, _) => OpenPrompt(); + wm.AddWindow(openButton); + + saveButton = new Button(window, 0, 0, 1, 1); + saveButton.Text = "Save"; + saveButton.OnClick = (_, _) => Save(); + wm.AddWindow(saveButton); + + saveAsButton = new Button(window, 0, 0, 1, 1); + saveAsButton.Text = "Save As"; + saveAsButton.OnClick = (_, _) => SaveAs(); + wm.AddWindow(saveAsButton); + + cellLabel = new TextBlock(window, 0, 0, 1, 1); + cellLabel.Text = "A1"; + cellLabel.Foreground = UITheme.TextPrimary; + wm.AddWindow(cellLabel); + + editorBox = new TextBox(window, 0, 0, 1, 1); + editorBox.PlaceholderText = "Cell value"; + editorBox.Submitted = ApplyEdit; + wm.AddWindow(editorBox); + + applyButton = new Button(window, 0, 0, 1, 1); + applyButton.Text = "Apply"; + applyButton.OnClick = (_, _) => ApplyEdit(); + wm.AddWindow(applyButton); + + leftButton = new Button(window, 0, 0, 1, 1); + leftButton.Text = "<"; + leftButton.OnClick = (_, _) => + { + startCol = Math.Max(0, startCol - 1); + RenderGrid(); + }; + wm.AddWindow(leftButton); + + rightButton = new Button(window, 0, 0, 1, 1); + rightButton.Text = ">"; + rightButton.OnClick = (_, _) => + { + startCol = Math.Min(MaxCols - 1, startCol + 1); + RenderGrid(); + }; + wm.AddWindow(rightButton); + + upButton = new Button(window, 0, 0, 1, 1); + upButton.Text = "^"; + upButton.OnClick = (_, _) => + { + startRow = Math.Max(0, startRow - 1); + RenderGrid(); + }; + wm.AddWindow(upButton); + + downButton = new Button(window, 0, 0, 1, 1); + downButton.Text = "v"; + downButton.OnClick = (_, _) => + { + startRow = Math.Min(MaxRows - 1, startRow + 1); + RenderGrid(); + }; + wm.AddWindow(downButton); + + grid = new Window(this, window, 0, ToolbarHeight, window.Width, window.Height - ToolbarHeight - StatusHeight); + grid.OnDown = GridDown; + wm.AddWindow(grid); + + statusBar = new StatusBar(window, 0, window.Height - StatusHeight, window.Width, StatusHeight); + statusBar.Text = "Ready"; + statusBar.DetailText = "Unsaved (.cws)"; + wm.AddWindow(statusBar); + + ClearSheet(); + Layout(); + + if (!string.IsNullOrWhiteSpace(startupPath)) + { + OpenFromPath(startupPath); + } + + wm.Update(window); + } + + public override void Run() + { + } + } +}