#nullable enable namespace UICatalog.Scenarios; [ScenarioMetadata ("Notepad", "Multi-tab text editor using the TabView control.")] [ScenarioCategory ("Controls")] [ScenarioCategory ("TabView")] [ScenarioCategory ("TextView")] public class Notepad : Scenario { private TabView? _focusedTabView; private int _numNewTabs = 1; private TabView? _tabView; public Shortcut? LenShortcut { get; private set; } public override void Main () { Application.Init (); Window top = new () { BorderStyle = LineStyle.None, }; // MenuBar MenuBar menu = new (); menu.Add ( new MenuBarItem ( "_File", [ new MenuItem { Title = "_New", Key = Key.N.WithCtrl.WithAlt, Action = New }, new MenuItem { Title = "_Open", Action = Open }, new MenuItem { Title = "_Save", Action = Save }, new MenuItem { Title = "Save _As", Action = () => SaveAs () }, new MenuItem { Title = "_Close", Action = Close }, new MenuItem { Title = "_Quit", Action = Quit } ] ) ); menu.Add ( new MenuBarItem ( "_About", [ new MenuItem { Title = "_About", Action = () => MessageBox.Query (Application.Instance, "Notepad", "About Notepad...", "Ok") } ] ) ); _tabView = CreateNewTabView (); _tabView.Style.ShowBorder = true; _tabView.ApplyStyleChanges (); _tabView.X = 0; _tabView.Y = Pos.Bottom (menu); _tabView.Width = Dim.Fill (); _tabView.Height = Dim.Fill (1); LenShortcut = new (Key.Empty, "Len: ", null); // StatusBar StatusBar statusBar = new ( [ new (Application.QuitKey, "Quit", Quit), new (Key.F2, "Open", Open), new (Key.F1, "New", New), new (Key.F3, "Save", Save), new (Key.F6, "Close", Close), LenShortcut ] ) { AlignmentModes = AlignmentModes.IgnoreFirstOrLast }; top.Add (menu, _tabView, statusBar); _focusedTabView = _tabView; _tabView.SelectedTabChanged += TabView_SelectedTabChanged; _tabView.HasFocusChanging += (s, e) => _focusedTabView = _tabView; top.IsModalChanged += (s, e) => { if (e.Value) { New (); LenShortcut.Title = $"Len:{_focusedTabView?.Text?.Length ?? 0}"; } }; Application.Run (top); top.Dispose (); Application.Shutdown (); } public void Save () { if (_focusedTabView?.SelectedTab is { }) { Save (_focusedTabView, _focusedTabView.SelectedTab); } } public void Save (TabView tabViewToSave, Tab tabToSave) { if (tabToSave is not OpenedFile tab) { return; } if (tab.File is null) { SaveAs (); } else { tab.Save (); } tabViewToSave.SetNeedsDraw (); } public bool SaveAs () { if (_focusedTabView?.SelectedTab is not OpenedFile tab) { return false; } SaveDialog fd = new (); Application.Run (fd); if (string.IsNullOrWhiteSpace (fd.Path) || fd.Canceled) { fd.Dispose (); return false; } tab.File = new (fd.Path); tab.Text = fd.FileName; tab.Save (); fd.Dispose (); return true; } private void Close () { if (_focusedTabView?.SelectedTab is { }) { Close (_focusedTabView, _focusedTabView.SelectedTab); } } private void Close (TabView tv, Tab tabToClose) { if (tabToClose is not OpenedFile tab) { return; } _focusedTabView = tv; if (tab.UnsavedChanges) { int? result = MessageBox.Query (Application.Instance, "Save Changes", $"Save changes to {tab.Text.TrimEnd ('*')}", "Yes", "No", "Cancel" ); if (result is null || result == 2) { // user cancelled return; } if (result == 0) { if (tab.File is null) { SaveAs (); } else { tab.Save (); } } } // close and dispose the tab tv.RemoveTab (tab); tab.View?.Dispose (); _focusedTabView = tv; // If last tab is closed, open a new one if (tv.Tabs.Count == 0) { New (); } } private TabView CreateNewTabView () { TabView tv = new () { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; tv.TabClicked += TabView_TabClicked; tv.SelectedTabChanged += TabView_SelectedTabChanged; tv.HasFocusChanging += (s, e) => _focusedTabView = tv; return tv; } private void New () { Open (null!, $"new {_numNewTabs++}"); } private void Open () { OpenDialog open = new () { Title = "Open", AllowsMultipleSelection = true }; Application.Run (open); bool canceled = open.Canceled; if (!canceled) { foreach (string path in open.FilePaths) { if (string.IsNullOrEmpty (path) || !File.Exists (path)) { break; } // TODO should open in focused TabView Open (new (path), Path.GetFileName (path)); } } open.Dispose (); } /// Creates a new tab with initial text /// File that was read or null if a new blank document /// private void Open (FileInfo? fileInfo, string tabName) { if (_focusedTabView is null) { return; } OpenedFile tab = new (this) { DisplayText = tabName, File = fileInfo }; tab.View = tab.CreateTextView (fileInfo); tab.SavedText = tab.View.Text; tab.RegisterTextViewEvents (_focusedTabView); _focusedTabView.AddTab (tab, true); } private void Quit () { Application.RequestStop (); } private void TabView_SelectedTabChanged (object? sender, TabChangedEventArgs e) { if (LenShortcut is { }) { LenShortcut.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; } e.NewTab?.View?.SetFocus (); } private void TabView_TabClicked (object? sender, TabMouseEventArgs e) { // we are only interested in right clicks if (!e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) { return; } View [] items; if (e.Tab is null) { items = [new MenuItem { Title = "Open", Action = Open }]; } else { var tv = (TabView)sender!; items = [ new MenuItem { Title = "Save", Action = () => Save (_focusedTabView!, e.Tab) }, new MenuItem { Title = "Close", Action = () => Close (tv, e.Tab) } ]; } PopoverMenu contextMenu = new (items); // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. if (sender is TabView tabView && tabView.App?.Popover is { }) { tabView.App.Popover.Register (contextMenu); } contextMenu.MakeVisible (e.MouseEvent.ScreenPosition); e.MouseEvent.Handled = true; } private class OpenedFile (Notepad notepad) : Tab { private readonly Notepad _notepad = notepad; public OpenedFile CloneTo (TabView other) { OpenedFile newTab = new (_notepad) { DisplayText = Text, File = File }; newTab.View = newTab.CreateTextView (newTab.File); newTab.SavedText = newTab.View.Text; newTab.RegisterTextViewEvents (other); other.AddTab (newTab, true); return newTab; } public View CreateTextView (FileInfo? file) { var initialText = string.Empty; if (file is { Exists: true }) { initialText = System.IO.File.ReadAllText (file.FullName); } return new TextView { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), Text = initialText, AllowsTab = false }; } public FileInfo? File { get; set; } public void RegisterTextViewEvents (TabView parent) { if (View is not TextView textView) { return; } // when user makes changes rename tab to indicate unsaved textView.ContentsChanged += (s, k) => { // if current text doesn't match saved text bool areDiff = UnsavedChanges; if (areDiff) { if (!DisplayText.EndsWith ('*')) { DisplayText = Text + '*'; } } else { if (DisplayText.EndsWith ('*')) { DisplayText = Text.TrimEnd ('*'); } } if (_notepad.LenShortcut is { }) { _notepad.LenShortcut.Title = $"Len:{textView.Text.Length}"; } }; } /// The text of the tab the last time it was saved public string? SavedText { get; set; } public bool UnsavedChanges => View is { } && !string.Equals (SavedText, View.Text); internal void Save () { if (View is null || File is null || string.IsNullOrWhiteSpace (File.FullName)) { return; } string newText = View.Text; System.IO.File.WriteAllText (File.FullName, newText); SavedText = newText; DisplayText = DisplayText.TrimEnd ('*'); } } }