From 5b845307a2fa2a44bfc32fbd0e4d9a085dff2857 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Wed, 20 May 2020 11:37:12 -0600 Subject: [PATCH] UI catalog (#387) * key down/up support * line endings? * line endings * KeyDown/Up support * line endings * line endings * Revert "Drop NuGet restore" This reverts commit 5c7a0d05f077755943ec66e6a82db890a24cd056. * Revert "Revert "Drop NuGet restore"" This reverts commit 2dc5fce8654ffeb6f3e570b0bdefcc6a5b6a6d2b. * updated demo * defined styles * Smarter StatusBar bottom tracking. * Prepping for https://github.com/migueldeicaza/gui.cs/issues/376 * Oops. * Fixed StatusBar 'snap to bottom' * line endings * Revert "Fixed StatusBar 'snap to bottom'" This reverts commit 9a91c957e2ed40f5b36c301eda3d107366aebb3d. * started UICatalog project * Initial working POC. * Fix newlines * merge * textalignment demo tweaks * textalignment demo tweaks * Unicode Menu Scenario * not sure why this keeps changing * re-added project to .sln file * re-enabled status bar * moved scenarios to dir * building a dim and pos demo * terminal.sln * progress...barely * fixed exit * progress with some underlying fixes to Label * added readme * fixes build issue * launch * made default colors readable on Windows * major UI Catalog upgrade * added more demos and updated readme * refactored and added more tests * added ref to Issue #437 * added OnKeyUp support to Curses and Net drivers * more tweaks - grab PR #438 first * Added a OpenSelectedItem event to the ListView #429 * updates * moved KeyUpHandler out of special ESC stuff * more tweaks & improvements * testing top window bug * supported OpenSelectedItem * lots of updates * fixed regression, fixed #444 * better button scenario * tweaks * add Ready event to Toplevel * dotfx .gitignroe * ready for ready * updated colors based on feedback; consolodated config code * tweaked readme * readme * Added Editor demonstrating TextView * Added Editor demonstrating TextView * added hexeditor scenario Co-authored-by: Miguel de Icaza Co-authored-by: BDisp --- Designer/Designer.csproj | 1 + Example/Example.csproj | 1 + Example/demo.cs.orig | 611 +++++++++++++++++++++ Terminal.Gui/Core.cs | 4 +- Terminal.Gui/Drivers/CursesDriver.cs | 38 +- Terminal.Gui/Drivers/NetDriver.cs | 1 + Terminal.Gui/Drivers/WindowsDriver.cs | 121 ++-- Terminal.Gui/Event.cs | 33 +- Terminal.Gui/Views/Label.cs | 7 +- Terminal.Gui/Views/ListView.cs | 17 +- Terminal.sln | 10 + UICatalog/.editorconfig | 23 + UICatalog/.gitignore | 9 + UICatalog/Program.cs | 271 +++++++++ UICatalog/Properties/launchSettings.json | 8 + UICatalog/README.md | 122 ++++ UICatalog/Scenario.cs | 181 ++++++ UICatalog/Scenarios/Buttons.cs | 105 ++++ UICatalog/Scenarios/DimAndPosLayout.cs | 82 +++ UICatalog/Scenarios/Editor.cs | 150 +++++ UICatalog/Scenarios/Generic.cs | 17 + UICatalog/Scenarios/HexEditor.cs | 154 ++++++ UICatalog/Scenarios/Keys.cs | 181 ++++++ UICatalog/Scenarios/MessageBoxes.cs | 42 ++ UICatalog/Scenarios/Mouse.cs | 34 ++ UICatalog/Scenarios/TextAlignment.cs | 26 + UICatalog/Scenarios/TopLevelNoWindowBug.cs | 40 ++ UICatalog/Scenarios/UnicodeInMenu.cs | 35 ++ UICatalog/UICatalog.csproj | 13 + UICatalog/generic_screenshot.png | Bin 0 -> 22113 bytes UICatalog/screenshot.png | Bin 0 -> 26223 bytes 31 files changed, 2259 insertions(+), 78 deletions(-) create mode 100644 Example/demo.cs.orig create mode 100644 UICatalog/.editorconfig create mode 100644 UICatalog/.gitignore create mode 100644 UICatalog/Program.cs create mode 100644 UICatalog/Properties/launchSettings.json create mode 100644 UICatalog/README.md create mode 100644 UICatalog/Scenario.cs create mode 100644 UICatalog/Scenarios/Buttons.cs create mode 100644 UICatalog/Scenarios/DimAndPosLayout.cs create mode 100644 UICatalog/Scenarios/Editor.cs create mode 100644 UICatalog/Scenarios/Generic.cs create mode 100644 UICatalog/Scenarios/HexEditor.cs create mode 100644 UICatalog/Scenarios/Keys.cs create mode 100644 UICatalog/Scenarios/MessageBoxes.cs create mode 100644 UICatalog/Scenarios/Mouse.cs create mode 100644 UICatalog/Scenarios/TextAlignment.cs create mode 100644 UICatalog/Scenarios/TopLevelNoWindowBug.cs create mode 100644 UICatalog/Scenarios/UnicodeInMenu.cs create mode 100644 UICatalog/UICatalog.csproj create mode 100644 UICatalog/generic_screenshot.png create mode 100644 UICatalog/screenshot.png diff --git a/Designer/Designer.csproj b/Designer/Designer.csproj index 545c2b0b9..84de5ca1d 100644 --- a/Designer/Designer.csproj +++ b/Designer/Designer.csproj @@ -8,6 +8,7 @@ Designer Designer v4.7.2 + win-x86 diff --git a/Example/Example.csproj b/Example/Example.csproj index dc751b396..001459c47 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -8,6 +8,7 @@ Terminal Terminal v4.7.2 + win-x64 diff --git a/Example/demo.cs.orig b/Example/demo.cs.orig new file mode 100644 index 000000000..f3363488b --- /dev/null +++ b/Example/demo.cs.orig @@ -0,0 +1,611 @@ +using Terminal.Gui; +using System; +using Mono.Terminal; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using NStack; +using System.Text; + +static class Demo { + //class Box10x : View, IScrollView { + class Box10x : View { + int w = 40; + int h = 50; + + public bool WantCursorPosition { get; set; } = false; + + public Box10x (int x, int y) : base (new Rect (x, y, 20, 10)) + { + } + + public Size GetContentSize () + { + return new Size (w, h); + } + + public void SetCursorPosition (Point pos) + { + throw new NotImplementedException (); + } + + public override void Redraw (Rect region) + { + //Point pos = new Point (region.X, region.Y); + Driver.SetAttribute (ColorScheme.Focus); + + for (int y = 0; y < h; y++) { + Move (0, y); + Driver.AddStr (y.ToString ()); + for (int x = 0; x < w - y.ToString ().Length; x++) { + //Driver.AddRune ((Rune)('0' + (x + y) % 10)); + if (y.ToString ().Length < w) + Driver.AddStr (" "); + } + } + //Move (pos.X, pos.Y); + } + } + + class Filler : View { + public Filler (Rect rect) : base (rect) + { + } + + public override void Redraw (Rect region) + { + Driver.SetAttribute (ColorScheme.Focus); + var f = Frame; + + for (int y = 0; y < f.Width; y++) { + Move (0, y); + for (int x = 0; x < f.Height; x++) { + Rune r; + switch (x % 3) { + case 0: + Driver.AddRune (y.ToString ().ToCharArray (0, 1) [0]); + if (y > 9) + Driver.AddRune (y.ToString ().ToCharArray (1, 1) [0]); + r = '.'; + break; + case 1: + r = 'o'; + break; + default: + r = 'O'; + break; + } + Driver.AddRune (r); + } + } + } + } + + static void ShowTextAlignments () + { +<<<<<<< HEAD + var container = new Window ($"Show Text Alignments") { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + container.OnKeyUp += (KeyEvent ke) => { + if (ke.Key == Key.Esc) + container.Running = false; + }; +======= + var container = new Dialog ( + "Text Alignments", 70, 20, + new Button ("Ok", is_default: true) { Clicked = () => { Application.RequestStop (); } }, + new Button ("Cancel") { Clicked = () => { Application.RequestStop (); } }); + +>>>>>>> cb40c5c2491a559658481d20dd4b6a3343c0183f + + int i = 0; + string txt = "Hello world, how are you doing today?"; + container.Add ( +<<<<<<< HEAD + new Label ($"{i+1}-{txt}") { TextAlignment = TextAlignment.Left, Y = 3, Width = Dim.Fill () }, + new Label ($"{i+2}-{txt}") { TextAlignment = TextAlignment.Right, Y = 5, Width = Dim.Fill () }, + new Label ($"{i+3}-{txt}") { TextAlignment = TextAlignment.Centered, Y = 7, Width = Dim.Fill () }, + new Label ($"{i+4}-{txt}") { TextAlignment = TextAlignment.Justified, Y = 9, Width = Dim.Fill () } +======= + new Label (new Rect (0, 1, 50, 3), $"{i+1}-{txt}") { TextAlignment = TextAlignment.Left }, + new Label (new Rect (0, 3, 50, 3), $"{i+2}-{txt}") { TextAlignment = TextAlignment.Right }, + new Label (new Rect (0, 5, 50, 3), $"{i+3}-{txt}") { TextAlignment = TextAlignment.Centered }, + new Label (new Rect (0, 7, 50, 3), $"{i+4}-{txt}") { TextAlignment = TextAlignment.Justified } +>>>>>>> cb40c5c2491a559658481d20dd4b6a3343c0183f + ); + + Application.Run (container); + } + + static void ShowEntries (View container) + { + var scrollView = new ScrollView (new Rect (50, 10, 20, 8)) { + ContentSize = new Size (20, 50), + //ContentOffset = new Point (0, 0), + ShowVerticalScrollIndicator = true, + ShowHorizontalScrollIndicator = true + }; +#if false + scrollView.Add (new Box10x (0, 0)); +#else + scrollView.Add (new Filler (new Rect (0, 0, 40, 40))); +#endif + + // This is just to debug the visuals of the scrollview when small + var scrollView2 = new ScrollView (new Rect (72, 10, 3, 3)) { + ContentSize = new Size (100, 100), + ShowVerticalScrollIndicator = true, + ShowHorizontalScrollIndicator = true + }; + scrollView2.Add (new Box10x (0, 0)); + var progress = new ProgressBar (new Rect (68, 1, 10, 1)); + bool timer (MainLoop caller) + { + progress.Pulse (); + return true; + } + + Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (300), timer); + + + // A little convoluted, this is because I am using this to test the + // layout based on referencing elements of another view: + + var login = new Label ("Login: ") { X = 3, Y = 6 }; + var password = new Label ("Password: ") { + X = Pos.Left (login), + Y = Pos.Bottom (login) + 1 + }; + var loginText = new TextField ("") { + X = Pos.Right (password), + Y = Pos.Top (login), + Width = 40 + }; + + var passText = new TextField ("") { + Secret = true, + X = Pos.Left (loginText), + Y = Pos.Top (password), + Width = Dim.Width (loginText) + }; + + var tf = new Button (3, 19, "Ok"); + // Add some content + container.Add ( + login, + loginText, + password, + passText, + new FrameView (new Rect (3, 10, 25, 6), "Options"){ + new CheckBox (1, 0, "Remember me"), + new RadioGroup (1, 2, new [] { "_Personal", "_Company" }), + }, + new ListView (new Rect (59, 6, 16, 4), new string [] { + "First row", + "<>", + "This is a very long row that should overflow what is shown", + "4th", + "There is an empty slot on the second row", + "Whoa", + "This is so cool" + }), + scrollView, + scrollView2, + tf, + new Button (10, 19, "Cancel"), + new TimeField (3, 20, DateTime.Now), + new TimeField (23, 20, DateTime.Now, true), + new DateField (3, 22, DateTime.Now), + new DateField (23, 22, DateTime.Now, true), + progress, + new Label (3, 24, "Press F9 (on Unix, ESC+9 is an alias) to activate the menubar"), + menuKeysStyle, + menuAutoMouseNav + + ); + container.SendSubviewToBack (tf); + } + + public static Label ml2; + + static void NewFile () + { + var d = new Dialog ( + "New File", 50, 20, + new Button ("Ok", is_default: true) { Clicked = () => { Application.RequestStop (); } }, + new Button ("Cancel") { Clicked = () => { Application.RequestStop (); } }); + ml2 = new Label (1, 1, "Mouse Debug Line"); + d.Add (ml2); + Application.Run (d); + } + + // + // Creates a nested editor + static void Editor (Toplevel top) + { + var tframe = top.Frame; + var ntop = new Toplevel (tframe); + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_Close", "", () => {Application.RequestStop ();}), + }), + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Copy", "", null), + new MenuItem ("C_ut", "", null), + new MenuItem ("_Paste", "", null) + }), + }); + ntop.Add (menu); + + string fname = null; + foreach (var s in new [] { "/etc/passwd", "c:\\windows\\win.ini" }) + if (System.IO.File.Exists (s)) { + fname = s; + break; + } + + var win = new Window (fname ?? "Untitled") { + X = 0, + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + ntop.Add (win); + + var text = new TextView (new Rect (0, 0, tframe.Width - 2, tframe.Height - 3)); + + if (fname != null) + text.Text = System.IO.File.ReadAllText (fname); + win.Add (text); + + Application.Run (ntop); + } + + static bool Quit () + { + var n = MessageBox.Query (50, 7, "Quit Demo", "Are you sure you want to quit this demo?", "Yes", "No"); + return n == 0; + } + + static void Close () + { + MessageBox.ErrorQuery (50, 7, "Error", "There is nothing to close", "Ok"); + } + + // Watch what happens when I try to introduce a newline after the first open brace + // it introduces a new brace instead, and does not indent. Then watch me fight + // the editor as more oddities happen. + + public static void Open () + { + var d = new OpenDialog ("Open", "Open a file") { AllowsMultipleSelection = true }; + Application.Run (d); + + if (!d.Canceled) + MessageBox.Query (50, 7, "Selected File", string.Join (", ", d.FilePaths), "Ok"); + } + + public static void ShowHex (Toplevel top) + { + var tframe = top.Frame; + var ntop = new Toplevel (tframe); + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_Close", "", () => {Application.RequestStop ();}), + }), + }); + ntop.Add (menu); + + var win = new Window ("/etc/passwd") { + X = 0, + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + ntop.Add (win); + + var source = System.IO.File.OpenRead ("/etc/passwd"); + var hex = new HexView (source) { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + win.Add (hex); + Application.Run (ntop); + + } + + public class MenuItemDetails : MenuItem { + ustring title; + string help; + Action action; + + public MenuItemDetails (ustring title, string help, Action action) : base (title, help, action) + { + this.title = title; + this.help = help; + this.action = action; + } + + public static MenuItemDetails Instance (MenuItem mi) + { + return (MenuItemDetails)mi.GetMenuItem (); + } + } + + public delegate MenuItem MenuItemDelegate (MenuItemDetails menuItem); + + public static void ShowMenuItem (MenuItem mi) + { + BindingFlags flags = BindingFlags.Public | BindingFlags.Static; + MethodInfo minfo = typeof (MenuItemDetails).GetMethod ("Instance", flags); + MenuItemDelegate mid = (MenuItemDelegate)Delegate.CreateDelegate (typeof (MenuItemDelegate), minfo); + MessageBox.Query (70, 7, mi.Title.ToString (), + $"{mi.Title.ToString ()} selected. Is from submenu: {mi.GetMenuBarItem ()}", "Ok"); + } + + static void MenuKeysStyle_Toggled (object sender, EventArgs e) + { + menu.UseKeysUpDownAsKeysLeftRight = menuKeysStyle.Checked; + } + + static void MenuAutoMouseNav_Toggled (object sender, EventArgs e) + { + menu.WantMousePositionReports = menuAutoMouseNav.Checked; + } + + + static void Copy () + { + TextField textField = menu.LastFocused as TextField; + if (textField != null && textField.SelectedLength != 0) { + textField.Copy (); + } + } + + static void Cut () + { + TextField textField = menu.LastFocused as TextField; + if (textField != null && textField.SelectedLength != 0) { + textField.Cut (); + } + } + + static void Paste () + { + TextField textField = menu.LastFocused as TextField; + if (textField != null) { + textField.Paste (); + } + } + + static void Help () + { + MessageBox.Query (50, 7, "Help", "This is a small help\nBe kind.", "Ok"); + } + + #region Selection Demo + + static void ListSelectionDemo (bool multiple) + { + var d = new Dialog ("Selection Demo", 60, 20, + new Button ("Ok", is_default: true) { Clicked = () => { Application.RequestStop (); } }, + new Button ("Cancel") { Clicked = () => { Application.RequestStop (); } }); + + var animals = new List () { "Alpaca", "Llama", "Lion", "Shark", "Goat" }; + var msg = new Label ("Use space bar or control-t to toggle selection") { + X = 1, + Y = 1, + Width = Dim.Fill () - 1, + Height = 1 + }; + + var list = new ListView (animals) { + X = 1, + Y = 3, + Width = Dim.Fill () - 4, + Height = Dim.Fill () - 4, + AllowsMarking = true, + AllowsMultipleSelection = multiple + }; + d.Add (msg, list); + Application.Run (d); + + var result = ""; + for (int i = 0; i < animals.Count; i++) { + if (list.Source.IsMarked (i)) { + result += animals [i] + " "; + } + } + MessageBox.Query (60, 10, "Selected Animals", result == "" ? "No animals selected" : result, "Ok"); + } + #endregion + + + #region OnKeyDown / OnKeyUp Demo + private static void OnKeyDownUpDemo () + { + var container = new Dialog ( + "OnKeyDown & OnKeyUp demo", 80, 20, + new Button ("Close") { Clicked = () => { Application.RequestStop (); } }) { + Width = Dim.Fill (), + Height = Dim.Fill (), + }; + + var list = new List (); + var listView = new ListView (list) { + X = 0, + Y = 0, + Width = Dim.Fill () - 1, + Height = Dim.Fill () - 2, + }; + listView.ColorScheme = Colors.TopLevel; + container.Add (listView); + + void KeyUpDown (KeyEvent keyEvent, string updown) + { + if ((keyEvent.Key & Key.CtrlMask) != 0) { + list.Add ($"Key{updown,-4}: Ctrl "); + } else if ((keyEvent.Key & Key.AltMask) != 0) { + list.Add ($"Key{updown,-4}: Alt "); + } else { + list.Add ($"Key{updown,-4}: {(((uint)keyEvent.KeyValue & (uint)Key.CharMask) > 26 ? $"{(char)keyEvent.KeyValue}" : $"{keyEvent.Key}")}"); + } + listView.MoveDown (); + } + + container.OnKeyDown += (KeyEvent keyEvent) => KeyUpDown (keyEvent, "Down"); + container.OnKeyUp += (KeyEvent keyEvent) => KeyUpDown (keyEvent, "Up"); + Application.Run (container); + } + #endregion + + public static Label ml; + public static MenuBar menu; + public static CheckBox menuKeysStyle; + public static CheckBox menuAutoMouseNav; + static void Main () + { + if (Debugger.IsAttached) + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); + + //Application.UseSystemConsole = true; + + Application.Init (); + + var top = Application.Top; + + //Open (); +#if true + int margin = 3; + var win = new Window ("Hello") { + X = 1, + Y = 1, + + Width = Dim.Fill () - margin, + Height = Dim.Fill () - margin + }; +#else + var tframe = top.Frame; + + var win = new Window (new Rect (0, 1, tframe.Width, tframe.Height - 1), "Hello"); +#endif + MenuItemDetails [] menuItems = { + new MenuItemDetails ("F_ind", "", null), + new MenuItemDetails ("_Replace", "", null), + new MenuItemDetails ("_Item1", "", null), + new MenuItemDetails ("_Not From Sub Menu", "", null) + }; + + menuItems [0].Action = () => ShowMenuItem (menuItems [0]); + menuItems [1].Action = () => ShowMenuItem (menuItems [1]); + menuItems [2].Action = () => ShowMenuItem (menuItems [2]); + menuItems [3].Action = () => ShowMenuItem (menuItems [3]); + + menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("Text _Editor Demo", "", () => { Editor (top); }), + new MenuItem ("_New", "Creates new file", NewFile), + new MenuItem ("_Open", "", Open), + new MenuItem ("_Hex", "", () => ShowHex (top)), + new MenuItem ("_Close", "", () => Close ()), + new MenuItem ("_Disabled", "", () => { }, () => false), + null, + new MenuItem ("_Quit", "", () => { if (Quit ()) top.Running = false; }) + }), + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Copy", "", Copy), + new MenuItem ("C_ut", "", Cut), + new MenuItem ("_Paste", "", Paste), + new MenuItem ("_Find and Replace", + new MenuBarItem (new MenuItem[] {menuItems [0], menuItems [1] })), + menuItems[3] + }), + new MenuBarItem ("_List Demos", new MenuItem [] { + new MenuItem ("Select _Multiple Items", "", () => ListSelectionDemo (true)), + new MenuItem ("Select _Single Item", "", () => ListSelectionDemo (false)), + }), + new MenuBarItem ("A_ssorted", new MenuItem [] { + new MenuItem ("_Show text alignments", "", () => ShowTextAlignments ()), + new MenuItem ("_OnKeyDown/Up", "", () => OnKeyDownUpDemo ()) + }), + new MenuBarItem ("_Test Menu and SubMenus", new MenuItem [] { + new MenuItem ("SubMenu1Item_1", + new MenuBarItem (new MenuItem[] { + new MenuItem ("SubMenu2Item_1", + new MenuBarItem (new MenuItem [] { + new MenuItem ("SubMenu3Item_1", + new MenuBarItem (new MenuItem [] { menuItems [2] }) + ) + }) + ) + }) + ) + }), + new MenuBarItem ("_About...", "Demonstrates top-level menu item", () => MessageBox.ErrorQuery (50, 7, "About Demo", "This is a demo app for gui.cs", "Ok")), + }); + + menuKeysStyle = new CheckBox (3, 25, "UseKeysUpDownAsKeysLeftRight", true); + menuKeysStyle.Toggled += MenuKeysStyle_Toggled; + menuAutoMouseNav = new CheckBox (40, 25, "UseMenuAutoNavigation", true); + menuAutoMouseNav.Toggled += MenuAutoMouseNav_Toggled; + + ShowEntries (win); + + int count = 0; + ml = new Label (new Rect (3, 17, 47, 1), "Mouse: "); + Application.RootMouseEvent += delegate (MouseEvent me) { + ml.TextColor = Colors.TopLevel.Normal; + ml.Text = $"Mouse: ({me.X},{me.Y}) - {me.Flags} {count++}"; + }; + + var test = new Label (3, 18, "Se iniciará el análisis"); + win.Add (test); + win.Add (ml); + + var drag = new Label ("Drag: ") { X = 70, Y = 24 }; + var dragText = new TextField ("") { + X = Pos.Right (drag), + Y = Pos.Top (drag), + Width = 40 + }; + + var statusBar = new StatusBar (new StatusItem [] { + new StatusItem(Key.F1, "~F1~ Help", () => Help()), + new StatusItem(Key.F2, "~F2~ Load", null), + new StatusItem(Key.F3, "~F3~ Save", null), + new StatusItem(Key.ControlX, "~^X~ Quit", () => { if (Quit ()) top.Running = false; }), + }) { + Parent = null, + }; + + win.Add (drag, dragText); +#if true + // FIXED: This currently causes a stack overflow, because it is referencing a window that has not had its size allocated yet + + var bottom = new Label ("This should go on the bottom of the same top-level!"); + win.Add (bottom); + var bottom2 = new Label ("This should go on the bottom of another top-level!"); + top.Add (bottom2); + + Application.OnLoad = () => { + bottom.X = win.X; + bottom.Y = Pos.Bottom (win) - Pos.Top (win) - margin; + bottom2.X = Pos.Left (win); + bottom2.Y = Pos.Bottom (win); + }; +#endif + + + top.Add (win); + //top.Add (menu); + top.Add (menu, statusBar); + Application.Run (); + } +} diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index 08fa9a53a..4bed3fbf6 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -1066,7 +1066,7 @@ namespace Terminal.Gui { } /// - /// Invoked when a character key is pressed and occurs after the key down event. + /// Invoked when a character key is pressed and occurs after the key up event. /// public Action OnKeyPress; @@ -1083,7 +1083,6 @@ namespace Terminal.Gui { /// public override bool ProcessHotKey (KeyEvent keyEvent) { - OnKeyPress?.Invoke (keyEvent); if (subviews == null || subviews.Count == 0) return false; foreach (var view in subviews) @@ -1095,7 +1094,6 @@ namespace Terminal.Gui { /// public override bool ProcessColdKey (KeyEvent keyEvent) { - OnKeyPress?.Invoke (keyEvent); if (subviews == null || subviews.Count == 0) return false; foreach (var view in subviews) diff --git a/Terminal.Gui/Drivers/CursesDriver.cs b/Terminal.Gui/Drivers/CursesDriver.cs index 0edc7e23c..95a658108 100644 --- a/Terminal.Gui/Drivers/CursesDriver.cs +++ b/Terminal.Gui/Drivers/CursesDriver.cs @@ -52,7 +52,7 @@ namespace Terminal.Gui { if (sync) Application.Driver.Refresh (); ccol++; - var runeWidth = Rune.ColumnWidth(rune); + var runeWidth = Rune.ColumnWidth (rune); if (runeWidth > 1) { for (int i = 1; i < runeWidth; i++) { ccol++; @@ -192,7 +192,7 @@ namespace Terminal.Gui { }; } - void ProcessInput (Action keyHandler, Action mouseHandler) + void ProcessInput (Action keyHandler, Action keyUpHandler, Action mouseHandler) { int wch; var code = Curses.get_wch (out wch); @@ -212,6 +212,7 @@ namespace Terminal.Gui { return; } keyHandler (new KeyEvent (MapCursesKey (wch))); + keyUpHandler (new KeyEvent (MapCursesKey (wch))); return; } @@ -219,7 +220,7 @@ namespace Terminal.Gui { if (wch == 27) { Curses.timeout (200); - code = Curses.get_wch (out wch); + code = Curses.get_wch (out int wch2); if (code == Curses.KEY_CODE_YES) keyHandler (new KeyEvent (Key.AltMask | MapCursesKey (wch))); if (code == 0) { @@ -227,23 +228,28 @@ namespace Terminal.Gui { // The ESC-number handling, debatable. // Simulates the AltMask itself by pressing Alt + Space. - if (wch == (int)Key.Space) + if (wch2 == (int)Key.Space) key = new KeyEvent (Key.AltMask); - else if (wch - (int)Key.Space >= 'A' && wch - (int)Key.Space <= 'Z') - key = new KeyEvent ((Key)((uint)Key.AltMask + (wch - (int)Key.Space))); - else if (wch >= '1' && wch <= '9') - key = new KeyEvent ((Key)((int)Key.F1 + (wch - '0' - 1))); - else if (wch == '0') + else if (wch2 - (int)Key.Space >= 'A' && wch2 - (int)Key.Space <= 'Z') + key = new KeyEvent ((Key)((uint)Key.AltMask + (wch2 - (int)Key.Space))); + else if (wch2 >= '1' && wch <= '9') + key = new KeyEvent ((Key)((int)Key.F1 + (wch2 - '0' - 1))); + else if (wch2 == '0') key = new KeyEvent (Key.F10); - else if (wch == 27) - key = new KeyEvent ((Key)wch); + else if (wch2 == 27) + key = new KeyEvent ((Key)wch2); else - key = new KeyEvent (Key.AltMask | (Key)wch); + key = new KeyEvent (Key.AltMask | (Key)wch2); keyHandler (key); - } else + } else { keyHandler (new KeyEvent (Key.Esc)); - } else + } + } else { keyHandler (new KeyEvent ((Key)wch)); + } + // Cause OnKeyUp and OnKeyPressed. Note that the special handling for ESC above + // will not impact KeyUp. + keyUpHandler (new KeyEvent ((Key)wch)); } public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) @@ -252,7 +258,7 @@ namespace Terminal.Gui { Curses.timeout (-1); (mainLoop.Driver as Mono.Terminal.UnixMainLoop).AddWatch (0, Mono.Terminal.UnixMainLoop.Condition.PollIn, x => { - ProcessInput (keyHandler, mouseHandler); + ProcessInput (keyHandler, keyUpHandler, mouseHandler); return true; }); @@ -314,7 +320,7 @@ namespace Terminal.Gui { Colors.Menu.Focus = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLACK); Colors.Menu.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN); Colors.Menu.Normal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_CYAN); - Colors.Menu.Disabled = MakeColor(Curses.COLOR_WHITE, Curses.COLOR_CYAN); + Colors.Menu.Disabled = MakeColor (Curses.COLOR_WHITE, Curses.COLOR_CYAN); Colors.Dialog.Normal = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE); Colors.Dialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN); diff --git a/Terminal.Gui/Drivers/NetDriver.cs b/Terminal.Gui/Drivers/NetDriver.cs index 99cef7ac5..3b7fa188d 100644 --- a/Terminal.Gui/Drivers/NetDriver.cs +++ b/Terminal.Gui/Drivers/NetDriver.cs @@ -328,6 +328,7 @@ namespace Terminal.Gui { if (map == (Key)0xffffffff) return; keyHandler (new KeyEvent (map)); + keyUpHandler (new KeyEvent (map)); }; } diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index 8de136777..343182dc0 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -440,15 +440,10 @@ namespace Terminal.Gui { public WindowsDriver () { - Colors.TopLevel = new ColorScheme (); - - Colors.TopLevel.Normal = MakeColor (ConsoleColor.Green, ConsoleColor.Black); - Colors.TopLevel.Focus = MakeColor (ConsoleColor.White, ConsoleColor.DarkCyan); - Colors.TopLevel.HotNormal = MakeColor (ConsoleColor.DarkYellow, ConsoleColor.Black); - Colors.TopLevel.HotFocus = MakeColor (ConsoleColor.DarkYellow, ConsoleColor.DarkCyan); - winConsole = new WindowsConsole (); + SetupColorsAndBorders (); + cols = Console.WindowWidth; rows = Console.WindowHeight; WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); @@ -459,6 +454,54 @@ namespace Terminal.Gui { Task.Run ((Action)WindowsInputHandler); } + private void SetupColorsAndBorders () + { + Colors.TopLevel = new ColorScheme (); + Colors.Base = new ColorScheme (); + Colors.Dialog = new ColorScheme (); + Colors.Menu = new ColorScheme (); + Colors.Error = new ColorScheme (); + + Colors.TopLevel.Normal = MakeColor (ConsoleColor.Green, ConsoleColor.Black); + Colors.TopLevel.Focus = MakeColor (ConsoleColor.White, ConsoleColor.DarkCyan); + Colors.TopLevel.HotNormal = MakeColor (ConsoleColor.DarkYellow, ConsoleColor.Black); + Colors.TopLevel.HotFocus = MakeColor (ConsoleColor.DarkBlue, ConsoleColor.DarkCyan); + + Colors.Base.Normal = MakeColor (ConsoleColor.White, ConsoleColor.DarkBlue); + Colors.Base.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Base.HotNormal = MakeColor (ConsoleColor.DarkCyan, ConsoleColor.DarkBlue); + Colors.Base.HotFocus = MakeColor (ConsoleColor.Blue, ConsoleColor.Gray); + + Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.DarkGray); + Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); + Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.DarkGray); + Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); + Colors.Menu.Disabled = MakeColor (ConsoleColor.Gray, ConsoleColor.DarkGray); + + Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.DarkGray); + Colors.Dialog.HotNormal = MakeColor (ConsoleColor.DarkBlue, ConsoleColor.Gray); + Colors.Dialog.HotFocus = MakeColor (ConsoleColor.DarkBlue, ConsoleColor.DarkGray); + + Colors.Error.Normal = MakeColor (ConsoleColor.DarkRed, ConsoleColor.White); + Colors.Error.Focus = MakeColor (ConsoleColor.White, ConsoleColor.DarkRed); + Colors.Error.HotNormal = MakeColor (ConsoleColor.Black, ConsoleColor.White); + Colors.Error.HotFocus = MakeColor (ConsoleColor.Black, ConsoleColor.DarkRed); + + HLine = '\u2500'; + VLine = '\u2502'; + Stipple = '\u2592'; + Diamond = '\u25c6'; + ULCorner = '\u250C'; + LLCorner = '\u2514'; + URCorner = '\u2510'; + LRCorner = '\u2518'; + LeftTee = '\u251c'; + RightTee = '\u2524'; + TopTee = '\u22a4'; + BottomTee = '\u22a5'; + } + [StructLayout (LayoutKind.Sequential)] public struct ConsoleKeyInfoEx { public ConsoleKeyInfo consoleKeyInfo; @@ -564,11 +607,24 @@ namespace Terminal.Gui { case WindowsConsole.EventType.Key: var map = MapKey (ToConsoleKeyInfoEx (inputEvent.KeyEvent)); if (map == (Key)0xffffffff) { - KeyEvent key = default; + KeyEvent key = new KeyEvent (); + // Shift = VK_SHIFT = 0x10 // Ctrl = VK_CONTROL = 0x11 // Alt = VK_MENU = 0x12 + if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.CapslockOn)) { + inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.CapslockOn; + } + + if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ScrolllockOn)) { + inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.ScrolllockOn; + } + + if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.NumlockOn)) { + inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.NumlockOn; + } + switch (inputEvent.KeyEvent.dwControlKeyState) { case WindowsConsole.ControlKeyState.RightAltPressed: case WindowsConsole.ControlKeyState.RightAltPressed | @@ -617,10 +673,10 @@ namespace Terminal.Gui { keyUpHandler (key); } else { if (inputEvent.KeyEvent.bKeyDown) { - // Key Down - Fire KeyDown Event and KeyStroke (ProcessKey) Event keyDownHandler (new KeyEvent (map)); - keyHandler (new KeyEvent (map)); } else { + // Key Up - Fire KeyDown Event and KeyStroke (ProcessKey) Event + keyHandler (new KeyEvent (map)); keyUpHandler (new KeyEvent (map)); } } @@ -918,6 +974,9 @@ namespace Terminal.Gui { case ConsoleKey.OemComma: case ConsoleKey.OemPlus: case ConsoleKey.OemMinus: + if (keyInfo.KeyChar == 0) + return Key.Unknown; + return (Key)((uint)keyInfo.KeyChar); } @@ -971,48 +1030,10 @@ namespace Terminal.Gui { public override void Init (Action terminalResized) { TerminalResized = terminalResized; - - Colors.Base = new ColorScheme (); - Colors.Dialog = new ColorScheme (); - Colors.Menu = new ColorScheme (); - Colors.Error = new ColorScheme (); - - HLine = '\u2500'; - VLine = '\u2502'; - Stipple = '\u2592'; - Diamond = '\u25c6'; - ULCorner = '\u250C'; - LLCorner = '\u2514'; - URCorner = '\u2510'; - LRCorner = '\u2518'; - LeftTee = '\u251c'; - RightTee = '\u2524'; - TopTee = '\u22a4'; - BottomTee = '\u22a5'; - - Colors.Base.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Blue); - Colors.Base.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); - Colors.Base.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Blue); - Colors.Base.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); - - Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Cyan); - Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); - Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); - Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); - Colors.Menu.Disabled = MakeColor (ConsoleColor.DarkGray, ConsoleColor.Cyan); - - Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); - Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); - Colors.Dialog.HotNormal = MakeColor (ConsoleColor.Blue, ConsoleColor.Gray); - Colors.Dialog.HotFocus = MakeColor (ConsoleColor.Blue, ConsoleColor.Cyan); - - Colors.Error.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Red); - Colors.Error.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); - Colors.Error.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Red); - Colors.Error.HotFocus = Colors.Error.HotNormal; - Console.Clear (); + SetupColorsAndBorders (); } + void ResizeScreen () { OutputBuffer = new WindowsConsole.CharInfo [Rows * Cols]; diff --git a/Terminal.Gui/Event.cs b/Terminal.Gui/Event.cs index c4e9b9e1a..169b60084 100644 --- a/Terminal.Gui/Event.cs +++ b/Terminal.Gui/Event.cs @@ -45,7 +45,7 @@ namespace Terminal.Gui { ControlSpace = 0, /// - /// The key code for the user pressing Control-A + /// The key code for the user pressing Control-A /// ControlA = 1, /// @@ -288,8 +288,7 @@ namespace Terminal.Gui { /// /// Describes a keyboard event. /// - public struct KeyEvent { - + public class KeyEvent { /// /// Symb olid definition for the key. /// @@ -321,6 +320,10 @@ namespace Terminal.Gui { //public bool IsCtrl => ((uint)Key >= 1) && ((uint)Key <= 26); public bool IsCtrl => (Key & Key.CtrlMask) != 0; + public KeyEvent () + { + Key = Key.Unknown; + } /// /// Constructs a new KeyEvent from the provided Key value - can be a rune cast into a Key value /// @@ -328,6 +331,28 @@ namespace Terminal.Gui { { Key = k; } + + public override string ToString () + { + string msg = ""; + var key = this.Key; + if ((this.Key & Key.ShiftMask) != 0) { + msg += "Shift-"; + } + if ((this.Key & Key.CtrlMask) != 0) { + msg += "Ctrl-"; + } + if ((this.Key & Key.AltMask) != 0) { + msg += "Alt-"; + } + + if (string.IsNullOrEmpty (msg)) { + msg += $"{(((uint)this.KeyValue & (uint)Key.CharMask) > 27 ? $"{(char)this.KeyValue}" : $"{key}")}"; + } else { + msg += $"{(((uint)this.KeyValue & (uint)Key.CharMask) > 27 ? $"{(char)this.KeyValue}" : $"")}"; + } + return msg; + } } /// @@ -486,7 +511,7 @@ namespace Terminal.Gui { /// Returns a that represents the current . /// /// A that represents the current . - public override string ToString() + public override string ToString () { return $"({X},{Y}:{Flags}"; } diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 29808e781..b265fccda 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -124,7 +124,7 @@ namespace Terminal.Gui { for (int i = 0; i < spaces; i++) s.Append (' '); if (extras > 0) { - s.Append ('_'); + //s.Append ('_'); extras--; } } @@ -179,8 +179,11 @@ namespace Terminal.Gui { int x; switch (textAlignment) { case TextAlignment.Left: + x = Frame.Left; + break; case TextAlignment.Justified: - x = 0; + Recalc (); + x = Frame.Left; break; case TextAlignment.Right: x = Frame.Right - str.Length; diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 9cb5bfe16..309c408e0 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -294,6 +294,11 @@ namespace Terminal.Gui { /// public event Action SelectedChanged; + /// + /// This event is raised on Enter key or Double Click to open the selected item. + /// + public event EventHandler OpenSelectedItem; + /// /// Handles cursor movement for this view, passes all other events. /// @@ -325,6 +330,11 @@ namespace Terminal.Gui { return true; else break; + + case Key.Enter: + OpenSelectedItem?.Invoke (this, new EventArgs ()); + break; + } return base.ProcessKey (kb); } @@ -451,7 +461,7 @@ namespace Terminal.Gui { /// public override bool MouseEvent(MouseEvent me) { - if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) return false; if (!HasFocus) @@ -469,9 +479,10 @@ namespace Terminal.Gui { SetNeedsDisplay (); return true; } - if (SelectedChanged != null) - SelectedChanged(); + SelectedChanged?.Invoke (); SetNeedsDisplay (); + if (me.Flags == MouseFlags.Button1DoubleClicked) + OpenSelectedItem?.Invoke (this, new EventArgs ()); return true; } } diff --git a/Terminal.sln b/Terminal.sln index 5bace6618..77680ffd0 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui", "Terminal.Gu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Designer", "Designer\Designer.csproj", "{1228D992-C801-49BB-839A-7BD28A3FFF0A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UICatalog", "UICatalog\UICatalog.csproj", "{88979F89-9A42-448F-AE3E-3060145F6375}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x86 = Debug|x86 @@ -25,6 +27,14 @@ Global {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Debug|x86.Build.0 = Debug|x86 {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Release|x86.ActiveCfg = Release|x86 {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Release|x86.Build.0 = Release|x86 + {88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88979F89-9A42-448F-AE3E-3060145F6375}.Debug|x86.ActiveCfg = Debug|Any CPU + {88979F89-9A42-448F-AE3E-3060145F6375}.Debug|x86.Build.0 = Debug|Any CPU + {88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.Build.0 = Release|Any CPU + {88979F89-9A42-448F-AE3E-3060145F6375}.Release|x86.ActiveCfg = Release|Any CPU + {88979F89-9A42-448F-AE3E-3060145F6375}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 diff --git a/UICatalog/.editorconfig b/UICatalog/.editorconfig new file mode 100644 index 000000000..040e7abd9 --- /dev/null +++ b/UICatalog/.editorconfig @@ -0,0 +1,23 @@ +[*.cs] +indent_style = tab +indent_size = 8 +tab_width = 8 +csharp_new_line_before_open_brace = methods,local_functions +csharp_new_line_before_else = false +csharp_new_line_before_catch = false +csharp_new_line_before_finally = false +end_of_line = crlf + +csharp_indent_case_contents = true +csharp_indent_switch_labels = false +csharp_indent_labels = flush_left +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_preserve_single_line_blocks = true +dotnet_style_require_accessibility_modifiers = never +csharp_style_var_when_type_is_apparent = true +csharp_prefer_braces = false +csharp_space_before_open_square_brackets = true +csharp_space_between_method_call_name_and_opening_parenthesis = true +csharp_space_between_method_declaration_name_and_open_parenthesis = true \ No newline at end of file diff --git a/UICatalog/.gitignore b/UICatalog/.gitignore new file mode 100644 index 000000000..4378419e7 --- /dev/null +++ b/UICatalog/.gitignore @@ -0,0 +1,9 @@ +############### +# folder # +############### +/**/DROP/ +/**/TEMP/ +/**/packages/ +/**/bin/ +/**/obj/ +_site diff --git a/UICatalog/Program.cs b/UICatalog/Program.cs new file mode 100644 index 000000000..14a7956db --- /dev/null +++ b/UICatalog/Program.cs @@ -0,0 +1,271 @@ +using NStack; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Terminal.Gui; + +namespace UICatalog { + /// + /// Main program for the Terminal.gui UI Catalog app. This app provides a chooser that allows + /// for a calalog of UI demos, examples, and tests. + /// + class Program { + private static Toplevel _top; + private static MenuBar _menu; + private static int _nameColumnWidth; + private static Window _leftPane; + private static List _categories; + private static ListView _categoryListView; + private static Window _rightPane; + private static List _scenarios; + private static ListView _scenarioListView; + private static StatusBar _statusBar; + + private static Scenario _runningScenario = null; + + static void Main (string [] args) + { + if (Debugger.IsAttached) + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); + + _scenarios = Scenario.GetDerivedClassesCollection ().ToList (); + + if (args.Length > 0) { + var item = _scenarios.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals (args [0], StringComparison.OrdinalIgnoreCase)); + _runningScenario = (Scenario)Activator.CreateInstance (_scenarios [item]); + Application.Init (); + _runningScenario.Init (Application.Top); + _runningScenario.Setup (); + _runningScenario.Run (); + _runningScenario = null; + return; + } + + Scenario scenario = GetScenarioToRun (); + while (scenario != null) { + Application.Init (); + scenario.Init (Application.Top); + scenario.Setup (); + scenario.Run (); + scenario = GetScenarioToRun (); + } + } + + /// + /// Create all controls. This gets called once and the controls remain with their state between Sceanrio runs. + /// + private static void Setup () + { + _menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_Quit", "", () => Application.RequestStop() ) + }), + new MenuBarItem ("_About...", "About this app", () => MessageBox.Query (0, 6, "About UI Catalog", "UI Catalog is a comprehensive sample library for Terminal.Gui", "Ok")), + }); + + _leftPane = new Window ("Categories") { + X = 0, + Y = 1, // for menu + Width = 25, + Height = Dim.Fill (), + CanFocus = false, + }; + + + _categories = Scenario.GetAllCategories (); + _categoryListView = new ListView (_categories) { + X = 1, + Y = 0, + Width = Dim.Fill (0), + Height = Dim.Fill (2), + AllowsMarking = false, + CanFocus = true, + }; + _categoryListView.OpenSelectedItem += (o, a) => { + _top.SetFocus (_rightPane); + }; + _categoryListView.SelectedChanged += CategoryListView_SelectedChanged; + _leftPane.Add (_categoryListView); + + _rightPane = new Window ("Scenarios") { + X = 25, + Y = 1, // for menu + Width = Dim.Fill (), + Height = Dim.Fill (), + CanFocus = false, + + }; + + _nameColumnWidth = Scenario.ScenarioMetadata.GetName (_scenarios.OrderByDescending (t => Scenario.ScenarioMetadata.GetName (t).Length).FirstOrDefault ()).Length; + + _scenarioListView = new ListView () { + X = 0, + Y = 0, + Width = Dim.Fill (0), + Height = Dim.Fill (0), + AllowsMarking = false, + CanFocus = true, + }; + + //_scenarioListView.OnKeyPress += (KeyEvent ke) => { + // if (_top.MostFocused == _scenarioListView && ke.Key == Key.Enter) { + // _scenarioListView_OpenSelectedItem (null, null); + // } + //}; + + _scenarioListView.OpenSelectedItem += _scenarioListView_OpenSelectedItem; + _rightPane.Add (_scenarioListView); + + _categoryListView.SelectedItem = 0; + CategoryListView_SelectedChanged (); + + _statusBar = new StatusBar (new StatusItem [] { + //new StatusItem(Key.F1, "~F1~ Help", () => Help()), + new StatusItem(Key.ControlQ, "~CTRL-Q~ Quit", () => { + if (_runningScenario is null){ + // This causes GetScenarioToRun to return null + _runningScenario = null; + Application.RequestStop(); + } else { + _runningScenario.RequestStop(); + } + }), + }); + } + + /// + /// This shows the selection UI. Each time it is run, it calls Application.Init to reset everything. + /// + /// + private static Scenario GetScenarioToRun () + { + Application.Init (); + + if (_menu == null) { + Setup (); + } + + _top = Application.Top; + _top.OnKeyUp += KeyUpHandler; + _top.Add (_menu); + _top.Add (_leftPane); + _top.Add (_rightPane); + _top.Add (_statusBar); + + // HACK: There is no other way to SetFocus before Application.Run. See Issue #445 +#if false + if (_runningScenario != null) + Application.Iteration += Application_Iteration; +#else + _top.Ready += (o, a) => { + if (_runningScenario != null) { + _top.SetFocus (_rightPane); + _runningScenario = null; + } + }; +#endif + + Application.Run (_top); + return _runningScenario; + } + +#if false + private static void Application_Iteration (object sender, EventArgs e) + { + Application.Iteration -= Application_Iteration; + _top.SetFocus (_rightPane); + } +#endif + private static void _scenarioListView_OpenSelectedItem (object sender, EventArgs e) + { + if (_runningScenario is null) { + var source = _scenarioListView.Source as ScenarioListDataSource; + _runningScenario = (Scenario)Activator.CreateInstance (source.Scenarios [_scenarioListView.SelectedItem]); + Application.RequestStop (); + } + } + + internal class ScenarioListDataSource : IListDataSource { + public List Scenarios { get; set; } + + public bool IsMarked (int item) => false;// Scenarios [item].IsMarked; + + public int Count => Scenarios.Count; + + public ScenarioListDataSource (List itemList) => Scenarios = itemList; + + public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width) + { + container.Move (col, line); + // Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible + var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item])); + RenderUstr (driver, $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width); + } + + public void SetMark (int item, bool value) + { + } + + // A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461 + private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width) + { + int used = 0; + int index = 0; + while (index < ustr.Length) { + (var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length); + var count = Rune.ColumnWidth (rune); + if (used + count >= width) break; + driver.AddRune (rune); + used += count; + index += size; + } + + while (used < width) { + driver.AddRune (' '); + used++; + } + } + } + + /// + /// When Scenarios are running we need to override the behavior of the Menu + /// and Statusbar to enable Scenarios that use those (or related key input) + /// to not be impacted. Same as for tabs. + /// + /// + private static void KeyUpHandler (KeyEvent ke) + { + if (_runningScenario != null) { + //switch (ke.Key) { + //case Key.Esc: + // //_runningScenario.RequestStop (); + // break; + //case Key.Enter: + // break; + //} + } else if (ke.Key == Key.Tab || ke.Key == Key.BackTab) { + // BUGBUG: Work around Issue #434 by implementing our own TAB navigation + if (_top.MostFocused == _categoryListView) + _top.SetFocus (_rightPane); + else + _top.SetFocus (_leftPane); + } + } + + private static void CategoryListView_SelectedChanged () + { + var item = _categories [_categoryListView.SelectedItem]; + List newlist; + if (item.Equals ("All")) { + newlist = _scenarios; + + } else { + newlist = _scenarios.Where (t => Scenario.ScenarioCategory.GetCategories (t).Contains (item)).ToList (); + } + _scenarioListView.Source = new ScenarioListDataSource (newlist); + _scenarioListView.SelectedItem = 0; + } + } +} diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json new file mode 100644 index 000000000..1d6b7542f --- /dev/null +++ b/UICatalog/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "UICatalog": { + "commandName": "Project", + "commandLineArgs": "HexEditor" + } + } +} \ No newline at end of file diff --git a/UICatalog/README.md b/UICatalog/README.md new file mode 100644 index 000000000..6f6b019f8 --- /dev/null +++ b/UICatalog/README.md @@ -0,0 +1,122 @@ +# Terminal.Gui UI Catalog + +UI Catalog is a comprehensive sample library for Terminal.Gui. It attempts to satisfy the following goals: + +1. Be an easy to use showcase for Terminal.Gui concepts and features. +2. Provide sample code that illustrates how to properly implement said concepts & features. +3. Make it easy for contributors to add additional samples in a structured way. + +![screenshot](screenshot.png) + +## Motivation + +The original `demo.cs` sample app for Terminal.Gui is neither good to showcase, nor does it explain different concepts. In addition, because it is built on a single source file, it has proven to cause friction when multiple contributors are simultaneously working on different aspects of Terminal.Gui. See [Issue #368](https://github.com/migueldeicaza/Terminal.Gui/issues/368) for more background. + +## How To Use + +`Program.cs` is the main app and provides a UI for selecting and running **Scenarios**. Each **Scenario* is implemented as a class derived from `Scenario` and `Program.cs` uses reflection to dynamically build the UI. + +**Scenarios** are tagged with categories using the `[ScenarioCategory]` attribute. The left pane of the main screen lists the categories. Clicking on a category shows all the scenarios in that category. + +**Scenarios** can be run either from the **UICatalog.exe** app UI or by being specified on the command line: + +``` +UICatalog.exe +``` + +e.g. + +``` +UICatalog.exe Buttons +``` + +When a **Scenario** is run, it runs as though it were a standalone `Terminal.Gui` app. However, scaffolding is provided (in the `Scenario` base class) that (optionally) takes care of `Terminal.Gui` initialization. + +## Contributing by Adding Scenarios + +To add a new **Scenario** simply: + +1. Create a new `.cs` file in the `Scenarios` directory that derives from `Scenario`. +2. Add a `[ScenarioMetaData]` attribute to the class specifying the scenario's name and description. +3. Add one or more `[ScenarioCategory]` attributes to the class specifying which categories the sceanrio belongs to. If you don't specify a category the sceanrio will show up in "All". +4. Implement the `Setup` override which will be called when a user selects the scenario to run. +5. Optionally, implement the `Init` and/or `Run` overrides to provide a custom implementation. + +The sample below is provided in the `Scenarios` directory as a generic sample that can be copied and re-named: + +```csharp +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "Generic", Description: "Generic sample - A template for creating new Scenarios")] + [ScenarioCategory ("Controls")] + class MyScenario : Scenario { + public override void Setup () + { + // Put your scenario code here, e.g. + Win.Add (new Button ("Press me!") { + X = Pos.Center (), + Y = Pos.Center (), + Clicked = () => MessageBox.Query (20, 7, "Hi", "Neat?", "Yes", "No") + }); + } + } +} +``` + +`Scenario` provides a `Toplevel` and `Window` the provides a canvas for the Scenario to operate. The default `Window` shows the Scenario name and supports exiting the Scenario through the `Esc` key. + +![screenshot](generic_screenshot.png) + +To build a more advanced scenario, where control of the `Toplevel` and `Window` is needed (e.g. for scenarios using `MenuBar` or `StatusBar`), simply set the `Top` and `Window` properties as appropriate, as seen in the `UnicodeInMenu` scenario: + +```csharp +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "Unicode In Menu", Description: "Unicode menus per PR #204")] + [ScenarioCategory ("Text")] + [ScenarioCategory ("Controls")] + class UnicodeInMenu : Scenario { + public override void Setup () + { + Top = new Toplevel (new Rect (0, 0, Application.Driver.Cols, Application.Driver.Rows)); + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_Файл", new MenuItem [] { + new MenuItem ("_Создать", "Creates new file", null), + new MenuItem ("_Открыть", "", null), + new MenuItem ("Со_хранить", "", null), + new MenuItem ("_Выход", "", () => Application.RequestStop() ) + }), + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Copy", "", null), + new MenuItem ("C_ut", "", null), + new MenuItem ("_Paste", "", null) + }) + }); + Top.Add (menu); + + Win = new Window ($"Scenario: {GetName ()}") { + X = 0, + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + Top.Add (Win); + } + } +} +``` + +For complete control, the `Init` and `Run` overrides can be implemented. The `base.Init` assigns `Application.Top` to `Top` and creates `Win`. The `base.Run` simply calls `Application.Run(Top)`. + +## Contribution Guidelines + +- Provide a terse, descriptive name for `Scenarios`. Keep them short; the `ListView` that displays them dynamically sizes the column width and long names will make it hard for people to use. +- Provide a clear description. +- Comment `Scenario` code to describe to others why it's a useful `Scenario`. +- Annotate `Scenarios` with `[ScenarioCategory]` attributes. Try to minimize the number of new categories created. +- Use the `Bug Rero` Category for `Scnarios` that reproduce bugs. + - Include the Github Issue # in the Description. + - Once the bug has been fixed in `master` submit another PR to remove the `Scenario` (or modify it to provide a good regression test). +- Tag bugs or suggestions for `UI Catalog` in the main `Terminal.Gui` Github Issues with "UICatalog: ". \ No newline at end of file diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs new file mode 100644 index 000000000..e491a4c8e --- /dev/null +++ b/UICatalog/Scenario.cs @@ -0,0 +1,181 @@ +using NStack; +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace UICatalog { + /// + /// Base class for each demo/scenario. To define a new sceanrio simply + /// + /// 1) declare a class derived from Scenario, + /// 2) Set Name and Description as appropriate using [ScenarioMetadata] attribute + /// 3) Set one or more categories with the [ScenarioCategory] attribute + /// 4) Implement Setup. + /// 5) Optionally, implement Run. + /// + /// The Main program uses reflection to find all sceanarios and adds them to the + /// ListViews. Press ENTER to run the selected sceanrio. Press CTRL-Q to exit it. + /// + public class Scenario { + /// + /// The Top level for the Scenario. This should be set to `Application.Top` in most cases. + /// + public Toplevel Top { get; set; } + + /// + /// + public Window Win { get; set; } + + /// + /// Helper that provides the default Window implementation with a frame and + /// label showing the name of the Scenario and logic to exit back to + /// the Scenario picker UI. + /// Override Init to provide any `Toplevel` behavior needed. + /// + /// + public virtual void Init(Toplevel top) + { + Top = top; + Win = new Window ($"CTRL-Q to Close - Scenario: {GetName ()}") { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + Top.Add (Win); + } + + [System.AttributeUsage (System.AttributeTargets.Class)] + public class ScenarioMetadata : System.Attribute { + /// + /// Scenario Name + /// + public string Name { get; set; } + + /// + /// Scenario Description + /// + public string Description { get; set; } + + public ScenarioMetadata (string Name, string Description) + { + this.Name = Name; + this.Description = Description; + } + + /// + /// Static helper function to get the Scenario Name given a Type + /// + /// + /// + public static string GetName (Type t) => ((ScenarioMetadata)System.Attribute.GetCustomAttributes (t) [0]).Name; + + /// + /// Static helper function to get the Scenario Description given a Type + /// + /// + /// + public static string GetDescription (Type t) => ((ScenarioMetadata)System.Attribute.GetCustomAttributes (t) [0]).Description; + } + + /// + /// Helper to get the Scenario Name + /// + /// + public string GetName () => ScenarioMetadata.GetName (this.GetType ()); + + /// + /// Helper to get the Scenario Descripiton + /// + /// + public string GetDescription () => ScenarioMetadata.GetDescription (this.GetType ()); + + [System.AttributeUsage (System.AttributeTargets.Class, AllowMultiple = true)] + public class ScenarioCategory : System.Attribute { + /// + /// Category Name + /// + public string Name { get; set; } + + public ScenarioCategory (string Name) => this.Name = Name; + + /// + /// Static helper function to get the Scenario Name given a Type + /// + /// + /// + public static string GetName (Type t) => ((ScenarioCategory)System.Attribute.GetCustomAttributes (t) [0]).Name; + + /// + /// Static helper function to get the Scenario Categories given a Type + /// + /// + /// + public static List GetCategories (Type t) => System.Attribute.GetCustomAttributes (t) + .ToList () + .Where (a => a is ScenarioCategory) + .Select (a => ((ScenarioCategory)a).Name) + .ToList (); + } + + /// + /// Helper function to get the Categories of a Scenario + /// + /// + public List GetCategories () => ScenarioCategory.GetCategories (this.GetType ()); + + public override string ToString () => $"{GetName (),-30}{GetDescription ()}"; + + /// + /// Override this to implement the Scenario setup logic (create controls, etc...). + /// + public virtual void Setup () + { + } + + /// + /// Runs the scenario. Override to start the scearnio using a Top level different than `Top`. + /// + public virtual void Run () + { + Application.Run (Top); + } + + /// + /// Stops the scenario. Override to implement shutdown behavior for the Scenario. + /// + public virtual void RequestStop () + { + Application.RequestStop (); + } + + /// + /// Returns a list of all Categories set by all of the scenarios defined in the project. + /// + internal static List GetAllCategories () + { + List categories = new List () { "All" }; + foreach (Type type in typeof (Scenario).Assembly.GetTypes () + .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf (typeof (Scenario)))) { + List attrs = System.Attribute.GetCustomAttributes (type).ToList (); + categories = categories.Union (attrs.Where (a => a is ScenarioCategory).Select (a => ((ScenarioCategory)a).Name)).ToList (); + } + return categories; + } + + /// + /// Returns an instance of each Scenario defined in the project. + /// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class + /// + internal static List GetDerivedClassesCollection () + { + List objects = new List (); + foreach (Type type in typeof (Scenario).Assembly.GetTypes () + .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf (typeof (Scenario)))) { + objects.Add (type); + } + return objects; + } + } +} diff --git a/UICatalog/Scenarios/Buttons.cs b/UICatalog/Scenarios/Buttons.cs new file mode 100644 index 000000000..64b92bfce --- /dev/null +++ b/UICatalog/Scenarios/Buttons.cs @@ -0,0 +1,105 @@ +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "Buttons", Description: "Demonstrates all sorts of Buttons")] + [ScenarioCategory ("Controls")] + [ScenarioCategory ("Layout")] + class Buttons : Scenario { + public override void Setup () + { + // Add a label & text field so we can demo IsDefault + var editLabel = new Label ("TextField (to demo IsDefault):") { + X = 0, + Y = 0, + }; + Win.Add (editLabel); + var edit = new TextField ("") { + X = Pos.Right (editLabel) + 1, + Y = Pos.Top (editLabel), + Width = Dim.Fill (2), + }; + Win.Add (edit); + + // This is the default button (IsDefault = true); if user presses ENTER in the TextField + // the scenario will quit + var defaultButton = new Button ("Quit") { + X = Pos.Center (), + // BUGBUG: Throws an exception + //Y= Pos.Bottom(Win), + Y = 20, + IsDefault = true, + Clicked = () => Application.RequestStop (), + }; + Win.Add (defaultButton); + + var y = 2; + var button = new Button (10, y, "Base Color") { + ColorScheme = Colors.Base, + Clicked = () => MessageBox.Query (30, 7, "Message", "Question?", "Yes", "No") + }; + Win.Add (button); + + y += 2; + Win.Add (new Button (10, y, "Error Color") { + ColorScheme = Colors.Error, + Clicked = () => MessageBox.Query (30, 7, "Message", "Question?", "Yes", "No") + }); + + y += 2; + Win.Add (new Button (10, y, "Dialog Color") { + ColorScheme = Colors.Dialog, + Clicked = () => MessageBox.Query (30, 7, "Message", "Question?", "Yes", "No") + }); + + y += 2; + Win.Add (new Button (10, y, "Menu Color") { + ColorScheme = Colors.Menu, + Clicked = () => MessageBox.Query (30, 7, "Message", "Question?", "Yes", "No") + }); + + y += 2; + Win.Add (new Button (10, y, "TopLevel Color") { + ColorScheme = Colors.TopLevel, + Clicked = () => MessageBox.Query (30, 7, "Message", "Question?", "Yes", "No") + }); + + y += 2; + Win.Add (new Button (10, y, "A super long button that will probably expose a bug in clipping or wrapping of text. Will it?") { + Clicked = () => MessageBox.Query (30, 7, "Message", "Question?", "Yes", "No") + }); + + y += 2; + // Note the 'N' in 'Newline' will be the hotkey + Win.Add (new Button (10, y, "a Newline\nin the button") { + Clicked = () => MessageBox.Query (30, 7, "Message", "Question?", "Yes", "No") + }); + + y += 2; + // BUGBUG: Buttons don't support specifying hotkeys with _?!? + Win.Add (button = new Button (10, y, "Te_xt Changer") { + }); + button.Clicked = () => button.Text += $"{y++}"; + + Win.Add (new Button ("Lets see if this will move as \"Text Changer\" grows") { + X = Pos.Right(button) + 10, + Y = y, + }); + + y += 2; + Win.Add (new Button (10, y, "Delete") { + ColorScheme = Colors.Error, + Clicked = () => Win.Remove (button) + }); + + y += 2; + Win.Add (new Button (10, y, "Change Default") { + Clicked = () => { + defaultButton.IsDefault = !defaultButton.IsDefault; + button.IsDefault = !button.IsDefault; + }, + }); + + + } + } +} diff --git a/UICatalog/Scenarios/DimAndPosLayout.cs b/UICatalog/Scenarios/DimAndPosLayout.cs new file mode 100644 index 000000000..f1f543a57 --- /dev/null +++ b/UICatalog/Scenarios/DimAndPosLayout.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Terminal.Gui; + +namespace UICatalog { + /// + /// This Scenario demonstrates how to use Termina.gui's Dim and Pos Layout System. + /// [x] - Using Dim.Fill to fill a window + /// [x] - Using Dim.Fill and Dim.Pos to automatically align controls based on an initial control + /// [ ] - ... + /// + [ScenarioMetadata (Name: "DimAndPosLayout", Description: "Demonstrates using the Dim and Pos Layout System")] + [ScenarioCategory ("Layout")] + class DimAndPosLayout : Scenario { + + public override void Setup () + { + Top.LayoutStyle = LayoutStyle.Computed; + // Demonstrate using Dim to create a ruler that always measures the top-level window's width + // BUGBUG: Dim.Fill returns too big a value sometimes. + //const string rule = "|123456789"; + //var labelRuler = new Label ("ruler") { + // X = 0, + // Y = 0, + // Width = Dim.Fill (1), // BUGBUG: I don't think this should be needed; DimFill() should respect container's frame. X does. + // ColorScheme = Colors.Error + //}; + + //Application.OnResized += () => { + // labelRuler.Text = rule.Repeat ((int)Math.Ceiling((double)(labelRuler.Bounds.Width) / (double)rule.Length))[0..(labelRuler.Bounds.Width)]; + //}; + + //win.Add (labelRuler); + + // Demonstrate using Dim to create a window that fills the parent with a margin + int margin = 20; + var subWin = new Window ($"Sub Windoww with {margin} character margin") { + X = margin, + Y = 2, + Width = Dim.Fill (margin), + Height = Dim.Fill () + }; + Win.Add (subWin); + + int i = 1; + string txt = "Hello world, how are you doing today"; + var labelList = new List