using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace OpenNest.Desktop { /// An open nest: its document, its file and whether it has unsaved changes. public sealed class NestTab { internal NestTab(Document document, string path) { Document = document; Path = path; } public Document Document { get; } /// The full path of the file it was opened from or saved to, else null. public string Path { get; internal set; } /// /// The file Save writes without asking: the one saved to this session, if it still /// exists. Null for a nest only opened, as in the WinForms app, so the first Save asks. /// public string SavePath => Document.HasSavePath ? Document.LastSavePath : null; /// True after a change this session that is not saved. public bool Modified { get; private set; } /// The nest's name, else the file name. public string Name => !string.IsNullOrWhiteSpace(Document.Name) ? Document.Name : System.IO.Path.GetFileNameWithoutExtension(Path ?? string.Empty); public void MarkModified() => Modified = true; public void MarkSaved() => Modified = false; } /// /// A save or export refused before anything was written; the message says why and what to /// do instead. /// public sealed class SaveTargetRefusedException : InvalidOperationException { public SaveTargetRefusedException(string message) : base(message) { } } /// /// The nests open in the desktop app, one tab each, and which one is active. Opening a file /// that is already open activates its tab; closing the active tab activates the one to its /// right, else the one to its left. /// public sealed class NestTabs { private readonly List tabs = new(); /// Raised after a tab is added, closed or activated. public event EventHandler Changed; public IReadOnlyList Tabs => tabs; /// The active tab's index, or -1 when no nest is open. public int ActiveIndex { get; private set; } = -1; public NestTab Active => ActiveIndex >= 0 ? tabs[ActiveIndex] : null; /// True when any open nest has unsaved changes. public bool AnyModified => tabs.Any(t => t.Modified); /// /// Activates the tab already showing , or loads it with /// into a new tab after the others. A load that throws adds no tab. /// public NestTab Open(string path, Func load) { ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(load); var fullPath = System.IO.Path.GetFullPath(path); var existing = tabs.FindIndex(t => t.Path != null && SamePath(t.Path, fullPath)); if (existing >= 0) { Activate(existing); return tabs[existing]; } var tab = new NestTab(load(fullPath), fullPath); tabs.Add(tab); ActiveIndex = tabs.Count - 1; Changed?.Invoke(this, EventArgs.Empty); return tab; } public void Activate(int index) { if (index < 0 || index >= tabs.Count) throw new ArgumentOutOfRangeException(nameof(index)); if (index == ActiveIndex) return; ActiveIndex = index; Changed?.Invoke(this, EventArgs.Empty); } /// Removes the tab; the host asks about unsaved changes first. public void Close(NestTab tab) { var index = tabs.IndexOf(tab); if (index < 0) return; tabs.RemoveAt(index); if (tabs.Count == 0) ActiveIndex = -1; else if (index < ActiveIndex) ActiveIndex--; else if (index == ActiveIndex) ActiveIndex = System.Math.Min(index, tabs.Count - 1); Changed?.Invoke(this, EventArgs.Empty); } /// /// Saves the tab's nest to , which becomes its file and name, and /// clears its unsaved mark. Refuses a file another open tab shows, so one file never has /// two tabs. A failed write throws and changes nothing. /// public void SaveAs(NestTab tab, string path) { var fullPath = CheckTarget(tab, path, export: false); tab.Document.SaveAs(fullPath); tab.Path = fullPath; tab.MarkSaved(); Changed?.Invoke(this, EventArgs.Empty); } /// /// Writes a copy of the tab's nest to without changing its file, /// name or unsaved mark. Refuses the tab's own file, which only Save may write, and a /// file another open tab shows. /// public void ExportCopy(NestTab tab, string path) { var fullPath = CheckTarget(tab, path, export: true); tab.Document.ExportCopy(fullPath); } private string CheckTarget(NestTab tab, string path, bool export) { ArgumentNullException.ThrowIfNull(tab); ArgumentNullException.ThrowIfNull(path); if (!tabs.Contains(tab)) throw new SaveTargetRefusedException( $"{tab.Name} was closed before it could be written. Open it again to save it."); var fullPath = System.IO.Path.GetFullPath(path); if (export && tab.Path != null && SamePath(tab.Path, fullPath)) throw new SaveTargetRefusedException( $"{System.IO.Path.GetFileName(fullPath)} is this nest's own file. Use " + "File > Save to save it there, or export the copy under another name."); var other = tabs.FirstOrDefault( t => t != tab && t.Path != null && SamePath(t.Path, fullPath)); if (other != null) throw new SaveTargetRefusedException( $"{System.IO.Path.GetFileName(fullPath)} is open in the {other.Name} tab. " + "Close that tab first, or choose another file name."); return fullPath; } // Windows paths ignore case; Linux and macOS paths do not. internal static StringComparer PathComparer => OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; private static bool SamePath(string a, string b) => PathComparer.Equals(a, b); } }