using System; using System.IO; using System.Linq; using Terminal.Gui; namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "Notepad", Description: "Multi-tab text editor uising the TabView control.")] [ScenarioCategory ("Controls"), ScenarioCategory ("TabView")] public class Notepad : Scenario { TabView tabView; private int numbeOfNewTabs = 1; private TabView focusedTabView; private StatusItem lenStatusItem; // Don't create a Window, just return the top-level view public override void Init () { Application.Init (); Application.Top.ColorScheme = Colors.Base; } public override void Setup () { var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_New", "", () => New()), new MenuItem ("_Open", "", () => Open()), new MenuItem ("_Save", "", () => Save()), new MenuItem ("Save _As", "", () => SaveAs()), new MenuItem ("_Close", "", () => Close()), new MenuItem ("_Quit", "", () => Quit()), }) }); Application.Top.Add (menu); tabView = CreateNewTabView (); tabView.Style.ShowBorder = true; tabView.ApplyStyleChanges (); // Start with only a single view but support splitting to show side by side var split = new TileView(1) { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1), }; split.Tiles.ElementAt(0).ContentView.Add (tabView); split.LineStyle = LineStyle.None; Application.Top.Add (split); lenStatusItem = new StatusItem (Key.CharMask, "Len: ", null); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), // These shortcut keys don't seem to work correctly in linux //new StatusItem(Key.CtrlMask | Key.N, "~^O~ Open", () => Open()), //new StatusItem(Key.CtrlMask | Key.N, "~^N~ New", () => New()), new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()), new StatusItem(Key.CtrlMask | Key.W, "~^W~ Close", () => Close()), lenStatusItem, }); focusedTabView = tabView; tabView.SelectedTabChanged += TabView_SelectedTabChanged; tabView.Enter += (s, e) => focusedTabView = tabView; Application.Top.Add (statusBar); New (); } private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e) { lenStatusItem.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; } private void TabView_TabClicked (object sender, TabMouseEventArgs e) { // we are only interested in right clicks if(!e.MouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked)) { return; } MenuBarItem items; if (e.Tab == null) { items = new MenuBarItem (new MenuItem [] { new MenuItem ($"Open", "", () => Open()), }); } else { var tv = (TabView)sender; var t = (OpenedFile)e.Tab; items = new MenuBarItem (new MenuItem [] { new MenuItem ($"Save", "", () => Save(focusedTabView, e.Tab)), new MenuItem ($"Close", "", () => Close(tv, e.Tab)), null, new MenuItem ($"Split Up", "", () => SplitUp(tv,t)), new MenuItem ($"Split Down", "", () => SplitDown(tv,t)), new MenuItem ($"Split Right", "", () => SplitRight(tv,t)), new MenuItem ($"Split Left", "", () => SplitLeft(tv,t)), }); } ((View)sender).ViewToScreen (e.MouseEvent.X, e.MouseEvent.Y, out int screenX, out int screenY,true); var contextMenu = new ContextMenu (screenX,screenY, items); contextMenu.Show (); e.MouseEvent.Handled = true; } private void SplitUp (TabView sender, OpenedFile tab) { Split(0, Orientation.Horizontal,sender,tab); } private void SplitDown (TabView sender, OpenedFile tab) { Split(1, Orientation.Horizontal,sender,tab); } private void SplitLeft (TabView sender, OpenedFile tab) { Split(0, Orientation.Vertical,sender,tab); } private void SplitRight (TabView sender, OpenedFile tab) { Split(1, Orientation.Vertical,sender,tab); } private void Split (int offset, Orientation orientation,TabView sender, OpenedFile tab) { var split = (TileView)sender.SuperView.SuperView; var tileIndex = split.IndexOf(sender); if(tileIndex == -1) { return; } if(orientation != split.Orientation) { split.TrySplitTile(tileIndex,1,out split); split.Orientation = orientation; tileIndex = 0; } var newTile = split.InsertTile(tileIndex + offset); var newTabView = CreateNewTabView (); tab.CloneTo (newTabView); newTile.ContentView.Add(newTabView); newTabView.EnsureFocus(); newTabView.FocusFirst(); newTabView.FocusNext(); } private TabView CreateNewTabView () { var tv = new TabView () { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), }; tv.TabClicked += TabView_TabClicked; tv.SelectedTabChanged += TabView_SelectedTabChanged; tv.Enter += (s, e) => focusedTabView = tv; return tv; } private void New () { Open (null, $"new {numbeOfNewTabs++}"); } private void Close () { Close (focusedTabView, focusedTabView.SelectedTab); } private void Close (TabView tv, Tab tabToClose) { var tab = tabToClose as OpenedFile; if (tab == null) { return; } focusedTabView = tv; if (tab.UnsavedChanges) { int result = MessageBox.Query ("Save Changes", $"Save changes to {tab.Text.TrimEnd ('*')}", "Yes", "No", "Cancel"); if (result == -1 || result == 2) { // user cancelled return; } if (result == 0) { if(tab.File == null) { SaveAs (); } else { tab.Save (); } } } // close and dispose the tab tv.RemoveTab (tab); tab.View.Dispose (); focusedTabView = tv; if(tv.Tabs.Count == 0) { var split = (TileView)tv.SuperView.SuperView; // if it is the last TabView on screen don't drop it or we will // be unable to open new docs! if(split.IsRootTileView() && split.Tiles.Count == 1) { return; } var tileIndex = split.IndexOf (tv); split.RemoveTile (tileIndex); if(split.Tiles.Count == 0) { var parent = split.GetParentTileView (); if (parent == null) { return; } var idx = parent.IndexOf (split); if (idx == -1) { return; } parent.RemoveTile (idx); } } } private void Open () { var open = new OpenDialog ("Open") { AllowsMultipleSelection = true }; Application.Run (open); if (!open.Canceled) { foreach (var path in open.FilePaths) { if (string.IsNullOrEmpty (path) || !File.Exists (path)) { return; } // TODO should open in focused TabView Open (new FileInfo (path), Path.GetFileName (path)); } } } /// /// 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) { var tab = new OpenedFile (focusedTabView, tabName, fileInfo); focusedTabView.AddTab (tab, true); } public void Save () { Save (focusedTabView, focusedTabView.SelectedTab); } public void Save (TabView tabViewToSave, Tab tabToSave) { var tab = tabToSave as OpenedFile; if (tab == null) { return; } if (tab.File == null) { SaveAs (); } tab.Save (); tabViewToSave.SetNeedsDisplay (); } public bool SaveAs () { var tab = focusedTabView.SelectedTab as OpenedFile; if (tab == null) { return false; } var fd = new SaveDialog (); Application.Run (fd); if (string.IsNullOrWhiteSpace (fd.Path)) { return false; } if(fd.Canceled) { return false; } tab.File = new FileInfo (fd.Path); tab.Text = fd.FileName; tab.Save (); return true; } private class OpenedFile : Tab { public FileInfo File { get; set; } /// /// The text of the tab the last time it was saved /// /// public string SavedText { get; set; } public bool UnsavedChanges => !string.Equals (SavedText, View.Text); public OpenedFile (TabView parent, string name, FileInfo file) : base (name, CreateTextView(file)) { File = file; SavedText = View.Text; RegisterTextViewEvents (parent); } private void RegisterTextViewEvents (TabView parent) { var textView = (TextView)View; // when user makes changes rename tab to indicate unsaved textView.KeyUp += (s, k) => { // if current text doesn't match saved text var areDiff = this.UnsavedChanges; if (areDiff) { if (!this.Text.EndsWith ('*')) { this.Text = this.Text + '*'; parent.SetNeedsDisplay (); } } else { if (Text.EndsWith ('*')) { Text = Text.TrimEnd ('*'); parent.SetNeedsDisplay (); } } }; } private static View CreateTextView (FileInfo file) { string initialText = string.Empty; if(file != null && file.Exists) { 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 OpenedFile CloneTo(TabView other) { var newTab = new OpenedFile (other, base.Text.ToString(), File); other.AddTab (newTab, true); return newTab; } internal void Save () { var newText = View.Text; System.IO.File.WriteAllText (File.FullName, newText); SavedText = newText; Text = Text.TrimEnd ('*'); } } private void Quit () { Application.RequestStop (); } } }