From a0eaf70304db2524f7d9d7e455da14ec35ebc461 Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Tue, 24 Mar 2026 22:10:25 +0800 Subject: [PATCH] =?UTF-8?q?MarkIt=E6=9F=A5=E7=9C=8B=E5=99=A8=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BuildTime.txt | 2 +- GitCommit.txt | 2 +- Gui/AppManager.cs | 1 + Gui/Apps/Files.cs | 20 +- Gui/Apps/MarkItViewer.cs | 428 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 Gui/Apps/MarkItViewer.cs diff --git a/BuildTime.txt b/BuildTime.txt index 672aed1..7ab80b0 100644 --- a/BuildTime.txt +++ b/BuildTime.txt @@ -1 +1 @@ -2026-03-24 20:13:51 \ No newline at end of file +2026-03-24 22:06:48 \ No newline at end of file diff --git a/GitCommit.txt b/GitCommit.txt index 243dab1..a6ab992 100644 --- a/GitCommit.txt +++ b/GitCommit.txt @@ -1 +1 @@ -4dd47bf \ No newline at end of file +10e3a9d \ No newline at end of file diff --git a/Gui/AppManager.cs b/Gui/AppManager.cs index 8cc0da3..8569a3c 100644 --- a/Gui/AppManager.cs +++ b/Gui/AppManager.cs @@ -138,6 +138,7 @@ namespace CMLeonOS.Gui RegisterApp(new AppMetadata("Memory Statistics", () => { return new Apps.MemoryStatistics(); }, Icons.Icon_MemoryStatistics, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("CodeStudio", () => { return new Apps.CodeStudio.CodeStudio(); }, Icons.Icon_CodeStudio, Color.FromArgb(14, 59, 76))); RegisterApp(new AppMetadata("Image Viewer", () => { return new ImageViewer(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); + RegisterApp(new AppMetadata("MarkIt Viewer", () => { return new MarkItViewer(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); Logger.Logger.Instance.Info("AppManager", $"{AppMetadatas.Count} apps were registered."); diff --git a/Gui/Apps/Files.cs b/Gui/Apps/Files.cs index 61f9798..dda4b46 100644 --- a/Gui/Apps/Files.cs +++ b/Gui/Apps/Files.cs @@ -104,7 +104,7 @@ namespace CMLeonOS.Gui.Apps return extension switch { - ".txt" or ".md" or ".log" => Icons.Icon_File_Text, + ".txt" or ".md" or ".log" or ".mi" => Icons.Icon_File_Text, ".rs" => Icons.Icon_File_Rs, ".ini" or ".cfg" => Icons.Icon_File_Config, _ => Icons.Icon_File @@ -246,6 +246,10 @@ namespace CMLeonOS.Gui.Apps { ProcessManager.AddProcess(this, new ImageViewer(path)).Start(); } + else if (extension == ".mi") + { + ProcessManager.AddProcess(this, new MarkItViewer(path)).Start(); + } else { ShowOpenWithPrompt(path); @@ -272,6 +276,9 @@ namespace CMLeonOS.Gui.Apps case "Image Viewer": ProcessManager.AddProcess(this, new ImageViewer(path)).Start(); break; + case "MarkIt Viewer": + ProcessManager.AddProcess(this, new MarkItViewer(path)).Start(); + break; default: Logger.Logger.Instance.Warning("Files", $"Unsupported open-with app: {appName}"); break; @@ -280,7 +287,7 @@ namespace CMLeonOS.Gui.Apps private void ShowOpenWithPrompt(string path) { - AppWindow openWithWindow = new AppWindow(this, 320, 240, 300, 176); + AppWindow openWithWindow = new AppWindow(this, 320, 240, 300, 206); openWithWindow.Title = "Open With"; openWithWindow.Icon = AppManager.DefaultAppIcon; wm.AddWindow(openWithWindow); @@ -317,6 +324,15 @@ namespace CMLeonOS.Gui.Apps }; wm.AddWindow(imageViewerButton); + Button markItButton = new Button(openWithWindow, 12, 98, 84, 24); + markItButton.Text = "MarkIt"; + markItButton.OnClick = (_, _) => + { + wm.RemoveWindow(openWithWindow); + OpenWithApp(path, "MarkIt Viewer"); + }; + wm.AddWindow(markItButton); + Button cancelButton = new Button(openWithWindow, openWithWindow.Width - 80 - 12, openWithWindow.Height - 20 - 12, 80, 20); cancelButton.Text = "Cancel"; cancelButton.OnClick = (_, _) => diff --git a/Gui/Apps/MarkItViewer.cs b/Gui/Apps/MarkItViewer.cs new file mode 100644 index 0000000..5985940 --- /dev/null +++ b/Gui/Apps/MarkItViewer.cs @@ -0,0 +1,428 @@ +using CMLeonOS; +using CMLeonOS.Gui.SmoothMono; +using CMLeonOS.Gui.UILib; +using CMLeonOS.Logger; +using CMLeonOS.Utils; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; + +namespace CMLeonOS.Gui.Apps +{ + internal class MarkItViewer : Process + { + internal MarkItViewer() : base("MarkIt Viewer", ProcessType.Application) { } + + internal MarkItViewer(string path) : base("MarkIt Viewer", ProcessType.Application) + { + initialPath = path; + } + + private struct StyledGlyph + { + internal char Character; + internal Color Foreground; + internal Color Background; + internal bool HasBackground; + } + + private class StyledLine + { + internal List Glyphs { get; } = new List(); + } + + private AppWindow window; + private Window statusHeader; + private Window contentView; + private Button openButton; + private Button upButton; + private Button downButton; + + private readonly WindowManager wm = ProcessManager.GetProcess(); + private FileBrowser fileBrowser; + + private readonly List lines = new List(); + private int scrollLine = 0; + private string currentPath = string.Empty; + private string initialPath = string.Empty; + private string statusText = "Open a MarkIt file to preview."; + + private const int toolbarHeight = 32; + private const int headerHeight = 50; + private const int padding = 8; + private const int buttonWidth = 64; + private const int buttonHeight = 24; + private const int contentInset = 8; + + private static bool MatchesAt(string text, int index, string token) + { + if (index < 0 || token == null || index + token.Length > text.Length) + { + return false; + } + + for (int i = 0; i < token.Length; i++) + { + if (text[index + i] != token[i]) + { + return false; + } + } + + return true; + } + + private static Color ParseColor(string colorName) + { + if (string.IsNullOrWhiteSpace(colorName)) + { + return UITheme.TextPrimary; + } + + string name = colorName.ToLower().Trim(); + switch (name) + { + case "black": return Color.Black; + case "darkblue": return Color.DarkBlue; + case "darkgreen": return Color.DarkGreen; + case "darkcyan": return Color.DarkCyan; + case "darkred": return Color.DarkRed; + case "darkmagenta": return Color.DarkMagenta; + case "darkyellow": return Color.FromArgb(112, 96, 0); + case "gray": return Color.Gray; + case "darkgray": return Color.DarkGray; + case "blue": return Color.Blue; + case "green": return Color.Green; + case "cyan": return Color.Cyan; + case "red": return Color.Red; + case "magenta": return Color.Magenta; + case "yellow": return Color.Yellow; + case "white": return Color.White; + default: return UITheme.TextPrimary; + } + } + + private void SetStatus(string text) + { + statusText = text; + RenderHeader(); + } + + private void RenderHeader() + { + statusHeader.Clear(Color.FromArgb(235, 241, 248)); + statusHeader.DrawRectangle(0, 0, statusHeader.Width, statusHeader.Height, Color.FromArgb(180, 192, 208)); + statusHeader.DrawString("MarkIt Viewer", Color.FromArgb(28, 38, 52), 12, 8); + statusHeader.DrawString(statusText, Color.FromArgb(97, 110, 126), 12, 26); + wm.Update(statusHeader); + } + + private void Relayout() + { + openButton.MoveAndResize(window.Width - ((buttonWidth * 3) + (padding * 3)), padding, buttonWidth, buttonHeight); + upButton.MoveAndResize(window.Width - ((buttonWidth * 2) + (padding * 2)), padding, buttonWidth, buttonHeight); + downButton.MoveAndResize(window.Width - (buttonWidth + padding), padding, buttonWidth, buttonHeight); + + int contentY = toolbarHeight + headerHeight; + statusHeader.MoveAndResize(0, toolbarHeight, window.Width, headerHeight); + contentView.MoveAndResize(0, contentY, window.Width, window.Height - contentY); + + openButton.Render(); + upButton.Render(); + downButton.Render(); + RenderHeader(); + RenderContent(); + } + + private void ResetDocument() + { + lines.Clear(); + lines.Add(new StyledLine()); + scrollLine = 0; + currentPath = string.Empty; + window.Title = "MarkIt Viewer"; + SetStatus("Open a MarkIt file to preview."); + RenderContent(); + } + + private void ParseDocument(string content) + { + lines.Clear(); + lines.Add(new StyledLine()); + + Color currentForeground = UITheme.TextPrimary; + Color currentBackground = UITheme.Surface; + bool hasBackground = false; + + int i = 0; + while (i < content.Length) + { + if (MatchesAt(content, i, "{/color}")) + { + currentForeground = UITheme.TextPrimary; + i += 8; + continue; + } + + if (MatchesAt(content, i, "{/bg}")) + { + hasBackground = false; + i += 5; + continue; + } + + if (MatchesAt(content, i, "{color:")) + { + int end = content.IndexOf('}', i); + if (end > i + 7) + { + currentForeground = ParseColor(content.Substring(i + 7, end - (i + 7))); + i = end + 1; + continue; + } + } + + if (MatchesAt(content, i, "{bg:")) + { + int end = content.IndexOf('}', i); + if (end > i + 4) + { + currentBackground = ParseColor(content.Substring(i + 4, end - (i + 4))); + hasBackground = true; + i = end + 1; + continue; + } + } + + char ch = content[i]; + if (ch == '\r') + { + i++; + continue; + } + + if (ch == '\n') + { + lines.Add(new StyledLine()); + i++; + continue; + } + + lines[lines.Count - 1].Glyphs.Add(new StyledGlyph + { + Character = ch, + Foreground = currentForeground, + Background = currentBackground, + HasBackground = hasBackground + }); + i++; + } + + if (lines.Count == 0) + { + lines.Add(new StyledLine()); + } + } + + private int VisibleLineCount() + { + int visible = (contentView.Height - (contentInset * 2)) / FontData.Height; + return Math.Max(1, visible); + } + + private int VisibleColumnCount() + { + int visible = (contentView.Width - (contentInset * 2)) / FontData.Width; + return Math.Max(1, visible); + } + + private void ClampScroll() + { + int maxScroll = Math.Max(0, lines.Count - VisibleLineCount()); + if (scrollLine < 0) + { + scrollLine = 0; + } + if (scrollLine > maxScroll) + { + scrollLine = maxScroll; + } + } + + private void RenderContent() + { + contentView.Clear(Color.FromArgb(252, 254, 255)); + contentView.DrawRectangle(0, 0, contentView.Width, contentView.Height, Color.FromArgb(192, 204, 221)); + + ClampScroll(); + + int visibleLines = VisibleLineCount(); + int visibleColumns = VisibleColumnCount(); + int startLine = scrollLine; + int endLineExclusive = Math.Min(lines.Count, startLine + visibleLines); + + int drawY = contentInset; + for (int lineIndex = startLine; lineIndex < endLineExclusive; lineIndex++) + { + StyledLine line = lines[lineIndex]; + int drawX = contentInset; + int maxColumns = Math.Min(visibleColumns, line.Glyphs.Count); + + for (int i = 0; i < maxColumns; i++) + { + StyledGlyph glyph = line.Glyphs[i]; + if (glyph.HasBackground) + { + contentView.DrawFilledRectangle(drawX, drawY, FontData.Width, FontData.Height, glyph.Background); + } + contentView.DrawString(glyph.Character.ToString(), glyph.Foreground, drawX, drawY); + drawX += FontData.Width; + } + + drawY += FontData.Height; + } + + wm.Update(contentView); + } + + private void UpdateStatusWithPosition() + { + if (lines.Count == 0) + { + SetStatus("No content."); + return; + } + + int visible = VisibleLineCount(); + int start = Math.Min(lines.Count, scrollLine + 1); + int end = Math.Min(lines.Count, scrollLine + visible); + if (string.IsNullOrWhiteSpace(currentPath)) + { + SetStatus($"Lines {start}-{end} / {lines.Count}"); + return; + } + + SetStatus($"{Path.GetFileName(currentPath)} | Lines {start}-{end} / {lines.Count}"); + } + + private void ScrollUp(int x, int y) + { + scrollLine--; + ClampScroll(); + RenderContent(); + UpdateStatusWithPosition(); + } + + private void ScrollDown(int x, int y) + { + scrollLine++; + ClampScroll(); + RenderContent(); + UpdateStatusWithPosition(); + } + + private bool LoadFile(string path, bool showPopup = true) + { + if (string.IsNullOrWhiteSpace(path)) + { + if (showPopup) + { + new MessageBox(this, "MarkIt Viewer", "Path is empty.").Show(); + } + return false; + } + + string sanitizedPath = PathUtil.Sanitize(path.Trim()); + if (!File.Exists(sanitizedPath)) + { + if (showPopup) + { + new MessageBox(this, "MarkIt Viewer", "File not found.").Show(); + } + return false; + } + + try + { + string content = File.ReadAllText(sanitizedPath); + ParseDocument(content); + scrollLine = 0; + currentPath = sanitizedPath; + window.Title = $"MarkIt Viewer - {Path.GetFileName(sanitizedPath)}"; + RenderContent(); + UpdateStatusWithPosition(); + return true; + } + catch (Exception ex) + { + Logger.Logger.Instance.Error("MarkItViewer", $"Failed to open file: {ex.Message}"); + if (showPopup) + { + new MessageBox(this, "MarkIt Viewer", "Unable to open this file.").Show(); + } + return false; + } + } + + private void OpenDialog() + { + fileBrowser = new FileBrowser(this, wm, (string selectedPath) => + { + if (!string.IsNullOrWhiteSpace(selectedPath)) + { + LoadFile(selectedPath); + } + }); + fileBrowser.Show(); + } + + public override void Start() + { + base.Start(); + + window = new AppWindow(this, 180, 110, 760, 500); + window.Title = "MarkIt Viewer"; + window.Icon = AppManager.DefaultAppIcon; + window.CanResize = true; + window.UserResized = Relayout; + window.Closing = TryStop; + wm.AddWindow(window); + + openButton = new Button(window, 0, 0, 1, 1); + openButton.Text = "Open"; + openButton.OnClick = (_, _) => OpenDialog(); + wm.AddWindow(openButton); + + upButton = new Button(window, 0, 0, 1, 1); + upButton.Text = "Up"; + upButton.OnClick = ScrollUp; + wm.AddWindow(upButton); + + downButton = new Button(window, 0, 0, 1, 1); + downButton.Text = "Down"; + downButton.OnClick = ScrollDown; + wm.AddWindow(downButton); + + statusHeader = new Window(this, window, 0, toolbarHeight, 1, 1); + wm.AddWindow(statusHeader); + + contentView = new Window(this, window, 0, toolbarHeight + headerHeight, 1, 1); + wm.AddWindow(contentView); + + ResetDocument(); + Relayout(); + + if (!string.IsNullOrWhiteSpace(initialPath)) + { + LoadFile(initialPath, showPopup: false); + } + + wm.Update(window); + } + + public override void Run() + { + } + } +}