From 0aff2f1c87fe5fddf2b3ff030bbfb87a86f49f8b Mon Sep 17 00:00:00 2001 From: Maciej Date: Tue, 9 Jan 2024 19:38:45 +0100 Subject: [PATCH] Fixes #3149: UICatalog - Notepad example improvements (#3150) * Fix the application crashing when closing the save file dialog without saving * Allow user to write in multiple tabs without saving file * Maintain code consistency --------- Co-authored-by: Tig --- UICatalog/Scenarios/Notepad.cs | 730 +++++++++++++++++---------------- 1 file changed, 367 insertions(+), 363 deletions(-) diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index 5361b57ae..166669a65 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -1,413 +1,417 @@ -using System; -using System.IO; +using System.IO; using System.Linq; using Terminal.Gui; -namespace UICatalog.Scenarios { +namespace UICatalog.Scenarios; - [ScenarioMetadata (Name: "Notepad", Description: "Multi-tab text editor using the TabView control.")] - [ScenarioCategory ("Controls"), ScenarioCategory ("TabView"), ScenarioCategory ("TextView")] - public class Notepad : Scenario { - TabView tabView; +[ScenarioMetadata (Name: "Notepad", Description: "Multi-tab text editor using the TabView control.")] +[ScenarioCategory ("Controls"), ScenarioCategory ("TabView"), ScenarioCategory ("TextView")] +public class Notepad : Scenario { + TabView tabView; - private int numbeOfNewTabs = 1; - private TabView focusedTabView; - private StatusItem lenStatusItem; + 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; - } + // 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(), null, null, KeyCode.N | KeyCode.CtrlMask | KeyCode.AltMask), - new MenuItem ("_Open", "", () => Open()), - new MenuItem ("_Save", "", () => Save()), - new MenuItem ("Save _As", "", () => SaveAs()), - new MenuItem ("_Close", "", () => Close()), - new MenuItem ("_Quit", "", () => Quit()), - }), - new MenuBarItem ("_About", "", () => MessageBox.Query("Notepad", "About Notepad...", "Ok")) - }); - 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 (KeyCode.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(KeyCode.CtrlMask | KeyCode.S, "~^S~ Save", () => Save()), - new StatusItem(KeyCode.CtrlMask | KeyCode.W, "~^W~ Close", () => Close()), - lenStatusItem, + public override void Setup () + { + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_New", "", () => New(), null, null, KeyCode.N | KeyCode.CtrlMask | KeyCode.AltMask), + new MenuItem ("_Open", "", () => Open()), + new MenuItem ("_Save", "", () => Save()), + new MenuItem ("Save _As", "", () => SaveAs()), + new MenuItem ("_Close", "", () => Close()), + new MenuItem ("_Quit", "", () => Quit()), + }), + new MenuBarItem ("_About", "", () => MessageBox.Query("Notepad", "About Notepad...", "Ok")) }); - focusedTabView = tabView; - tabView.SelectedTabChanged += TabView_SelectedTabChanged; - tabView.Enter += (s, e) => focusedTabView = tabView; + Application.Top.Add (menu); - Application.Top.Add (statusBar); - Application.Top.Ready += (s, e) => New (); + 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 (KeyCode.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(KeyCode.CtrlMask | KeyCode.S, "~^S~ Save", () => Save()), + new StatusItem(KeyCode.CtrlMask | KeyCode.W, "~^W~ Close", () => Close()), + _lenStatusItem, + }); + _focusedTabView = tabView; + tabView.SelectedTabChanged += TabView_SelectedTabChanged; + tabView.Enter += (s, e) => _focusedTabView = tabView; + + Application.Top.Add (statusBar); + Application.Top.Ready += (s, e) => New (); + } + + private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e) + { + _lenStatusItem.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; } - private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e) - { - lenStatusItem.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; + 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)), + }); } - private void TabView_TabClicked (object sender, TabMouseEventArgs e) - { - // we are only interested in right clicks - if (!e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) { + ((View)sender).BoundsToScreen (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; } - 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)), - }); + if (result == 0) { + if (tab.File == null) { + SaveAs (); + } else { + tab.Save (); + } } - - ((View)sender).BoundsToScreen (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); + // close and dispose the tab + tv.RemoveTab (tab); + tab.View.Dispose (); + _focusedTabView = tv; - } - 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); - } + if (tv.Tabs.Count == 0) { - private void Split (int offset, Orientation orientation, TabView sender, OpenedFile tab) - { + var split = (TileView)tv.SuperView.SuperView; - var split = (TileView)sender.SuperView.SuperView; - var tileIndex = split.IndexOf (sender); - - if (tileIndex == -1) { + // 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; } - if (orientation != split.Orientation) { - split.TrySplitTile (tileIndex, 1, out split); - split.Orientation = orientation; - tileIndex = 0; + 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); } + } + } - var newTile = split.InsertTile (tileIndex + offset); - var newTabView = CreateNewTabView (); - tab.CloneTo (newTabView); - newTile.ContentView.Add (newTabView); + private void Open () + { + var open = new OpenDialog ("Open") { AllowsMultipleSelection = true }; - newTabView.EnsureFocus (); - newTabView.FocusFirst (); - newTabView.FocusNext (); + 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; } - private TabView CreateNewTabView () + 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)) { - var tv = new TabView () { + 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 (!Text.EndsWith ('*')) { + + Text = 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, }; - - tv.TabClicked += TabView_TabClicked; - tv.SelectedTabChanged += TabView_SelectedTabChanged; - tv.Enter += (s, e) => focusedTabView = tv; - return tv; } - private void New () + public OpenedFile CloneTo (TabView other) { - Open (null, $"new {numbeOfNewTabs++}"); + var newTab = new OpenedFile (other, base.Text.ToString (), File); + other.AddTab (newTab, true); + return newTab; } - private void Close () + internal void Save () { - Close (focusedTabView, focusedTabView.SelectedTab); - } - private void Close (TabView tv, Tab tabToClose) - { - var tab = tabToClose as OpenedFile; + var newText = View.Text; - if (tab == null) { + if (File is null || string.IsNullOrWhiteSpace (File.FullName)) { return; } - focusedTabView = tv; + System.IO.File.WriteAllText (File.FullName, newText); + SavedText = newText; - 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 (); + Text = Text.TrimEnd ('*'); } } + + private void Quit () + { + Application.RequestStop (); + } }