diff --git a/.editorconfig b/.editorconfig index 3ad2ee46d..576839082 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,3 +17,6 @@ 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/.gitignore b/.gitignore index f10938059..c4865c0ac 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ obj *.userprefs *~ packages -.vs \ No newline at end of file +.vs +*.csproj.user diff --git a/Designer/Designer.csproj b/Designer/Designer.csproj index e87bd0a6b..545c2b0b9 100644 --- a/Designer/Designer.csproj +++ b/Designer/Designer.csproj @@ -7,7 +7,7 @@ Exe Designer Designer - v4.6.1 + v4.7.2 @@ -30,13 +30,10 @@ x86 + + ..\packages\NStack.Core.0.14.0\lib\netstandard2.0\NStack.dll + - - ..\packages\NStack.Core.0.11.0\lib\netstandard1.5\NStack.dll - - - ..\packages\NStack.Core.0.11.0\lib\netstandard1.5\NStack.dll - @@ -48,8 +45,7 @@ - - + \ No newline at end of file diff --git a/Designer/Program.cs b/Designer/Program.cs index d3dac91a4..a7d6a17f8 100644 --- a/Designer/Program.cs +++ b/Designer/Program.cs @@ -1,5 +1,6 @@ using System; using Terminal.Gui; +using Attribute = Terminal.Gui.Attribute; namespace Designer { class Surface : Window { @@ -9,12 +10,18 @@ namespace Designer { } class MainClass { + static void Close () + { + MessageBox.ErrorQuery (50, 7, "Error", "There is nothing to close", "Ok"); + } + public static void Main (string [] args) { Application.Init (); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_Close", "", () => Close ()), new MenuItem ("_Quit", "", () => { Application.RequestStop (); }) }), new MenuBarItem ("_Edit", new MenuItem [] { @@ -37,8 +44,27 @@ namespace Designer { Height = Dim.Fill () }; - //Application.Top.Add (menu); - Application.Top.Add (login, password); + var loginText = new TextField("") { + X = Pos.Right(password), + Y = Pos.Top(login), + Width = 40, + ColorScheme = new ColorScheme() { + Focus = Attribute.Make(Color.BrightYellow, Color.DarkGray), + Normal = Attribute.Make(Color.Green, Color.BrightYellow), + HotFocus = Attribute.Make(Color.BrightBlue, Color.Brown), + HotNormal = Attribute.Make(Color.Red, Color.BrightRed), + }, + }; + + var passText = new TextField ("") { + Secret = true, + X = Pos.Left (loginText), + Y = Pos.Top (password), + Width = Dim.Width (loginText) + }; + + surface.Add (login, password, loginText, passText); + Application.Top.Add (menu, surface); Application.Run (); } } diff --git a/Designer/app.config b/Designer/app.config deleted file mode 100644 index 3dbff35f4..000000000 --- a/Designer/app.config +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Designer/packages.config b/Designer/packages.config index e18eee05a..40bce897a 100644 --- a/Designer/packages.config +++ b/Designer/packages.config @@ -1,4 +1,4 @@ - + - - + + \ No newline at end of file diff --git a/Example/Example.csproj b/Example/Example.csproj index 3f6523667..dff1d63ad 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -1,4 +1,4 @@ - + Debug @@ -7,7 +7,10 @@ Exe Terminal Terminal - v4.6.1 + v4.7.2 + + + true @@ -30,14 +33,10 @@ x86 + + ..\packages\NStack.Core.0.14.0\lib\netstandard2.0\NStack.dll + - - ..\packages\NStack.Core.0.11.0\lib\netstandard1.5\NStack.dll - False - - - ..\packages\NStack.Core.0.11.0\lib\netstandard1.5\NStack.dll - @@ -52,4 +51,4 @@ - + \ No newline at end of file diff --git a/Example/demo.cs b/Example/demo.cs index 3cdf6a6f4..4d1a3a265 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -1,28 +1,49 @@ using Terminal.Gui; using System; using Mono.Terminal; -using System.Collections; using System.Collections.Generic; - +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using NStack; static class Demo { + //class Box10x : View, IScrollView { class Box10x : View { - public Box10x (int x, int y) : base (new Rect (x, y, 10, 10)) + 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 < 10; y++) { + for (int y = 0; y < h; y++) { Move (0, y); - for (int x = 0; x < 10; x++) { - - Driver.AddRune ((Rune)('0' + (x + y) % 10)); + 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); } } @@ -42,6 +63,9 @@ static class Demo { 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: @@ -60,24 +84,30 @@ static class Demo { static void ShowTextAlignments (View container) { + int i = 0; + string txt = "Hello world, how are you doing today"; container.Add ( - new Label (new Rect (0, 0, 40, 3), "1-Hello world, how are you doing today") { TextAlignment = TextAlignment.Left }, - new Label (new Rect (0, 4, 40, 3), "2-Hello world, how are you doing today") { TextAlignment = TextAlignment.Right }, - new Label (new Rect (0, 8, 40, 3), "3-Hello world, how are you doing today") { TextAlignment = TextAlignment.Centered }, - new Label (new Rect (0, 12, 40, 3), "4-Hello world, how are you doing today") { TextAlignment = TextAlignment.Justified }); + new FrameView (new Rect (75, 3, txt.Length + 6, 20), "Text Alignments") { + new Label(new Rect(0, 1, 40, 3), $"{i+1}-{txt}") { TextAlignment = TextAlignment.Left }, + new Label(new Rect(0, 5, 40, 3), $"{i+2}-{txt}") { TextAlignment = TextAlignment.Right }, + new Label(new Rect(0, 9, 40, 3), $"{i+3}-{txt}") { TextAlignment = TextAlignment.Centered }, + new Label(new Rect(0, 13, 40, 3), $"{i+4}-{txt}") { TextAlignment = TextAlignment.Justified } + }); } static void ShowEntries (View container) { var scrollView = new ScrollView (new Rect (50, 10, 20, 8)) { - ContentSize = new Size (100, 100), - ContentOffset = new Point (-1, -1), + ContentSize = new Size (20, 50), + //ContentOffset = new Point (0, 0), ShowVerticalScrollIndicator = true, ShowHorizontalScrollIndicator = true }; - +#if false scrollView.Add (new Box10x (0, 0)); - //scrollView.Add (new Filler (new Rect (0, 0, 40, 40))); +#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)) { @@ -93,7 +123,7 @@ static class Demo { return true; } - //Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (300), timer); + Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (300), timer); // A little convoluted, this is because I am using this to test the @@ -117,6 +147,7 @@ static class Demo { Width = Dim.Width (loginText) }; + var tf = new Button (3, 19, "Ok"); // Add some content container.Add ( login, @@ -127,7 +158,7 @@ static class Demo { new CheckBox (1, 0, "Remember me"), new RadioGroup (1, 2, new [] { "_Personal", "_Company" }), }, - new ListView (new Rect (60, 6, 16, 4), new string [] { + new ListView (new Rect (59, 6, 16, 4), new string [] { "First row", "<>", "This is a very long row that should overflow what is shown", @@ -137,16 +168,20 @@ static class Demo { "This is so cool" }), scrollView, - //scrollView2, - new Button (3, 19, "Ok"), + 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, 22, "Press F9 (on Unix, ESC+9 is an alias) to activate the menubar") + 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; @@ -162,12 +197,13 @@ static class Demo { Application.Run (d); } - // + // // Creates a nested editor - static void Editor(Toplevel top) { + static void Editor (Toplevel top) + { var tframe = top.Frame; - var ntop = new Toplevel(tframe); - var menu = new MenuBar(new MenuBarItem[] { + var ntop = new Toplevel (tframe); + var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_Close", "", () => {Application.RequestStop ();}), }), @@ -177,25 +213,25 @@ static class Demo { new MenuItem ("_Paste", "", null) }), }); - ntop.Add(menu); + ntop.Add (menu); string fname = null; - foreach (var s in new[] { "/etc/passwd", "c:\\windows\\win.ini" }) - if (System.IO.File.Exists(s)) { + 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") { + var win = new Window (fname ?? "Untitled") { X = 0, Y = 1, - Width = Dim.Fill(), - Height = Dim.Fill() + Width = Dim.Fill (), + Height = Dim.Fill () }; - ntop.Add(win); + ntop.Add (win); + + var text = new TextView (new Rect (0, 0, tframe.Width - 2, tframe.Height - 3)); - 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); @@ -211,7 +247,7 @@ static class Demo { static void Close () { - MessageBox.ErrorQuery (50, 5, "Error", "There is nothing to close", "Ok"); + 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 @@ -220,10 +256,11 @@ static class Demo { public static void Open () { - var d = new OpenDialog ("Open", "Open a file"); + var d = new OpenDialog ("Open", "Open a file") { AllowsMultipleSelection = true }; Application.Run (d); - MessageBox.Query (50, 7, "Selected File", string.Join (", ", d.FilePaths), "Ok"); + if (!d.Canceled) + MessageBox.Query (50, 7, "Selected File", string.Join (", ", d.FilePaths), "Ok"); } public static void ShowHex (Toplevel top) @@ -254,7 +291,76 @@ static class Demo { }; 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 @@ -278,7 +384,8 @@ static class Demo { Y = 3, Width = Dim.Fill () - 4, Height = Dim.Fill () - 4, - AllowsMarking = true + AllowsMarking = true, + AllowsMultipleSelection = false }; d.Add (msg, list); Application.Run (d); @@ -295,60 +402,120 @@ static class Demo { 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; - - var tframe = top.Frame; + //Open (); #if true var win = new Window ("Hello") { X = 0, - Y = 1, + Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; #else + var tframe = top.Frame; + var win = new Window (new Rect (0, 1, tframe.Width, tframe.Height - 1), "Hello"); #endif - var menu = new MenuBar (new MenuBarItem [] { + 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", "", null), - new MenuItem ("C_ut", "", null), - new MenuItem ("_Paste", "", null) + 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 MenuBarItem ("_List Demos", new MenuItem [] { new MenuItem ("Select Items", "", ListSelectionDemo), }), + new MenuBarItem ("Test Menu and SubMenus", new MenuItem [] { + new MenuItem ("SubMenu1Item1", + new MenuBarItem (new MenuItem[] { + new MenuItem ("SubMenu2Item1", + new MenuBarItem (new MenuItem [] { + new MenuItem ("SubMenu3Item1", + new MenuBarItem (new MenuItem [] { menuItems [2] }) + ) + }) + ) + }) + ) + }), }); + 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: "); + ml = new Label (new Rect (3, 18, 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); - - // ShowTextAlignments (win); + + ShowTextAlignments (win); + + 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", () => Quit()), + }); + + win.Add (drag, dragText); + top.Add (win); - top.Add (menu); + //top.Add (menu); + top.Add (menu, statusBar, ml); Application.Run (); } -} \ No newline at end of file +} diff --git a/Example/packages.config b/Example/packages.config index e18eee05a..40bce897a 100644 --- a/Example/packages.config +++ b/Example/packages.config @@ -1,4 +1,4 @@ - + - - + + \ No newline at end of file diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index bc222f3a2..e89130556 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -134,8 +134,8 @@ namespace Terminal.Gui { /// /// Determines the LayoutStyle for a view, if Absolute, during LayoutSubviews, the - /// value from the Frame will be used, if the value is Computer, then the Frame - /// will be updated from the X, Y Pos objets and the Width and Heigh Dim objects. + /// value from the Frame will be used, if the value is Computer, then the Frame + /// will be updated from the X, Y Pos objects and the Width and Height Dim objects. /// public enum LayoutStyle { /// @@ -229,6 +229,16 @@ namespace Terminal.Gui { View focused = null; Direction focusDirection; + /// + /// Event fired when the view get focus. + /// + public event EventHandler OnEnter; + + /// + /// Event fired when the view lost focus. + /// + public event EventHandler OnLeave; + internal Direction FocusDirection { get => SuperView?.FocusDirection ?? focusDirection; set { @@ -430,7 +440,7 @@ namespace Terminal.Gui { SetNeedsDisplay (Bounds); } - bool layoutNeeded = true; + internal bool layoutNeeded = true; internal void SetNeedsLayout () { @@ -439,7 +449,7 @@ namespace Terminal.Gui { layoutNeeded = true; if (SuperView == null) return; - SuperView.layoutNeeded = true; + SuperView.SetNeedsLayout (); } /// @@ -649,14 +659,14 @@ namespace Terminal.Gui { } /// - /// Clears the specfied rectangular region with the current color + /// Clears the specified rectangular region with the current color /// public void Clear (Rect r) { var h = r.Height; var w = r.Width; - for (int line = 0; line < h; line++) { - Move (0, line); + for (int line = r.Y; line < r.Y + h; line++) { + Driver.Move (r.X, line); for (int col = 0; col < w; col++) Driver.AddRune (' '); } @@ -826,11 +836,16 @@ namespace Terminal.Gui { } internal set { if (base.HasFocus != value) + if (value == true) + OnEnter?.Invoke (this, new EventArgs ()); + else + OnLeave?.Invoke (this, new EventArgs ()); SetNeedsDisplay (); base.HasFocus = value; // Remove focus down the chain of subviews if focus is removed if (value == false && focused != null) { + OnLeave?.Invoke (focused, new EventArgs ()); focused.HasFocus = false; focused = null; } @@ -919,7 +934,9 @@ namespace Terminal.Gui { if (!view.NeedDisplay.IsEmpty || view.childNeedsDisplay) { if (view.Frame.IntersectsWith (clipRect) && view.Frame.IntersectsWith (region)) { - // TODO: optimize this by computing the intersection of region and view.Bounds + // FIXED: optimize this by computing the intersection of region and view.Bounds + if (view.layoutNeeded) + view.LayoutSubviews (); view.Redraw (view.Bounds); } view.NeedDisplay = Rect.Empty; @@ -1310,7 +1327,7 @@ namespace Terminal.Gui { /// Frame. public Toplevel (Rect frame) : base (frame) { - ColorScheme = Colors.Base; + Initialize (); } /// @@ -1318,11 +1335,16 @@ namespace Terminal.Gui { /// public Toplevel () : base () { - ColorScheme = Colors.Base; + Initialize (); Width = Dim.Fill (); Height = Dim.Fill (); } + void Initialize () + { + ColorScheme = Colors.Base; + } + /// /// Convenience factory method that creates a new toplevel with the current terminal dimensions. /// @@ -1343,6 +1365,16 @@ namespace Terminal.Gui { /// public bool Modal { get; set; } + /// + /// Check id current toplevel has menu bar + /// + public bool HasMenuBar { get; set; } + + /// + /// Check id current toplevel has status bar + /// + public bool HasStatusBar { get; set; } + public override bool ProcessKey (KeyEvent keyEvent) { if (base.ProcessKey (keyEvent)) @@ -1392,6 +1424,103 @@ namespace Terminal.Gui { return false; } + public override void Add (View view) + { + if (this == Application.Top) { + if (view is MenuBar) + HasMenuBar = true; + if (view is StatusBar) + HasStatusBar = true; + } + base.Add (view); + } + + public override void Remove (View view) + { + if (this == Application.Top) { + if (view is MenuBar) + HasMenuBar = true; + if (view is StatusBar) + HasStatusBar = true; + } + base.Remove (view); + } + + public override void RemoveAll () + { + if (this == Application.Top) { + HasMenuBar = false; + HasStatusBar = false; + } + base.RemoveAll (); + } + + internal void EnsureVisibleBounds (Toplevel top, int x, int y, out int nx, out int ny) + { + nx = Math.Max (x, 0); + nx = nx + top.Frame.Width > Driver.Cols ? Math.Max(Driver.Cols - top.Frame.Width, 0) : nx; + bool m, s; + if (SuperView == null) + m = Application.Top.HasMenuBar; + else + m = ((Toplevel)SuperView).HasMenuBar; + int l = m ? 1 : 0; + ny = Math.Max (y, l); + if (SuperView == null) + s = Application.Top.HasStatusBar; + else + s = ((Toplevel)SuperView).HasStatusBar; + l = s ? Driver.Rows - 1 : Driver.Rows; + ny = Math.Min (ny, l); + ny = ny + top.Frame.Height > l ? Math.Max(l - top.Frame.Height, m ? 1 : 0) : ny; + } + + internal void PositionToplevels () + { + if (this != Application.Top) { + EnsureVisibleBounds (this, Frame.X, Frame.Y, out int nx, out int ny); + if (nx != Frame.X || ny != Frame.Y) { + X = nx; + Y = ny; + } + } else { + foreach (var top in Subviews) { + if (top is Toplevel) { + EnsureVisibleBounds ((Toplevel)top, top.Frame.X, top.Frame.Y, out int nx, out int ny); + if (nx != top.Frame.X || ny != top.Frame.Y) { + top.X = nx; + top.Y = ny; + } + if (HasStatusBar && ny + top.Frame.Height > Driver.Rows - 1) { + if (top.Height is Dim.DimFill) + top.Height = Dim.Fill () - 1; + } + } + } + } + } + + public override void Redraw (Rect region) + { + if (this == Application.Top) { + if (!NeedDisplay.IsEmpty) { + Driver.SetAttribute (Colors.TopLevel.Normal); + Clear (region); + Driver.SetAttribute (Colors.Base.Normal); + } + foreach (var view in Subviews) { + if (view.Frame.IntersectsWith (region)) { + //view.SetNeedsLayout (); + view.SetNeedsDisplay (view.Bounds); + } + } + + ClearNeedsDisplay (); + } + + base.Redraw (base.Bounds); + } + /// /// This method is invoked by Application.Begin as part of the Application.Run after /// the views have been laid out, and before the views are drawn for the first time. @@ -1572,48 +1701,48 @@ namespace Terminal.Gui { ClearNeedsDisplay (); } -#if true - // - // It does not look like the event is raised on clicked-drag + // + // FIXED:It does not look like the event is raised on clicked-drag // need to figure that out. // - Point? dragPosition; - public override bool MouseEvent(MouseEvent mouseEvent) + internal static Point? dragPosition; + Point start; + public override bool MouseEvent (MouseEvent mouseEvent) { - // The code is currently disabled, because the - // Driver.UncookMouse does not seem to have an effect if there is + // FIXED:The code is currently disabled, because the + // Driver.UncookMouse does not seem to have an effect if there is // a pending mouse event activated. - if (true) - return false; - - if ((mouseEvent.Flags == MouseFlags.Button1Pressed|| mouseEvent.Flags == MouseFlags.Button4Pressed)){ - + + int nx, ny; + if ((mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) || + mouseEvent.Flags == MouseFlags.Button4Pressed)) { if (dragPosition.HasValue) { - var dx = mouseEvent.X - dragPosition.Value.X; - var dy = mouseEvent.Y - dragPosition.Value.Y; + if (SuperView == null) { + Application.Top.SetNeedsDisplay (Frame); + Application.Top.Redraw (Frame); + } else { + SuperView.SetNeedsDisplay (Frame); + } + EnsureVisibleBounds (this, mouseEvent.X + mouseEvent.OfX - start.X, + mouseEvent.Y + mouseEvent.OfY, out nx, out ny); - var nx = Frame.X + dx; - var ny = Frame.Y + dy; - if (nx < 0) - nx = 0; - if (ny < 0) - ny = 0; - - //Demo.ml2.Text = $"{dx},{dy}"; - dragPosition = new Point (mouseEvent.X, mouseEvent.Y); - - // TODO: optimize, only SetNeedsDisplay on the before/after regions. - if (SuperView == null) - Application.Refresh (); - else - SuperView.SetNeedsDisplay (); + dragPosition = new Point (nx, ny); Frame = new Rect (nx, ny, Frame.Width, Frame.Height); + X = nx; + Y = ny; + //Demo.ml2.Text = $"{dx},{dy}"; + + // FIXED: optimize, only SetNeedsDisplay on the before/after regions. SetNeedsDisplay (); return true; } else { // Only start grabbing if the user clicks on the title bar. if (mouseEvent.Y == 0) { - dragPosition = new Point (mouseEvent.X, mouseEvent.Y); + start = new Point (mouseEvent.X, mouseEvent.Y); + dragPosition = new Point (); + nx = mouseEvent.X - mouseEvent.OfX; + ny = mouseEvent.Y - mouseEvent.OfY; + dragPosition = new Point (nx, ny); Application.GrabMouse (this); } @@ -1622,18 +1751,16 @@ namespace Terminal.Gui { } } - if (mouseEvent.Flags == MouseFlags.Button1Released) { + if (mouseEvent.Flags == MouseFlags.Button1Released && dragPosition.HasValue) { Application.UngrabMouse (); Driver.UncookMouse (); - dragPosition = null; - //Driver.StopReportingMouseMoves (); } //Demo.ml.Text = me.ToString (); return false; } -#endif + } /// @@ -1721,6 +1848,7 @@ namespace Terminal.Gui { d (state); return false; }); + mainLoop.Driver.Wakeup (); } public override void Send (SendOrPostCallback d, object state) @@ -1741,7 +1869,7 @@ namespace Terminal.Gui { /// public static void Init () => Init (() => Toplevel.Create ()); - static bool _initialized = false; + internal static bool _initialized = false; /// /// Initializes the Application @@ -1749,7 +1877,6 @@ namespace Terminal.Gui { static void Init (Func topLevelFactory) { if (_initialized) return; - _initialized = true; var p = Environment.OSVersion.Platform; Mono.Terminal.IMainLoopDriver mainLoopDriver; @@ -1770,6 +1897,7 @@ namespace Terminal.Gui { SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); Top = topLevelFactory (); Current = Top; + _initialized = true; } /// @@ -1867,7 +1995,7 @@ namespace Terminal.Gui { return start; } - static View mouseGrabView; + internal static View mouseGrabView; /// /// Grabs the mouse, forcing all mouse events to be routed to the specified view until UngrabMouse is called. @@ -1898,20 +2026,22 @@ namespace Terminal.Gui { static void ProcessMouseEvent (MouseEvent me) { + var view = FindDeepestView (Current, me.X, me.Y, out int rx, out int ry); RootMouseEvent?.Invoke (me); if (mouseGrabView != null) { var newxy = mouseGrabView.ScreenToView (me.X, me.Y); var nme = new MouseEvent () { X = newxy.X, Y = newxy.Y, - Flags = me.Flags + Flags = me.Flags, + OfX = me.X - newxy.X, + OfY = me.Y - newxy.Y, + View = view }; - mouseGrabView.MouseEvent (me); + mouseGrabView.MouseEvent (nme); return; } - int rx, ry; - var view = FindDeepestView (Current, me.X, me.Y, out rx, out ry); if (view != null) { if (!view.WantMousePositionReports && me.Flags == MouseFlags.ReportMousePosition) return; @@ -1919,7 +2049,10 @@ namespace Terminal.Gui { var nme = new MouseEvent () { X = rx, Y = ry, - Flags = me.Flags + Flags = me.Flags, + OfX = rx, + OfY = ry, + View = view }; // Should we bubbled up the event, if it is not handled? view.MouseEvent (nme); @@ -1969,7 +2102,7 @@ namespace Terminal.Gui { } /// - /// Building block API: completes the exection of a Toplevel that was started with Begin. + /// Building block API: completes the execution of a Toplevel that was started with Begin. /// /// The runstate returned by the method. static public void End (RunState runState) @@ -1980,6 +2113,9 @@ namespace Terminal.Gui { runState.Dispose (); } + /// + /// Finalize the driver. + /// public static void Shutdown () { Driver.End (); @@ -2047,8 +2183,7 @@ namespace Terminal.Gui { for (state.Toplevel.Running = true; state.Toplevel.Running;) { if (MainLoop.EventsPending (wait)) { MainLoop.MainIteration (); - if (Iteration != null) - Iteration (null, EventArgs.Empty); + Iteration?.Invoke (null, EventArgs.Empty); } else if (wait == false) return; if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.childNeedsDisplay) { @@ -2135,6 +2270,7 @@ namespace Terminal.Gui { var full = new Rect (0, 0, Driver.Cols, Driver.Rows); Driver.Clip = full; foreach (var t in toplevels) { + t.PositionToplevels (); t.RelativeLayout (full); t.LayoutSubviews (); } diff --git a/Terminal.Gui/Dialogs/FileDialog.cs b/Terminal.Gui/Dialogs/FileDialog.cs index c00f2b49d..069b8eea4 100644 --- a/Terminal.Gui/Dialogs/FileDialog.cs +++ b/Terminal.Gui/Dialogs/FileDialog.cs @@ -23,11 +23,13 @@ namespace Terminal.Gui { internal bool canChooseFiles = true; internal bool canChooseDirectories = false; internal bool allowsMultipleSelection = false; + FileDialog host; - public DirListView () + public DirListView (FileDialog host) { infos = new List<(string,bool,bool)> (); CanFocus = true; + this.host = host; } bool IsAllowed (FileSystemInfo fsi) @@ -71,6 +73,84 @@ namespace Terminal.Gui { Move (0, selected - top); } + int lastSelected; + bool shiftOnWheel; + public override bool MouseEvent (MouseEvent me) + { + if ((me.Flags & (MouseFlags.Button1Clicked | MouseFlags.Button1DoubleClicked | + MouseFlags.WheeledUp | MouseFlags.WheeledDown)) == 0) + return false; + + if (!HasFocus) + SuperView.SetFocus (this); + + if (infos == null) + return false; + + if (me.Y + top >= infos.Count) + return true; + + int lastSelectedCopy = shiftOnWheel ? lastSelected : selected; + + switch (me.Flags) { + case MouseFlags.Button1Clicked: + SetSelected (me); + SelectionChanged (); + SetNeedsDisplay (); + break; + case MouseFlags.Button1DoubleClicked: + SetSelected (me); + if (ExecuteSelection ()) { + host.canceled = false; + Application.RequestStop (); + } + return true; + case MouseFlags.Button1Clicked | MouseFlags.ButtonShift: + SetSelected (me); + if (shiftOnWheel) + lastSelected = lastSelectedCopy; + shiftOnWheel = false; + PerformMultipleSelection (lastSelected); + return true; + case MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl: + SetSelected (me); + PerformMultipleSelection (); + return true; + case MouseFlags.WheeledUp: + SetSelected (me); + selected = lastSelected; + MoveUp (); + return true; + case MouseFlags.WheeledDown: + SetSelected (me); + selected = lastSelected; + MoveDown (); + return true; + case MouseFlags.WheeledUp | MouseFlags.ButtonShift: + SetSelected (me); + selected = lastSelected; + lastSelected = lastSelectedCopy; + shiftOnWheel = true; + MoveUp (); + return true; + case MouseFlags.WheeledDown | MouseFlags.ButtonShift: + SetSelected (me); + selected = lastSelected; + lastSelected = lastSelectedCopy; + shiftOnWheel = true; + MoveDown (); + return true; + } + + return true; + } + + void SetSelected (MouseEvent me) + { + lastSelected = selected; + selected = top + me.Y; + } + void DrawString (int line, string str) { var f = Frame; @@ -91,7 +171,7 @@ namespace Terminal.Gui { } for (; used < width; used++) { Driver.AddRune (' '); - } + } } public override void Redraw (Rect region) @@ -123,7 +203,7 @@ namespace Terminal.Gui { if (allowsMultipleSelection) Driver.AddRune (fi.Item3 ? '*' : ' '); - + if (fi.Item2) Driver.AddRune ('/'); else @@ -138,35 +218,38 @@ namespace Terminal.Gui { void SelectionChanged () { + if (FilePaths.Count > 0) + FileChanged?.Invoke (string.Join (", ", GetFilesName (FilePaths))); + else + FileChanged?.Invoke (infos [selected].Item2 ? "" : Path.GetFileName (infos [selected].Item1)); if (SelectedChanged != null) { var sel = infos [selected]; SelectedChanged ((sel.Item1, sel.Item2)); } } + List GetFilesName (IReadOnlyList files) + { + List filesName = new List (); + + foreach (var file in files) { + filesName.Add (Path.GetFileName (file)); + } + + return filesName; + } + public override bool ProcessKey (KeyEvent keyEvent) { switch (keyEvent.Key) { case Key.CursorUp: case Key.ControlP: - if (selected > 0) { - selected--; - if (selected < top) - top = selected; - SelectionChanged (); - SetNeedsDisplay (); - } + MoveUp (); return true; case Key.CursorDown: case Key.ControlN: - if (selected + 1 < infos.Count) { - selected++; - if (selected >= top + Frame.Height) - top++; - SelectionChanged (); - SetNeedsDisplay (); - } + MoveDown (); return true; case Key.ControlV: @@ -187,22 +270,10 @@ namespace Terminal.Gui { return true; case Key.Enter: - var isDir = infos [selected].Item2; - - if (isDir) { - Directory = Path.GetFullPath (Path.Combine (Path.GetFullPath (Directory.ToString ()), infos [selected].Item1)); - if (DirectoryChanged != null) - DirectoryChanged (Directory); - } else { - if (FileChanged != null) - FileChanged (infos [selected].Item1); - if (canChooseFiles) { - // Let the OK handler take it over - return false; - } - // No files allowed, do not let the default handler take it. - } - return true; + if (ExecuteSelection ()) + return false; + else + return true; case Key.PageUp: n = (selected - Frame.Height); @@ -217,21 +288,97 @@ namespace Terminal.Gui { return true; case Key.Space: - case Key.ControlT: - if (allowsMultipleSelection) { - if ((canChooseFiles && infos [selected].Item2 == false) || - (canChooseDirectories && infos [selected].Item2 && - infos [selected].Item1 != "..")){ - infos [selected] = (infos [selected].Item1, infos [selected].Item2, !infos [selected].Item3); - SelectionChanged (); - SetNeedsDisplay (); - } - } + case Key.ControlT: + PerformMultipleSelection (); + return true; + + case Key.Home: + MoveFirst (); + return true; + + case Key.End: + MoveLast (); return true; } return base.ProcessKey (keyEvent); } + void MoveLast () + { + selected = infos.Count - 1; + top = infos.Count () - 1; + SelectionChanged (); + SetNeedsDisplay (); + } + + void MoveFirst () + { + selected = 0; + top = 0; + SelectionChanged (); + SetNeedsDisplay (); + } + + void MoveDown () + { + if (selected + 1 < infos.Count) { + selected++; + if (selected >= top + Frame.Height) + top++; + SelectionChanged (); + SetNeedsDisplay (); + } + } + + void MoveUp () + { + if (selected > 0) { + selected--; + if (selected < top) + top = selected; + SelectionChanged (); + SetNeedsDisplay (); + } + } + + internal bool ExecuteSelection () + { + var isDir = infos [selected].Item2; + + if (isDir) { + Directory = Path.GetFullPath (Path.Combine (Path.GetFullPath (Directory.ToString ()), infos [selected].Item1)); + DirectoryChanged?.Invoke (Directory); + } else { + FileChanged?.Invoke (infos [selected].Item1); + if (canChooseFiles) { + // Ensures that at least one file is selected. + if (FilePaths.Count == 0) + PerformMultipleSelection (); + // Let the OK handler take it over + return true; + } + // No files allowed, do not let the default handler take it. + } + return false; + } + + void PerformMultipleSelection (int? firstSelected = null) + { + if (allowsMultipleSelection) { + int first = Math.Min (firstSelected ?? selected, selected); + int last = Math.Max (selected, firstSelected ?? selected); + for (int i = first; i <= last; i++) { + if ((canChooseFiles && infos [i].Item2 == false) || + (canChooseDirectories && infos [i].Item2 && + infos [i].Item1 != "..")) { + infos [i] = (infos [i].Item1, infos [i].Item2, !infos [i].Item3); + } + } + SelectionChanged (); + SetNeedsDisplay (); + } + } + string [] allowedFileTypes; public string [] AllowedFileTypes { get => allowedFileTypes; @@ -278,6 +425,13 @@ namespace Terminal.Gui { TextField dirEntry, nameEntry; internal DirListView dirListView; + /// + /// Constructor for the OpenDialog and the SaveDialog. + /// + /// The title. + /// The prompt. + /// The name field label. + /// The message. public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message) : base (title, Driver.Cols - 20, Driver.Rows - 5, null) { this.message = new Label (Rect.Empty, "MESSAGE" + message); @@ -306,18 +460,16 @@ namespace Terminal.Gui { }; Add (this.nameFieldLabel, nameEntry); - dirListView = new DirListView () { + dirListView = new DirListView (this) { X = 1, Y = 3 + msgLines + 2, - Width = Dim.Fill () - 2, + Width = Dim.Fill () - 3, Height = Dim.Fill () - 2, }; DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory); Add (dirListView); dirListView.DirectoryChanged = (dir) => dirEntry.Text = dir; - dirListView.FileChanged = (file) => { - nameEntry.Text = file; - }; + dirListView.FileChanged = (file) => nameEntry.Text = file; this.cancel = new Button ("Cancel"); this.cancel.Clicked += () => { @@ -330,6 +482,7 @@ namespace Terminal.Gui { IsDefault = true, }; this.prompt.Clicked += () => { + dirListView.ExecuteSelection (); canceled = false; Application.RequestStop (); }; @@ -430,6 +583,11 @@ namespace Terminal.Gui { nameEntry.Text = Path.GetFileName(value.ToString()); } } + + /// + /// Check if the dialog was or not canceled. + /// + public bool Canceled { get => canceled; } } /// @@ -445,6 +603,11 @@ namespace Terminal.Gui { /// /// public class SaveDialog : FileDialog { + /// + /// Constructor of the save dialog. + /// + /// The title. + /// The message. public SaveDialog (ustring title, ustring message) : base (title, prompt: "Save", nameFieldLabel: "Save as:", message: message) { } @@ -482,6 +645,11 @@ namespace Terminal.Gui { /// /// public class OpenDialog : FileDialog { + /// + /// Constructor of the Open Dialog. + /// + /// + /// public OpenDialog (ustring title, ustring message) : base (title, prompt: "Open", nameFieldLabel: "Open", message: message) { } diff --git a/Terminal.Gui/Drivers/ConsoleDriver.cs b/Terminal.Gui/Drivers/ConsoleDriver.cs index 7ab4139f5..19cdc4e48 100644 --- a/Terminal.Gui/Drivers/ConsoleDriver.cs +++ b/Terminal.Gui/Drivers/ConsoleDriver.cs @@ -6,6 +6,7 @@ // using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Mono.Terminal; using NStack; @@ -93,14 +94,32 @@ namespace Terminal.Gui { /// public struct Attribute { internal int value; + internal Color foreground; + internal Color background; /// /// Initializes a new instance of the struct. /// /// Value. - public Attribute (int value) + /// Foreground + /// Background + public Attribute (int value, Color foreground = new Color(), Color background = new Color()) { this.value = value; + this.foreground = foreground; + this.background = background; + } + + /// + /// Initializes a new instance of the struct. + /// + /// Foreground + /// Background + public Attribute (Color foreground = new Color (), Color background = new Color ()) + { + this.value = value = ((int)foreground | (int)background << 4); + this.foreground = foreground; + this.background = background; } /// @@ -137,50 +156,207 @@ namespace Terminal.Gui { /// views contained inside. /// public class ColorScheme { + Attribute _normal; + Attribute _focus; + Attribute _hotNormal; + Attribute _hotFocus; + Attribute _disabled; + internal string caller = ""; + /// /// The default color for text, when the view is not focused. /// - public Attribute Normal; + public Attribute Normal { get { return _normal; } set { _normal = SetAttribute (value); } } + /// /// The color for text when the view has the focus. /// - public Attribute Focus; + public Attribute Focus { get { return _focus; } set { _focus = SetAttribute (value); } } /// /// The color for the hotkey when a view is not focused /// - public Attribute HotNormal; + public Attribute HotNormal { get { return _hotNormal; } set { _hotNormal = SetAttribute (value); } } /// /// The color for the hotkey when the view is focused. /// - public Attribute HotFocus; + public Attribute HotFocus { get { return _hotFocus; } set { _hotFocus = SetAttribute (value); } } + + /// + /// The default color for text, when the view is disabled. + /// + public Attribute Disabled { get { return _disabled; } set { _disabled = SetAttribute (value); } } + + bool preparingScheme = false; + + Attribute SetAttribute (Attribute attribute, [CallerMemberName]string callerMemberName = null) + { + if (!Application._initialized && !preparingScheme) + return attribute; + + if (preparingScheme) + return attribute; + + preparingScheme = true; + switch (caller) { + case "TopLevel": + switch (callerMemberName) { + case "Normal": + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + break; + case "Focus": + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + break; + case "HotFocus": + HotNormal = Application.Driver.MakeAttribute (attribute.foreground, HotNormal.background); + if (Focus.foreground != attribute.background) + Focus = Application.Driver.MakeAttribute (Focus.foreground, attribute.background); + break; + } + break; + + case "Base": + switch (callerMemberName) { + case "Normal": + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + break; + case "Focus": + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + break; + case "HotFocus": + HotNormal = Application.Driver.MakeAttribute (attribute.foreground, HotNormal.background); + if (Focus.foreground != attribute.background) + Focus = Application.Driver.MakeAttribute (Focus.foreground, attribute.background); + break; + } + break; + + case "Menu": + switch (callerMemberName) { + case "Normal": + if (Focus.background != attribute.background) + Focus = Application.Driver.MakeAttribute (attribute.foreground, Focus.background); + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + Disabled = Application.Driver.MakeAttribute (Disabled.foreground, attribute.background); + break; + case "Focus": + Normal = Application.Driver.MakeAttribute (attribute.foreground, Normal.background); + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + if (Focus.background != attribute.background) + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + Disabled = Application.Driver.MakeAttribute (Disabled.foreground, attribute.background); + break; + case "HotFocus": + HotNormal = Application.Driver.MakeAttribute (attribute.foreground, HotNormal.background); + if (Focus.foreground != attribute.background) + Focus = Application.Driver.MakeAttribute (Focus.foreground, attribute.background); + break; + case "Disabled": + if (Focus.background != attribute.background) + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + break; + + } + break; + + case "Dialog": + switch (callerMemberName) { + case "Normal": + if (Focus.background != attribute.background) + Focus = Application.Driver.MakeAttribute (attribute.foreground, Focus.background); + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + break; + case "Focus": + Normal = Application.Driver.MakeAttribute (attribute.foreground, Normal.background); + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + if (Focus.background != attribute.background) + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + if (Normal.foreground != attribute.background) + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + break; + case "HotFocus": + HotNormal = Application.Driver.MakeAttribute (attribute.foreground, HotNormal.background); + if (Focus.foreground != attribute.background) + Focus = Application.Driver.MakeAttribute (Focus.foreground, attribute.background); + break; + } + break; + + case "Error": + switch (callerMemberName) { + case "Normal": + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + case "HotFocus": + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, attribute.background); + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + break; + } + break; + + } + preparingScheme = false; + return attribute; + } } /// /// The default ColorSchemes for the application. /// public static class Colors { + static ColorScheme _toplevel; + static ColorScheme _base; + static ColorScheme _dialog; + static ColorScheme _menu; + static ColorScheme _error; + + /// + /// The application toplevel color scheme, for the default toplevel views. + /// + public static ColorScheme TopLevel { get { return _toplevel; } set { _toplevel = SetColorScheme (value); } } + /// /// The base color scheme, for the default toplevel views. /// - public static ColorScheme Base; - + public static ColorScheme Base { get { return _base; } set { _base = SetColorScheme (value); } } + /// /// The dialog color scheme, for standard popup dialog boxes /// - public static ColorScheme Dialog; + public static ColorScheme Dialog { get { return _dialog; } set { _dialog = SetColorScheme (value); } } /// /// The menu bar color /// - public static ColorScheme Menu; + public static ColorScheme Menu { get { return _menu; } set { _menu = SetColorScheme (value); } } /// /// The color scheme for showing errors. /// - public static ColorScheme Error; + public static ColorScheme Error { get { return _error; } set { _error = SetColorScheme (value); } } + static ColorScheme SetColorScheme (ColorScheme colorScheme, [CallerMemberName]string callerMemberName = null) + { + colorScheme.caller = callerMemberName; + return colorScheme; + } } /// @@ -238,7 +414,7 @@ namespace Terminal.Gui { RightTee, /// - /// Top tee + /// Top tee /// TopTee, @@ -253,6 +429,9 @@ namespace Terminal.Gui { /// ConsoleDriver is an abstract class that defines the requirements for a console driver. One implementation if the CursesDriver, and another one uses the .NET Console one. /// public abstract class ConsoleDriver { + /// + /// The handler fired when the terminal is resized. + /// protected Action TerminalResized; /// @@ -280,10 +459,16 @@ namespace Terminal.Gui { /// Rune to add. public abstract void AddRune (Rune rune); /// - /// Adds the specified + /// Adds the specified /// /// String. public abstract void AddStr (ustring str); + /// + /// Prepare the driver and set the key and mouse events handlers. + /// + /// + /// + /// public abstract void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action mouseHandler); /// @@ -312,19 +497,27 @@ namespace Terminal.Gui { /// C. public abstract void SetAttribute (Attribute c); - // Set Colors from limit sets of colors + /// + /// Set Colors from limit sets of colors. + /// + /// Foreground. + /// Background. public abstract void SetColors (ConsoleColor foreground, ConsoleColor background); // Advanced uses - set colors to any pre-set pairs, you would need to init_color // that independently with the R, G, B values. /// - /// Advanced uses - set colors to any pre-set pairs, you would need to init_color + /// Advanced uses - set colors to any pre-set pairs, you would need to init_color /// that independently with the R, G, B values. /// /// Foreground color identifier. /// Background color identifier. public abstract void SetColors (short foregroundColorId, short backgroundColorId); + /// + /// Set the handler when the terminal is resized. + /// + /// public void SetTerminalResized(Action terminalResized) { TerminalResized = terminalResized; @@ -388,7 +581,7 @@ namespace Terminal.Gui { for (int l = 0; l < padding; l++) for (b = 0; b < width; b++) AddRune (' '); - } + } } @@ -408,7 +601,14 @@ namespace Terminal.Gui { set => this.clip = value; } + /// + /// Start of mouse moves. + /// public abstract void StartReportingMouseMoves (); + + /// + /// Stop reporting mouses moves. + /// public abstract void StopReportingMouseMoves (); /// @@ -472,7 +672,7 @@ namespace Terminal.Gui { public Rune RightTee; /// - /// Top tee + /// Top tee /// public Rune TopTee; @@ -481,6 +681,12 @@ namespace Terminal.Gui { /// public Rune BottomTee; + /// + /// Make the attribute for the foreground and background colors. + /// + /// Foreground. + /// Background. + /// public abstract Attribute MakeAttribute (Color fore, Color back); } } diff --git a/Terminal.Gui/Drivers/CursesDriver.cs b/Terminal.Gui/Drivers/CursesDriver.cs index 6697579b0..73966586e 100644 --- a/Terminal.Gui/Drivers/CursesDriver.cs +++ b/Terminal.Gui/Drivers/CursesDriver.cs @@ -255,7 +255,7 @@ namespace Terminal.Gui { Colors.Base.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLUE); Colors.Base.HotFocus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN); - // Focused, + // Focused, // Selected, Hot: Yellow on Black // Selected, text: white on black // Unselected, hot: yellow on cyan @@ -264,6 +264,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.Dialog.Normal = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE); Colors.Dialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN); @@ -410,7 +411,7 @@ namespace Terminal.Gui { suspendSignal = 18; break; case "Linux": - // TODO: should fetch the machine name and + // TODO: should fetch the machine name and // if it is MIPS return 24 suspendSignal = 20; break; diff --git a/Terminal.Gui/Drivers/NetDriver.cs b/Terminal.Gui/Drivers/NetDriver.cs index 0487d0d7b..0b5217910 100644 --- a/Terminal.Gui/Drivers/NetDriver.cs +++ b/Terminal.Gui/Drivers/NetDriver.cs @@ -135,7 +135,7 @@ namespace Terminal.Gui { Colors.Base.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Blue); Colors.Base.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); - // Focused, + // Focused, // Selected, Hot: Yellow on Black // Selected, text: white on black // Unselected, hot: yellow on cyan @@ -144,6 +144,7 @@ namespace Terminal.Gui { Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Cyan); + Colors.Menu.Disabled = MakeColor(ConsoleColor.DarkGray, ConsoleColor.Cyan); Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); @@ -348,7 +349,7 @@ namespace Terminal.Gui { } // - // These are for the .NET driver, but running natively on Windows, wont run + // These are for the .NET driver, but running natively on Windows, wont run // on the Mono emulation // diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index 0074181c7..43f227458 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -1,5 +1,5 @@ // -// WindowsDriver.cs: Windows specific driver +// WindowsDriver.cs: Windows specific driver // // Authors: // Miguel de Icaza (miguel@gnome.org) @@ -13,10 +13,10 @@ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -107,7 +107,7 @@ namespace Terminal.Gui { ScreenBuffer = IntPtr.Zero; } - private bool ContinueListeningForConsoleEvents = true; + bool ContinueListeningForConsoleEvents = true; public uint ConsoleMode { get { @@ -151,7 +151,8 @@ namespace Terminal.Gui { Button3Pressed = 8, Button4Pressed = 16, RightmostButtonPressed = 2, - + WheeledUp = unchecked((int)0x780000), + WheeledDown = unchecked((int)0xFF880000), } [Flags] @@ -436,10 +437,17 @@ 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 (); cols = Console.WindowWidth; - rows = Console.WindowHeight - 1; + rows = Console.WindowHeight; WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); ResizeScreen (); @@ -519,7 +527,6 @@ namespace Terminal.Gui { } finally { eventReady.Reset(); } - Debug.WriteLine("Events ready"); if (!tokenSource.IsCancellationRequested) return result != null; @@ -561,7 +568,7 @@ namespace Terminal.Gui { case WindowsConsole.EventType.WindowBufferSize: cols = inputEvent.WindowBufferSizeEvent.size.X; - rows = inputEvent.WindowBufferSizeEvent.size.Y - 1; + rows = inputEvent.WindowBufferSizeEvent.size.Y; ResizeScreen (); UpdateOffScreen (); TerminalResized?.Invoke(); @@ -570,22 +577,34 @@ namespace Terminal.Gui { result = null; } - private WindowsConsole.ButtonState? LastMouseButtonPressed = null; + WindowsConsole.ButtonState? LastMouseButtonPressed = null; + bool IsButtonReleased = false; + bool IsButtonDoubleClicked = false; - private MouseEvent ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent) + MouseEvent ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent) { MouseFlags mouseFlag = MouseFlags.AllEvents; + if (IsButtonDoubleClicked) { + Task.Run (async () => { + await Task.Delay (300); + _ = new Action (() => IsButtonDoubleClicked = false); + }); + } + // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button. // This will tell when a mouse button is pressed. When the button is released this event will // be fired with it's bit set to 0. So when the button is up ButtonState will be 0. // To map to the correct driver events we save the last pressed mouse button so we can // map to the correct clicked event. - if (LastMouseButtonPressed != null && mouseEvent.ButtonState != 0) { + if ((LastMouseButtonPressed != null || IsButtonReleased) && mouseEvent.ButtonState != 0) { LastMouseButtonPressed = null; + IsButtonReleased = false; } - if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed == null) { + if ((mouseEvent.EventFlags == 0 && LastMouseButtonPressed == null && !IsButtonDoubleClicked) || + (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved && + mouseEvent.ButtonState != 0 && !IsButtonDoubleClicked)) { switch (mouseEvent.ButtonState) { case WindowsConsole.ButtonState.Button1Pressed: mouseFlag = MouseFlags.Button1Pressed; @@ -595,12 +614,32 @@ namespace Terminal.Gui { mouseFlag = MouseFlags.Button2Pressed; break; - case WindowsConsole.ButtonState.Button3Pressed: - mouseFlag = MouseFlags.Button3Pressed; + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button4Pressed; break; } + + if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) + mouseFlag |= MouseFlags.ReportMousePosition; LastMouseButtonPressed = mouseEvent.ButtonState; - } else if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed != null) { + } else if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed != null && !IsButtonReleased && + !IsButtonDoubleClicked) { + switch (LastMouseButtonPressed) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Released; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Released; + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button4Released; + break; + } + IsButtonReleased = true; + } else if ((mouseEvent.EventFlags == 0 || mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) && + IsButtonReleased) { switch (LastMouseButtonPressed) { case WindowsConsole.ButtonState.Button1Pressed: mouseFlag = MouseFlags.Button1Clicked; @@ -610,15 +649,59 @@ namespace Terminal.Gui { mouseFlag = MouseFlags.Button2Clicked; break; - case WindowsConsole.ButtonState.Button3Pressed: - mouseFlag = MouseFlags.Button3Clicked; + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button4Clicked; break; } LastMouseButtonPressed = null; + IsButtonReleased = false; + } else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick)) { + switch (mouseEvent.ButtonState) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1DoubleClicked; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2DoubleClicked; + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button4DoubleClicked; + break; + } + IsButtonDoubleClicked = true; + } else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && IsButtonDoubleClicked) { + switch (mouseEvent.ButtonState) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1TripleClicked; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2TripleClicked; + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button4TripleClicked; + break; + } + IsButtonDoubleClicked = false; + } else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled) { + switch (mouseEvent.ButtonState) { + case WindowsConsole.ButtonState.WheeledUp: + mouseFlag = MouseFlags.WheeledUp; + break; + + case WindowsConsole.ButtonState.WheeledDown: + mouseFlag = MouseFlags.WheeledDown; + break; + } + } else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) { mouseFlag = MouseFlags.ReportMousePosition; } + mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag); + return new MouseEvent () { X = mouseEvent.MousePosition.X, Y = mouseEvent.MousePosition.Y, @@ -626,6 +709,21 @@ namespace Terminal.Gui { }; } + static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag) + { + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed) || + mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)) + mouseFlag |= MouseFlags.ButtonCtrl; + + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) + mouseFlag |= MouseFlags.ButtonShift; + + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) || + mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) + mouseFlag |= MouseFlags.ButtonAlt; + return mouseFlag; + } + public ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) { var state = keyEvent.dwControlKeyState; @@ -676,23 +774,23 @@ namespace Terminal.Gui { case ConsoleKey.NumPad0: return keyInfoEx.NumLock ? (Key)(uint)'0' : Key.InsertChar; case ConsoleKey.NumPad1: - return keyInfoEx.NumLock ? (Key)(uint)'1' : Key.End; + return keyInfoEx.NumLock ? (Key)(uint)'1' : Key.End; case ConsoleKey.NumPad2: - return keyInfoEx.NumLock ? (Key)(uint)'2' : Key.CursorDown; + return keyInfoEx.NumLock ? (Key)(uint)'2' : Key.CursorDown; case ConsoleKey.NumPad3: return keyInfoEx.NumLock ? (Key)(uint)'3' : Key.PageDown; case ConsoleKey.NumPad4: - return keyInfoEx.NumLock ? (Key)(uint)'4' : Key.CursorLeft; + return keyInfoEx.NumLock ? (Key)(uint)'4' : Key.CursorLeft; case ConsoleKey.NumPad5: - return keyInfoEx.NumLock ? (Key)(uint)'5' : (Key)((uint)keyInfo.KeyChar); + return keyInfoEx.NumLock ? (Key)(uint)'5' : (Key)((uint)keyInfo.KeyChar); case ConsoleKey.NumPad6: - return keyInfoEx.NumLock ? (Key)(uint)'6' : Key.CursorRight; + return keyInfoEx.NumLock ? (Key)(uint)'6' : Key.CursorRight; case ConsoleKey.NumPad7: - return keyInfoEx.NumLock ? (Key)(uint)'7' : Key.Home; + return keyInfoEx.NumLock ? (Key)(uint)'7' : Key.Home; case ConsoleKey.NumPad8: - return keyInfoEx.NumLock ? (Key)(uint)'8' : Key.CursorUp; + return keyInfoEx.NumLock ? (Key)(uint)'8' : Key.CursorUp; case ConsoleKey.NumPad9: - return keyInfoEx.NumLock ? (Key)(uint)'9' : Key.PageUp; + return keyInfoEx.NumLock ? (Key)(uint)'9' : Key.PageUp; case ConsoleKey.Oem1: case ConsoleKey.Oem2: @@ -708,33 +806,39 @@ namespace Terminal.Gui { case ConsoleKey.OemPlus: case ConsoleKey.OemMinus: return (Key)((uint)keyInfo.KeyChar); - } + } - var key = keyInfo.Key; - var alphaBase = ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ (keyInfoEx.CapsLock)) ? 'A' : 'a'; + var key = keyInfo.Key; + var alphaBase = ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ (keyInfoEx.CapsLock)) ? 'A' : 'a'; - if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { - var delta = key - ConsoleKey.A; - if (keyInfo.Modifiers == ConsoleModifiers.Control) - return (Key)((uint)Key.ControlA + delta); - if (keyInfo.Modifiers == ConsoleModifiers.Alt) - return (Key)(((uint)Key.AltMask) | ((uint)'A' + delta)); - return (Key)((uint)alphaBase + delta); - } - if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { - var delta = key - ConsoleKey.D0; - if (keyInfo.Modifiers == ConsoleModifiers.Alt) - return (Key)(((uint)Key.AltMask) | ((uint)'0' + delta)); + if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { + var delta = key - ConsoleKey.A; + if (keyInfo.Modifiers == ConsoleModifiers.Control) + return (Key)((uint)Key.ControlA + delta); + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'A' + delta)); + if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { + if (keyInfo.KeyChar == 0) + return (Key)(((uint)Key.AltMask) + ((uint)Key.ControlA + delta)); + else + return (Key)((uint)keyInfo.KeyChar); + } + return (Key)((uint)alphaBase + delta); + } + if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { + var delta = key - ConsoleKey.D0; + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'0' + delta)); - return (Key)((uint)keyInfo.KeyChar); - } - if (key >= ConsoleKey.F1 && key <= ConsoleKey.F10) { - var delta = key - ConsoleKey.F1; + return (Key)((uint)keyInfo.KeyChar); + } + if (key >= ConsoleKey.F1 && key <= ConsoleKey.F10) { + var delta = key - ConsoleKey.F1; - return (Key)((int)Key.F1 + delta); + return (Key)((int)Key.F1 + delta); + } + return (Key)(0xffffffff); } - return (Key)(0xffffffff); - } public override void Init (Action terminalResized) { @@ -763,10 +867,11 @@ namespace Terminal.Gui { Colors.Base.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Blue); Colors.Base.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); - Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); + 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.Normal = MakeColor (ConsoleColor.White, 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); @@ -797,7 +902,7 @@ namespace Terminal.Gui { for (int row = 0; row < rows; row++) for (int col = 0; col < cols; col++) { int position = row * cols + col; - OutputBuffer [position].Attributes = (ushort)MakeColor (ConsoleColor.White, ConsoleColor.Blue); + OutputBuffer [position].Attributes = (ushort)Colors.TopLevel.Normal; OutputBuffer [position].Char.UnicodeChar = ' '; } } @@ -843,11 +948,13 @@ namespace Terminal.Gui { currentAttribute = c.value; } - private Attribute MakeColor (ConsoleColor f, ConsoleColor b) + Attribute MakeColor (ConsoleColor f, ConsoleColor b) { // Encode the colors into the int value. return new Attribute () { - value = ((int)f | (int)b << 4) + value = ((int)f | (int)b << 4), + foreground = (Color)f, + background = (Color)b }; } @@ -881,7 +988,7 @@ namespace Terminal.Gui { { if (damageRegion.Left == -1) return; - + var bufferCoords = new WindowsConsole.Coord (){ X = (short)Clip.Width, Y = (short)Clip.Height @@ -908,6 +1015,7 @@ namespace Terminal.Gui { }; winConsole.SetCursorPosition(position); } + public override void End () { winConsole.Cleanup(); @@ -945,5 +1053,4 @@ namespace Terminal.Gui { } - } diff --git a/Terminal.Gui/Event.cs b/Terminal.Gui/Event.cs index 8556a370b..aea8d9780 100644 --- a/Terminal.Gui/Event.cs +++ b/Terminal.Gui/Event.cs @@ -336,7 +336,7 @@ namespace Terminal.Gui { /// Button1DoubleClicked = unchecked((int)0x8), /// - /// The first mouse button was tripple-clicked. + /// The first mouse button was triple-clicked. /// Button1TripleClicked = unchecked((int)0x10), /// @@ -356,9 +356,9 @@ namespace Terminal.Gui { /// Button2DoubleClicked = unchecked((int)0x200), /// - /// The second mouse button was tripple-clicked. + /// The second mouse button was triple-clicked. /// - Button2TrippleClicked = unchecked((int)0x400), + Button2TripleClicked = unchecked((int)0x400), /// /// The third mouse button was pressed. /// @@ -376,7 +376,7 @@ namespace Terminal.Gui { /// Button3DoubleClicked = unchecked((int)0x8000), /// - /// The third mouse button was tripple-clicked. + /// The third mouse button was triple-clicked. /// Button3TripleClicked = unchecked((int)0x10000), /// @@ -396,15 +396,15 @@ namespace Terminal.Gui { /// Button4DoubleClicked = unchecked((int)0x200000), /// - /// The fourth button was tripple-clicked. + /// The fourth button was triple-clicked. /// Button4TripleClicked = unchecked((int)0x400000), /// - /// The fourth button was pressed. + /// Flag: the shift key was pressed when the mouse button took place. /// ButtonShift = unchecked((int)0x2000000), /// - /// Flag: the shift key was pressed when the mouse button took place. + /// Flag: the ctrl key was pressed when the mouse button took place. /// ButtonCtrl = unchecked((int)0x1000000), /// @@ -416,6 +416,14 @@ namespace Terminal.Gui { /// ReportMousePosition = unchecked((int)0x8000000), /// + /// Vertical button wheeled up. + /// + WheeledUp = unchecked((int)0x10000000), + /// + /// Vertical button wheeled up. + /// + WheeledDown = unchecked((int)0x20000000), + /// /// Mask that captures all the events. /// AllEvents = unchecked((int)0x7ffffff), @@ -440,6 +448,21 @@ namespace Terminal.Gui { /// public MouseFlags Flags; + /// + /// The offset X (column) location for the mouse event. + /// + public int OfX; + + /// + /// The offset Y (column) location for the mouse event. + /// + public int OfY; + + /// + /// The current view at the location for the mouse event. + /// + public View View; + /// /// Returns a that represents the current . /// diff --git a/Terminal.Gui/MonoCurses/UnmanagedLibrary.cs b/Terminal.Gui/MonoCurses/UnmanagedLibrary.cs index 1fce883e9..09882dbfd 100644 --- a/Terminal.Gui/MonoCurses/UnmanagedLibrary.cs +++ b/Terminal.Gui/MonoCurses/UnmanagedLibrary.cs @@ -172,7 +172,7 @@ namespace Mono.Terminal.Internal { /// /// Loads library in a platform specific way. /// - private static IntPtr PlatformSpecificLoadLibrary (string libraryPath) + static IntPtr PlatformSpecificLoadLibrary (string libraryPath) { if (IsWindows) { return Windows.LoadLibrary (libraryPath); @@ -192,7 +192,7 @@ namespace Mono.Terminal.Internal { throw new InvalidOperationException ("Unsupported platform."); } - private static string FirstValidLibraryPath (string [] libraryPathAlternatives) + static string FirstValidLibraryPath (string [] libraryPathAlternatives) { foreach (var path in libraryPathAlternatives) { if (File.Exists (path)) { @@ -204,7 +204,7 @@ namespace Mono.Terminal.Internal { string.Join (",", libraryPathAlternatives))); } - private static class Windows + static class Windows { [DllImport ("kernel32.dll")] internal static extern IntPtr LoadLibrary (string filename); @@ -213,7 +213,7 @@ namespace Mono.Terminal.Internal { internal static extern IntPtr GetProcAddress (IntPtr hModule, string procName); } - private static class Linux + static class Linux { [DllImport ("libdl.so")] internal static extern IntPtr dlopen (string filename, int flags); @@ -222,7 +222,7 @@ namespace Mono.Terminal.Internal { internal static extern IntPtr dlsym (IntPtr handle, string symbol); } - private static class MacOSX + static class MacOSX { [DllImport ("libSystem.dylib")] internal static extern IntPtr dlopen (string filename, int flags); @@ -238,7 +238,7 @@ namespace Mono.Terminal.Internal { /// dlopen and dlsym from the current process as on Linux /// Mono sure is linked against these symbols. /// - private static class Mono + static class Mono { [DllImport ("__Internal")] internal static extern IntPtr dlopen (string filename, int flags); @@ -252,7 +252,7 @@ namespace Mono.Terminal.Internal { /// dlopen and dlsym from the "libcoreclr.so", /// to avoid the dependency on libc-dev Linux. /// - private static class CoreCLR + static class CoreCLR { [DllImport ("libcoreclr.so")] internal static extern IntPtr dlopen (string filename, int flags); diff --git a/Terminal.Gui/MonoCurses/mainloop.cs b/Terminal.Gui/MonoCurses/mainloop.cs index 280c4dfee..f2927f8e0 100644 --- a/Terminal.Gui/MonoCurses/mainloop.cs +++ b/Terminal.Gui/MonoCurses/mainloop.cs @@ -354,10 +354,9 @@ namespace Mono.Terminal { /// public Func AddIdle (Func idleHandler) { - lock (idleHandlers) { + lock (idleHandlers) idleHandlers.Add (idleHandler); - driver.Wakeup (); - } + return idleHandler; } diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index fa2ce8b07..559d3c249 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -1,6 +1,6 @@ - net461;netstandard2.0 + net472;netstandard2.0 Terminal.Gui Terminal.Gui bin\Release\Terminal.Gui.xml @@ -57,23 +57,18 @@ - - - + - + diff --git a/Terminal.Gui/Types/PosDim.cs b/Terminal.Gui/Types/PosDim.cs index bf7fdf16c..1510d63ec 100644 --- a/Terminal.Gui/Types/PosDim.cs +++ b/Terminal.Gui/Types/PosDim.cs @@ -187,8 +187,16 @@ namespace Terminal.Gui { else return la - ra; } + + public override string ToString () + { + return $"{((PosView)left).Target.ToString ()},{right.ToString ()}"; + } + } + static PosCombine posCombine; + /// /// Adds a to a , yielding a new . /// @@ -197,7 +205,10 @@ namespace Terminal.Gui { /// The that is the sum of the values of left and right. public static Pos operator + (Pos left, Pos right) { - return new PosCombine (true, left, right); + PosCombine newPos = new PosCombine (true, left, right); + if (posCombine?.ToString () != newPos.ToString ()) + ((PosView)left).Target.SetNeedsLayout (); + return posCombine = newPos; } /// @@ -208,7 +219,10 @@ namespace Terminal.Gui { /// The that is the left minus right. public static Pos operator - (Pos left, Pos right) { - return new PosCombine (false, left, right); + PosCombine newPos = new PosCombine (false, left, right); + if (posCombine?.ToString () != newPos.ToString ()) + ((PosView)left).Target.SetNeedsLayout (); + return posCombine = newPos; } internal class PosView : Pos { @@ -276,6 +290,7 @@ namespace Terminal.Gui { } /// + /// Dim properties of a view to control the position. /// /// /// @@ -331,7 +346,7 @@ namespace Terminal.Gui { return new DimFactor (n / 100); } - class DimAbsolute : Dim { + internal class DimAbsolute : Dim { int n; public DimAbsolute (int n) { this.n = n; } @@ -351,7 +366,7 @@ namespace Terminal.Gui { } - class DimFill : Dim { + internal class DimFill : Dim { int margin; public DimFill (int margin) { this.margin = margin; } diff --git a/Terminal.Gui/Types/Rect.cs b/Terminal.Gui/Types/Rect.cs index d5165907c..7b6f1ef4d 100644 --- a/Terminal.Gui/Types/Rect.cs +++ b/Terminal.Gui/Types/Rect.cs @@ -43,7 +43,7 @@ namespace Terminal.Gui /// /// An uninitialized Rectangle Structure. /// - + public static readonly Rect Empty; /// @@ -54,7 +54,7 @@ namespace Terminal.Gui /// Produces a Rectangle structure from left, top, right /// and bottom coordinates. /// - + public static Rect FromLTRB (int left, int top, int right, int bottom) { @@ -70,7 +70,7 @@ namespace Terminal.Gui /// Produces a new Rectangle by inflating an existing /// Rectangle by the specified coordinate values. /// - + public static Rect Inflate (Rect rect, int x, int y) { Rect r = new Rect (rect.Location, rect.Size); @@ -85,7 +85,7 @@ namespace Terminal.Gui /// /// Inflates the Rectangle by a specified width and height. /// - + public void Inflate (int width, int height) { Inflate (new Size (width, height)); @@ -98,7 +98,7 @@ namespace Terminal.Gui /// /// Inflates the Rectangle by a specified Size. /// - + public void Inflate (Size size) { X -= size.Width; @@ -115,7 +115,7 @@ namespace Terminal.Gui /// Produces a new Rectangle by intersecting 2 existing /// Rectangles. Returns null if there is no intersection. /// - + public static Rect Intersect (Rect a, Rect b) { // MS.NET returns a non-empty rectangle if the two rectangles @@ -138,7 +138,7 @@ namespace Terminal.Gui /// Replaces the Rectangle with the intersection of itself /// and another Rectangle. /// - + public void Intersect (Rect rect) { this = Rect.Intersect (this, rect); @@ -152,7 +152,7 @@ namespace Terminal.Gui /// Produces a new Rectangle from the union of 2 existing /// Rectangles. /// - + public static Rect Union (Rect a, Rect b) { return FromLTRB (Math.Min (a.Left, b.Left), @@ -176,7 +176,7 @@ namespace Terminal.Gui return ((left.Location == right.Location) && (left.Size == right.Size)); } - + /// /// Inequality Operator /// @@ -192,7 +192,6 @@ namespace Terminal.Gui return ((left.Location != right.Location) || (left.Size != right.Size)); } - // ----------------------- // Public Constructors @@ -205,7 +204,7 @@ namespace Terminal.Gui /// /// Creates a Rectangle from Point and Size values. /// - + public Rect (Point location, Size size) { X = location.X; @@ -222,7 +221,7 @@ namespace Terminal.Gui /// Creates a Rectangle from a specified x,y location and /// width and height values. /// - + public Rect (int x, int y, int width, int height) { this.X = x; @@ -268,7 +267,7 @@ namespace Terminal.Gui /// The X coordinate of the left edge of the Rectangle. /// Read only. /// - + public int Left { get { return X; @@ -282,7 +281,7 @@ namespace Terminal.Gui /// /// The Location of the top-left corner of the Rectangle. /// - + public Point Location { get { return new Point (X, Y); @@ -301,7 +300,7 @@ namespace Terminal.Gui /// The X coordinate of the right edge of the Rectangle. /// Read only. /// - + public int Right { get { return X + Width; @@ -315,7 +314,7 @@ namespace Terminal.Gui /// /// The Size of the Rectangle. /// - + public Size Size { get { return new Size (Width, Height); @@ -334,7 +333,7 @@ namespace Terminal.Gui /// The Y coordinate of the top edge of the Rectangle. /// Read only. /// - + public int Top { get { return Y; @@ -348,7 +347,7 @@ namespace Terminal.Gui /// /// Checks if an x,y coordinate lies within this Rectangle. /// - + public bool Contains (int x, int y) { return ((x >= Left) && (x < Right) && @@ -362,7 +361,7 @@ namespace Terminal.Gui /// /// Checks if a Point lies within this Rectangle. /// - + public bool Contains (Point pt) { return Contains (pt.X, pt.Y); @@ -376,7 +375,7 @@ namespace Terminal.Gui /// Checks if a Rectangle lies entirely within this /// Rectangle. /// - + public bool Contains (Rect rect) { return (rect == Intersect (this, rect)); @@ -389,7 +388,7 @@ namespace Terminal.Gui /// /// Checks equivalence of this Rectangle and another object. /// - + public override bool Equals (object obj) { if (!(obj is Rect)) @@ -405,7 +404,7 @@ namespace Terminal.Gui /// /// Calculates a hashing value. /// - + public override int GetHashCode () { return (Height + Width) ^ X + Y; @@ -418,14 +417,14 @@ namespace Terminal.Gui /// /// Checks if a Rectangle intersects with this one. /// - + public bool IntersectsWith (Rect rect) { return !((Left >= rect.Right) || (Right <= rect.Left) || (Top >= rect.Bottom) || (Bottom <= rect.Top)); } - private bool IntersectsWithInclusive (Rect r) + bool IntersectsWithInclusive (Rect r) { return !((Left > r.Right) || (Right < r.Left) || (Top > r.Bottom) || (Bottom < r.Top)); @@ -444,7 +443,7 @@ namespace Terminal.Gui this.X += x; this.Y += y; } - + /// /// Offset Method /// @@ -458,7 +457,7 @@ namespace Terminal.Gui X += pos.X; Y += pos.Y; } - + /// /// ToString Method /// @@ -466,7 +465,7 @@ namespace Terminal.Gui /// /// Formats the Rectangle as a string in (x,y,w,h) notation. /// - + public override string ToString () { return String.Format ("{{X={0},Y={1},Width={2},Height={3}}}", diff --git a/Terminal.Gui/Types/Size.cs b/Terminal.Gui/Types/Size.cs index ff6493fa3..0a7bc204c 100644 --- a/Terminal.Gui/Types/Size.cs +++ b/Terminal.Gui/Types/Size.cs @@ -15,8 +15,8 @@ namespace Terminal.Gui { /// Stores an ordered pair of integers, which specify a Height and Width. /// public struct Size - { - private int width, height; + { + int width, height; /// /// Gets a Size structure that has a Height and Width value of 0. @@ -36,7 +36,7 @@ namespace Terminal.Gui { return new Size (sz1.Width + sz2.Width, sz1.Height + sz2.Height); } - + /// /// Equality Operator /// @@ -52,7 +52,7 @@ namespace Terminal.Gui { return ((sz1.Width == sz2.Width) && (sz1.Height == sz2.Height)); } - + /// /// Inequality Operator /// @@ -68,7 +68,7 @@ namespace Terminal.Gui { return ((sz1.Width != sz2.Width) || (sz1.Height != sz2.Height)); } - + /// /// Subtraction Operator /// @@ -82,7 +82,7 @@ namespace Terminal.Gui { return new Size (sz1.Width - sz2.Width, sz1.Height - sz2.Height); } - + /// /// Size to Point Conversion /// @@ -104,7 +104,7 @@ namespace Terminal.Gui { /// /// Creates a Size from a Point value. /// - + public Size (Point pt) { width = pt.X; @@ -118,7 +118,7 @@ namespace Terminal.Gui { /// /// Creates a Size from specified dimensions. /// - + public Size (int width, int height) { this.width = width; @@ -132,7 +132,7 @@ namespace Terminal.Gui { /// /// Indicates if both Width and Height are zero. /// - + public bool IsEmpty { get { return ((width == 0) && (height == 0)); @@ -146,7 +146,7 @@ namespace Terminal.Gui { /// /// The Width coordinate of the Size. /// - + public int Width { get { return width; @@ -163,7 +163,7 @@ namespace Terminal.Gui { /// /// The Height coordinate of the Size. /// - + public int Height { get { return height; @@ -180,7 +180,7 @@ namespace Terminal.Gui { /// /// Checks equivalence of this Size and another object. /// - + public override bool Equals (object obj) { if (!(obj is Size)) @@ -196,7 +196,7 @@ namespace Terminal.Gui { /// /// Calculates a hashing value. /// - + public override int GetHashCode () { return width^height; @@ -209,7 +209,7 @@ namespace Terminal.Gui { /// /// Formats the Size as a string in coordinate notation. /// - + public override string ToString () { return String.Format ("{{Width={0}, Height={1}}}", width, height); @@ -227,7 +227,13 @@ namespace Terminal.Gui { sz1.Height + sz2.Height); } - + + /// + /// Subtracts the width and height of one Size structure to the width and height of another Size structure. + /// + /// The subtract. + /// The first Size structure to subtract. + /// The second Size structure to subtract. public static Size Subtract (Size sz1, Size sz2) { return new Size (sz1.Width - sz2.Width, diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 7304000e6..f816a98f3 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -68,10 +68,16 @@ namespace Terminal.Gui { CanFocus = true; this.IsDefault = is_default; Text = text; + int w = SetWidthHeight (text, is_default); + Frame = new Rect (0, 0, w, 1); + } + + int SetWidthHeight (ustring text, bool is_default) + { int w = text.Length + 4 + (is_default ? 2 : 0); Width = w; Height = 1; - Frame = new Rect (0, 0, w, 1); + return w; } /// @@ -96,6 +102,9 @@ namespace Terminal.Gui { } set { + if (text?.Length != value?.Length) { + SetWidthHeight (value, is_default); + } text = value; Update (); } @@ -166,8 +175,7 @@ namespace Terminal.Gui { { if (Char.ToUpper ((char)key.KeyValue) == hot_key) { this.SuperView.SetFocus (this); - if (Clicked != null) - Clicked (); + Clicked?.Invoke (); return true; } return false; diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs new file mode 100644 index 000000000..1f1915a10 --- /dev/null +++ b/Terminal.Gui/Views/DateField.cs @@ -0,0 +1,241 @@ +// +// DateField.cs: text entry for date +// +// Author: Barry Nolte +// +// Licensed under the MIT license +// +using System; +using System.Globalization; +using System.Linq; +using NStack; + +namespace Terminal.Gui { + + /// + /// Date edit widget + /// + /// + /// This widget provides date editing functionality, and mouse support. + /// + public class DateField : TextField { + bool isShort; + + int longFieldLen = 10; + int shortFieldLen = 8; + int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } } + string sepChar; + string longFormat; + string shortFormat; + string Format { get { return isShort ? shortFormat : longFormat; } } + + + /// + /// Public constructor that creates a date edit field at an absolute position and fixed size. + /// + /// The x coordinate. + /// The y coordinate. + /// Initial date contents. + /// If true, shows only two digits for the year. + public DateField(int x, int y, DateTime date, bool isShort = false) : base(x, y, isShort ? 10 : 12, "") + { + CultureInfo cultureInfo = CultureInfo.CurrentCulture; + sepChar = cultureInfo.DateTimeFormat.DateSeparator; + longFormat = $" {cultureInfo.DateTimeFormat.ShortDatePattern}"; + shortFormat = GetShortFormat(longFormat); + this.isShort = isShort; + CursorPosition = 1; + Date = date; + Changed += DateField_Changed; + } + + void DateField_Changed(object sender, ustring e) + { + if (!DateTime.TryParseExact(Text.ToString(), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) + Text = e; + } + + string GetShortFormat(string lf) + { + return lf.Replace("yyyy", "yy"); + } + + /// + /// Gets or sets the date in the widget. + /// + /// + /// + public DateTime Date { + get { + if (!DateTime.TryParseExact(Text.ToString(), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) return new DateTime(); + return result; + } + set { + this.Text = value.ToString(Format); + } + } + + bool SetText(Rune key) + { + var text = TextModel.ToRunes(Text); + var newText = text.GetRange(0, CursorPosition); + newText.Add(key); + if (CursorPosition < FieldLen) + newText = newText.Concat(text.GetRange(CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList(); + return SetText(ustring.Make(newText)); + } + + bool SetText(ustring text) + { + ustring[] vals = text.Split(ustring.Make(sepChar)); + ustring[] frm = ustring.Make(Format).Split(ustring.Make(sepChar)); + bool isValidDate = true; + int idx = GetFormatIndex(frm, "y"); + int year = Int32.Parse(vals[idx].ToString()); + int month; + int day; + idx = GetFormatIndex(frm, "M"); + if (Int32.Parse(vals[idx].ToString()) < 1) { + isValidDate = false; + month = 1; + vals[idx] = "1"; + } else if (Int32.Parse(vals[idx].ToString()) > 12) { + isValidDate = false; + month = 12; + vals[idx] = "12"; + } else + month = Int32.Parse(vals[idx].ToString()); + idx = GetFormatIndex(frm, "d"); + if (Int32.Parse(vals[idx].ToString()) < 1) { + isValidDate = false; + day = 1; + vals[idx] = "1"; + } else if (Int32.Parse(vals[idx].ToString()) > 31) { + isValidDate = false; + day = DateTime.DaysInMonth(year, month); + vals[idx] = day.ToString(); + } else + day = Int32.Parse(vals[idx].ToString()); + string date = GetData(month, day, year, frm); + Text = date; + + if (!DateTime.TryParseExact(date, Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) || + !isValidDate) + return false; + return true; + } + + string GetData(int month, int day, int year, ustring[] fm) + { + string data = " "; + for (int i = 0; i < fm.Length; i++) { + if (fm[i].Contains("M")) + data += $"{month,2:00}"; + else if (fm[i].Contains("d")) + data += $"{day,2:00}"; + else + data += isShort ? $"{year,2:00}" : $"{year,4:0000}"; + if (i < 2) + data += $"{sepChar}"; + } + return data; + } + + int GetFormatIndex(ustring[] fm, string t) + { + int idx = -1; + for (int i = 0; i < fm.Length; i++) { + if (fm[i].Contains(t)) { + idx = i; + break; + } + } + return idx; + } + + void IncCursorPosition() + { + if (CursorPosition == FieldLen) + return; + if (Text[++CursorPosition] == sepChar.ToCharArray()[0]) + CursorPosition++; + } + + void DecCursorPosition() + { + if (CursorPosition == 1) + return; + if (Text[--CursorPosition] == sepChar.ToCharArray()[0]) + CursorPosition--; + } + + void AdjCursorPosition() + { + if (Text[CursorPosition] == sepChar.ToCharArray()[0]) + CursorPosition++; + } + + public override bool ProcessKey(KeyEvent kb) + { + switch (kb.Key) { + case Key.DeleteChar: + case Key.ControlD: + SetText('0'); + break; + + case Key.Delete: + case Key.Backspace: + SetText('0'); + DecCursorPosition(); + break; + + // Home, C-A + case Key.Home: + case Key.ControlA: + CursorPosition = 1; + break; + + case Key.CursorLeft: + case Key.ControlB: + DecCursorPosition(); + break; + + case Key.End: + case Key.ControlE: // End + CursorPosition = FieldLen; + break; + + case Key.CursorRight: + case Key.ControlF: + IncCursorPosition(); + break; + + default: + // Ignore non-numeric characters. + if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9')) + return false; + if (SetText(TextModel.ToRunes(ustring.Make((uint)kb.Key)).First())) + IncCursorPosition(); + return true; + } + return true; + } + + public override bool MouseEvent(MouseEvent ev) + { + if (!ev.Flags.HasFlag(MouseFlags.Button1Clicked)) + return false; + if (!HasFocus) + SuperView.SetFocus(this); + + var point = ev.X; + if (point > FieldLen) + point = FieldLen; + if (point < 1) + point = 1; + CursorPosition = point; + AdjCursorPosition(); + return true; + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 22866f382..f87d941ee 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -248,7 +248,7 @@ namespace Terminal.Gui { Attribute textColor = -1; /// /// The color used for the label - /// + /// public Attribute TextColor { get => textColor; set { diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 71ea3a709..41d4bc135 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -20,6 +20,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using NStack; namespace Terminal.Gui { @@ -123,6 +125,21 @@ namespace Terminal.Gui { } } + /// + /// Sets the source to an IList value asynchronously, if you want to set a full IListDataSource, use the Source property. + /// + /// An item implementing the IList interface. + public Task SetSourceAsync (IList source) + { + return Task.Factory.StartNew (() => { + if (source == null) + Source = null; + else + Source = MakeWrapper (source); + return source; + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + bool allowsMarking; /// /// Gets or sets a value indicating whether this allows items to be marked. @@ -142,6 +159,8 @@ namespace Terminal.Gui { } } + public bool AllowsMultipleSelection { get; set; } = true; + /// /// Gets or sets the item that is displayed at the top of the listview /// @@ -305,8 +324,21 @@ namespace Terminal.Gui { return base.ProcessKey (kb); } + public virtual bool AllowsAll () + { + if (!allowsMarking) + return false; + if (!AllowsMultipleSelection) { + for (int i = 0; i < Source.Count; i++) { + if (Source.IsMarked (i) && i != selected) + return false; + } + } + return true; + } + public virtual bool MarkUnmarkRow(){ - if (allowsMarking){ + if (AllowsAll ()) { Source.SetMark(SelectedItem, !Source.IsMarked(SelectedItem)); SetNeedsDisplay(); return true; @@ -400,7 +432,7 @@ namespace Terminal.Gui { return true; selected = top + me.Y; - if (allowsMarking) { + if (AllowsAll ()) { Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem)); SetNeedsDisplay (); return true; diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index ee120eb5f..f44c9b21c 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -1,515 +1,993 @@ -// -// Menu.cs: application menus and submenus -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// TODO: -// Add accelerator support, but should also support chords (ShortCut in MenuItem) -// Allow menus inside menus - -using System; -using NStack; -using System.Linq; - -namespace Terminal.Gui { - - /// - /// A menu item has a title, an associated help text, and an action to execute on activation. - /// - public class MenuItem { - - /// - /// Initializes a new . - /// - /// Title for the menu item. - /// Help text to display. - /// Action to invoke when the menu item is activated. - public MenuItem (ustring title, string help, Action action) - { - Title = title ?? ""; - Help = help ?? ""; - Action = action; - bool nextIsHot = false; - foreach (var x in Title) { - if (x == '_') - nextIsHot = true; - else { - if (nextIsHot) { - HotKey = Char.ToUpper ((char)x); - break; - } - nextIsHot = false; - } - } - } - - // - // - - /// - /// The hotkey is used when the menu is active, the shortcut can be triggered when the menu is not active. - /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry - /// if the ShortCut is set to "Control-N", this would be a global hotkey that would trigger as well - /// - public Rune HotKey; - - /// - /// This is the global setting that can be used as a global shortcut to invoke the action on the menu. - /// - public Key ShortCut; - - /// - /// Gets or sets the title. - /// - /// The title. - public ustring Title { get; set; } - - /// - /// Gets or sets the help text for the menu item. - /// - /// The help text. - public ustring Help { get; set; } - - /// - /// Gets or sets the action to be invoked when the menu is triggered - /// - /// Method to invoke. - public Action Action { get; set; } - internal int Width => Title.Length + Help.Length + 1 + 2; - } - - /// - /// A menu bar item contains other menu items. - /// - public class MenuBarItem { - public MenuBarItem (ustring title, MenuItem [] children) - { - SetTitle (title ?? ""); - Children = children; - } - - void SetTitle (ustring title) - { - if (title == null) - title = ""; - Title = title; - int len = 0; - foreach (var ch in Title) { - if (ch == '_') - continue; - len++; - } - TitleLength = len; - } - - /// - /// Gets or sets the title to display. - /// - /// The title. - public ustring Title { get; set; } - - /// - /// Gets or sets the children for this MenuBarItem - /// - /// The children. - public MenuItem [] Children { get; set; } - internal int TitleLength { get; private set; } - } - - class Menu : View { - MenuBarItem barItems; - MenuBar host; - int current; - - static Rect MakeFrame (int x, int y, MenuItem [] items) - { - int maxW = items.Max(z=>z?.Width) ?? 0; - - return new Rect (x, y, maxW + 2, items.Length + 2); - } - - public Menu (MenuBar host, int x, int y, MenuBarItem barItems) : base (MakeFrame (x, y, barItems.Children)) - { - this.barItems = barItems; - this.host = host; - current = -1; - for (int i = 0; i < barItems.Children.Length; i++) { - if (barItems.Children[i] != null) { - current = i; - break; - } - } - ColorScheme = Colors.Menu; - CanFocus = true; - } - - public override void Redraw (Rect region) - { - Driver.SetAttribute (ColorScheme.Normal); - DrawFrame (region, padding: 0, fill: true); - - for (int i = 0; i < barItems.Children.Length; i++){ - var item = barItems.Children [i]; - Move (1, i+1); - Driver.SetAttribute (item == null ? Colors.Base.Focus : i == current ? ColorScheme.Focus : ColorScheme.Normal); - for (int p = 0; p < Frame.Width-2; p++) - if (item == null) - Driver.AddRune (Driver.HLine); - else - Driver.AddRune (' '); - - if (item == null) - continue; - - Move (2, i + 1); - DrawHotString (item.Title, - i == current? ColorScheme.HotFocus : ColorScheme.HotNormal, - i == current ? ColorScheme.Focus : ColorScheme.Normal); - - // The help string - var l = item.Help.Length; - Move (Frame.Width - l - 2, 1 + i); - Driver.AddStr (item.Help); - } - } - - public override void PositionCursor () - { - Move (2, 1 + current); - } - - void Run (Action action) - { - if (action == null) - return; - - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); - } - - public override bool ProcessKey (KeyEvent kb) - { - switch (kb.Key) { - case Key.CursorUp: - if (current == -1) - break; - do { - current--; - if (current < 0) - current = barItems.Children.Length - 1; - } while (barItems.Children [current] == null); - SetNeedsDisplay (); - break; - case Key.CursorDown: - do { - current++; - if (current == barItems.Children.Length) - current = 0; - } while (barItems.Children [current] == null); - SetNeedsDisplay (); - break; - case Key.CursorLeft: - host.PreviousMenu (); - break; - case Key.CursorRight: - host.NextMenu (); - break; - case Key.Esc: - host.CloseMenu (); - break; - case Key.Enter: - host.CloseMenu (); - Run (barItems.Children [current].Action); - break; - default: - // TODO: rune-ify - if (Char.IsLetterOrDigit ((char)kb.KeyValue)) { - var x = Char.ToUpper ((char)kb.KeyValue); - - foreach (var item in barItems.Children) { - if (item.HotKey == x) { - host.CloseMenu (); - Run (item.Action); - return true; - } - } - } - break; - } - return true; - } - - public override bool MouseEvent(MouseEvent me) - { - if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1Released) { - if (me.Y < 1) - return true; - var item = me.Y - 1; - if (item >= barItems.Children.Length) - return true; - host.CloseMenu (); - Run (barItems.Children [item].Action); - return true; - } - if (me.Flags == MouseFlags.Button1Pressed) { - if (me.Y < 1) - return true; - if (me.Y - 1 >= barItems.Children.Length) - return true; - current = me.Y - 1; - SetNeedsDisplay (); - return true; - } - return false; - } - } - - /// - /// A menu bar for your application. - /// - public class MenuBar : View { - /// - /// The menus that were defined when the menubar was created. This can be updated if the menu is not currently visible. - /// - /// The menu array. - public MenuBarItem [] Menus { get; set; } - int selected; - Action action; - - - /// - /// Initializes a new instance of the class with the specified set of toplevel menu items. - /// - /// Individual menu items, if one of those contains a null, then a separator is drawn. - public MenuBar (MenuBarItem [] menus) : base () - { - X = 0; - Y = 0; - Width = Dim.Fill (); - Height = 1; - Menus = menus; - CanFocus = false; - selected = -1; - ColorScheme = Colors.Menu; - } - - public override void Redraw (Rect region) - { - Move (0, 0); - Driver.SetAttribute (Colors.Base.Focus); - for (int i = 0; i < Frame.Width; i++) - Driver.AddRune (' '); - - Move (1, 0); - int pos = 1; - - for (int i = 0; i < Menus.Length; i++) { - var menu = Menus [i]; - Move (pos, 0); - Attribute hotColor, normalColor; - if (i == selected){ - hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal; - normalColor = i == selected ? ColorScheme.Focus : ColorScheme.Normal; - } else { - hotColor = Colors.Base.Focus; - normalColor = Colors.Base.Focus; - } - DrawHotString (" " + menu.Title + " " + " ", hotColor, normalColor); - pos += menu.TitleLength+ 3; - } - PositionCursor (); - } - - public override void PositionCursor () - { - int pos = 0; - for (int i = 0; i < Menus.Length; i++) { - if (i == selected) { - pos++; - Move (pos, 0); - return; - } else { - pos += Menus [i].TitleLength + 4; - } - } - Move (0, 0); - } - - void Selected (MenuItem item) - { - // TODO: Running = false; - action = item.Action; - } - - public event EventHandler OnOpenMenu; - Menu openMenu; - View previousFocused; - - void OpenMenu (int index) - { - OnOpenMenu?.Invoke(this, null); - if (openMenu != null) - SuperView.Remove (openMenu); - - int pos = 0; - for (int i = 0; i < index; i++) - pos += Menus [i].Title.Length + 3; - - openMenu = new Menu (this, pos, 1, Menus [index]); - - SuperView.Add (openMenu); - SuperView.SetFocus (openMenu); - } - - // Starts the menu from a hotkey - void StartMenu () - { - if (openMenu != null) - return; - selected = 0; - SetNeedsDisplay (); - - previousFocused = SuperView.Focused; - OpenMenu (selected); - } - - // Activates the menu, handles either first focus, or activating an entry when it was already active - // For mouse events. - void Activate (int idx) - { - selected = idx; - if (openMenu == null) - previousFocused = SuperView.Focused; - - OpenMenu (idx); - SetNeedsDisplay (); - } - - internal void CloseMenu () - { - selected = -1; - SetNeedsDisplay (); - SuperView.Remove (openMenu); - previousFocused?.SuperView?.SetFocus (previousFocused); - openMenu = null; - } - - internal void PreviousMenu () - { - if (selected <= 0) - selected = Menus.Length - 1; - else - selected--; - - OpenMenu (selected); - } - - internal void NextMenu () - { - if (selected == -1) - selected = 0; - else if (selected + 1 == Menus.Length) - selected = 0; - else - selected++; - OpenMenu (selected); - } - - internal bool FindAndOpenMenuByHotkey(KeyEvent kb) - { - int pos = 0; +// +// Menu.cs: application menus and submenus +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// TODO: +// Add accelerator support, but should also support chords (ShortCut in MenuItem) +// Allow menus inside menus + +using System; +using NStack; +using System.Linq; +using System.Collections.Generic; + +namespace Terminal.Gui { + + /// + /// A menu item has a title, an associated help text, and an action to execute on activation. + /// + public class MenuItem { + + /// + /// Initializes a new . + /// + /// Title for the menu item. + /// Help text to display. + /// Action to invoke when the menu item is activated. + /// Function to determine if the action can currently be executred. + public MenuItem (ustring title, string help, Action action, Func canExecute = null) + { + Title = title ?? ""; + Help = help ?? ""; + Action = action; + CanExecute = canExecute; + bool nextIsHot = false; + foreach (var x in Title) { + if (x == '_') + nextIsHot = true; + else { + if (nextIsHot) { + HotKey = Char.ToUpper ((char)x); + break; + } + nextIsHot = false; + } + } + } + + /// + /// Initializes a new . + /// + /// Title for the menu item. + /// The menu sub-menu. + public MenuItem (ustring title, MenuBarItem subMenu) : this (title, "", null) + { + SubMenu = subMenu; + IsFromSubMenu = true; + } + + // + // + + /// + /// The hotkey is used when the menu is active, the shortcut can be triggered when the menu is not active. + /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry + /// if the ShortCut is set to "Control-N", this would be a global hotkey that would trigger as well + /// + public Rune HotKey; + + /// + /// This is the global setting that can be used as a global shortcut to invoke the action on the menu. + /// + public Key ShortCut; + + /// + /// Gets or sets the title. + /// + /// The title. + public ustring Title { get; set; } + + /// + /// Gets or sets the help text for the menu item. + /// + /// The help text. + public ustring Help { get; set; } + + /// + /// Gets or sets the action to be invoked when the menu is triggered + /// + /// Method to invoke. + public Action Action { get; set; } + + /// + /// Gets or sets the action to be invoked if the menu can be triggered + /// + /// Function to determine if action is ready to be executed. + public Func CanExecute { get; set; } + + /// + /// Shortcut to check if the menu item is enabled + /// + public bool IsEnabled () + { + return CanExecute == null ? true : CanExecute (); + } + + internal int Width => Title.Length + Help.Length + 1 + 2; + + /// + /// Gets or sets the parent for this MenuBarItem + /// + /// The parent. + internal MenuBarItem SubMenu { get; set; } + internal bool IsFromSubMenu { get; set; } + + /// + /// Merely a debugging aid to see the interaction with main + /// + public MenuItem GetMenuItem () + { + return this; + } + + /// + /// Merely a debugging aid to see the interaction with main + /// + public bool GetMenuBarItem () + { + return IsFromSubMenu; + } + } + + /// + /// A menu bar item contains other menu items. + /// + public class MenuBarItem { + /// + /// Initializes a new . + /// + /// Title for the menu item. + /// The items in the current menu. + public MenuBarItem (ustring title, MenuItem [] children) + { + SetTitle (title ?? ""); + Children = children; + } + + /// + /// Initializes a new . + /// + /// The items in the current menu. + public MenuBarItem (MenuItem[] children) : this (new string (' ', GetMaxTitleLength (children)), children) + { + } + + static int GetMaxTitleLength (MenuItem[] children) + { + int maxLength = 0; + foreach (var item in children) { + int len = GetMenuBarItemLength (item.Title); + if (len > maxLength) + maxLength = len; + item.IsFromSubMenu = true; + } + + return maxLength; + } + + void SetTitle (ustring title) + { + if (title == null) + title = ""; + Title = title; + TitleLength = GetMenuBarItemLength(Title); + } + + static int GetMenuBarItemLength(ustring title) + { + int len = 0; + foreach (var ch in title) { + if (ch == '_') + continue; + len++; + } + + return len; + } + + /// + /// Gets or sets the title to display. + /// + /// The title. + public ustring Title { get; set; } + + /// + /// Gets or sets the children for this MenuBarItem + /// + /// The children. + public MenuItem [] Children { get; set; } + internal int TitleLength { get; private set; } + } + + class Menu : View { + internal MenuBarItem barItems; + MenuBar host; + internal int current; + internal View previousSubFocused; + + static Rect MakeFrame (int x, int y, MenuItem [] items) + { + int maxW = items.Max(z => z?.Width) ?? 0; + + return new Rect (x, y, maxW + 2, items.Length + 2); + } + + public Menu (MenuBar host, int x, int y, MenuBarItem barItems) : base (MakeFrame (x, y, barItems.Children)) + { + this.barItems = barItems; + this.host = host; + current = -1; + for (int i = 0; i < barItems.Children.Length; i++) { + if (barItems.Children[i] != null) { + current = i; + break; + } + } + ColorScheme = Colors.Menu; + CanFocus = true; + WantMousePositionReports = host.WantMousePositionReports; + } + + internal Attribute DetermineColorSchemeFor (MenuItem item, int index) + { + if (item != null) { + if (index == current) return ColorScheme.Focus; + if (!item.IsEnabled ()) return ColorScheme.Disabled; + } + return ColorScheme.Normal; + } + + public override void Redraw (Rect region) + { + Driver.SetAttribute (ColorScheme.Normal); + DrawFrame (region, padding: 0, fill: true); + + for (int i = 0; i < barItems.Children.Length; i++) { + var item = barItems.Children [i]; + Driver.SetAttribute (item == null ? ColorScheme.Normal : i == current ? ColorScheme.Focus : ColorScheme.Normal); + if (item == null) { + Move (0, i + 1); + Driver.AddRune (Driver.LeftTee); + } else + Move (1, i + 1); + + Driver.SetAttribute (DetermineColorSchemeFor (item, i)); + for (int p = 0; p < Frame.Width - 2; p++) + if (item == null) + Driver.AddRune (Driver.HLine); + else if (p == Frame.Width - 3 && barItems.Children [i].SubMenu != null) + Driver.AddRune ('>'); + else + Driver.AddRune (' '); + + if (item == null) { + Move (Frame.Right - 1, i + 1); + Driver.AddRune (Driver.RightTee); + continue; + } + + Move (2, i + 1); + if (!item.IsEnabled ()) + DrawHotString (item.Title, ColorScheme.Disabled, ColorScheme.Disabled); + else + DrawHotString (item.Title, + i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal, + i == current ? ColorScheme.Focus : ColorScheme.Normal); + + // The help string + var l = item.Help.Length; + Move (Frame.Width - l - 2, 1 + i); + Driver.AddStr (item.Help); + } + PositionCursor (); + } + + public override void PositionCursor () + { + if (!host.isMenuClosed) + Move (2, 1 + current); + else + host.PositionCursor (); + } + + void Run (Action action) + { + if (action == null) + return; + + Application.UngrabMouse (); + host.CloseAllMenus (); + Application.Refresh (); + + Application.MainLoop.AddIdle (() => { + action (); + return false; + }); + } + + public override bool ProcessKey (KeyEvent kb) + { + bool disabled; + switch (kb.Key) { + case Key.CursorUp: + if (current == -1) + break; + do { + disabled = false; + current--; + if (host.UseKeysUpDownAsKeysLeftRight) { + if (current == -1 && barItems.Children [current + 1].IsFromSubMenu && host.selectedSub > -1) { + current++; + host.PreviousMenu (true); + break; + } + } + if (current < 0) + current = barItems.Children.Length - 1; + var item = barItems.Children [current]; + if (item == null || !item.IsEnabled ()) disabled = true; + } while (barItems.Children [current] == null || disabled); + SetNeedsDisplay (); + break; + case Key.CursorDown: + do { + current++; + disabled = false; + if (current == barItems.Children.Length) + current = 0; + var item = barItems.Children [current]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (host.UseKeysUpDownAsKeysLeftRight && barItems.Children [current]?.SubMenu != null && + !disabled && !host.isMenuClosed) { + CheckSubMenu (); + break; + } + if (host.isMenuClosed) + host.OpenMenu (host.selected); + } while (barItems.Children [current] == null || disabled); + SetNeedsDisplay (); + break; + case Key.CursorLeft: + host.PreviousMenu (true); + break; + case Key.CursorRight: + host.NextMenu (barItems.Children [current].IsFromSubMenu ? true : false); + break; + case Key.Esc: + Application.UngrabMouse (); + host.CloseAllMenus (); + break; + case Key.Enter: + CheckSubMenu (); + Run (barItems.Children [current].Action); + break; + default: + // TODO: rune-ify + if (Char.IsLetterOrDigit ((char)kb.KeyValue)) { + var x = Char.ToUpper ((char)kb.KeyValue); + + foreach (var item in barItems.Children) { + if (item == null) continue; + if (item.IsEnabled () && item.HotKey == x) { + host.CloseMenu (); + Run (item.Action); + return true; + } + } + } + break; + } + return true; + } + + public override bool MouseEvent(MouseEvent me) + { + if (!host.handled && !host.HandleGrabView (me, this)) { + return false; + } + host.handled = false; + bool disabled; + if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1Released) { + disabled = false; + if (me.Y < 1) + return true; + var meY = me.Y - 1; + if (meY >= barItems.Children.Length) + return true; + var item = barItems.Children [meY]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (item != null && !disabled) + Run (barItems.Children [meY].Action); + return true; + } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.ReportMousePosition) { + disabled = false; + if (me.Y < 1) + return true; + if (me.Y - 1 >= barItems.Children.Length) + return true; + var item = barItems.Children [me.Y - 1]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (item != null && !disabled) + current = me.Y - 1; + HasFocus = true; + SetNeedsDisplay (); + CheckSubMenu (); + return true; + } + return false; + } + + internal void CheckSubMenu () + { + if (barItems.Children [current] == null) + return; + var subMenu = barItems.Children [current].SubMenu; + if (subMenu != null) { + int pos = -1; + if (host.openSubMenu != null) + pos = host.openSubMenu.FindIndex (o => o?.barItems == subMenu); + host.Activate (host.selected, pos, subMenu); + } else if (host.openSubMenu != null && !barItems.Children [current].IsFromSubMenu) + host.CloseMenu (false, true); + } + + int GetSubMenuIndex (MenuBarItem subMenu) + { + int pos = -1; + if (this != null && Subviews.Count > 0) { + Menu v = null; + foreach (var menu in Subviews) { + if (((Menu)menu).barItems == subMenu) + v = (Menu)menu; + } + if (v != null) + pos = Subviews.IndexOf (v); + } + + return pos; + } + } + + + + /// + /// A menu bar for your application. + /// + public class MenuBar : View { + /// + /// The menus that were defined when the menubar was created. This can be updated if the menu is not currently visible. + /// + /// The menu array. + public MenuBarItem [] Menus { get; set; } + internal int selected; + internal int selectedSub; + Action action; + + /// + /// Used for change the navigation key style. + /// + public bool UseKeysUpDownAsKeysLeftRight { get; set; } = true; + + /// + /// Initializes a new instance of the class with the specified set of toplevel menu items. + /// + /// Individual menu items, if one of those contains a null, then a separator is drawn. + public MenuBar (MenuBarItem [] menus) : base () + { + X = 0; + Y = 0; + Width = Dim.Fill (); + Height = 1; + Menus = menus; + CanFocus = false; + selected = -1; + selectedSub = -1; + ColorScheme = Colors.Menu; + WantMousePositionReports = true; + isMenuClosed = true; + } + + public override void Redraw (Rect region) + { + Move (0, 0); + Driver.SetAttribute (Colors.Menu.Normal); + for (int i = 0; i < Frame.Width; i++) + Driver.AddRune (' '); + + Move (1, 0); + int pos = 1; + + for (int i = 0; i < Menus.Length; i++) { + var menu = Menus [i]; + Move (pos, 0); + Attribute hotColor, normalColor; + if (i == selected) { + hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal; + normalColor = i == selected ? ColorScheme.Focus : ColorScheme.Normal; + } else { + hotColor = Colors.Base.Focus; + normalColor = Colors.Base.Focus; + } + DrawHotString (" " + menu.Title + " " + " ", hotColor, normalColor); + pos += menu.TitleLength + 3; + } + PositionCursor (); + } + + public override void PositionCursor () + { + int pos = 0; + for (int i = 0; i < Menus.Length; i++) { + if (i == selected) { + pos++; + if (!isMenuClosed) + Move (pos, 0); + else + Move (pos + 1, 0); + return; + } else { + if (!isMenuClosed) + pos += Menus [i].TitleLength + 4; + else + pos += 2 + Menus [i].TitleLength + 1; + } + } + Move (0, 0); + } + + void Selected (MenuItem item) + { + // TODO: Running = false; + action = item.Action; + } + + public event EventHandler OnOpenMenu; + internal Menu openMenu; + Menu openCurrentMenu; + internal List openSubMenu; + View previousFocused; + internal bool isMenuOpening; + internal bool isMenuClosing; + internal bool isMenuClosed; + View lastFocused; + + /// + /// Get the lasted focused view before open the menu. + /// + public View LastFocused { get; private set; } + + internal void OpenMenu (int index, int sIndex = -1, MenuBarItem subMenu = null) + { + isMenuOpening = true; + OnOpenMenu?.Invoke (this, null); + int pos = 0; + switch (subMenu) { + case null: + lastFocused = lastFocused ?? SuperView.MostFocused; + if (openSubMenu != null) + CloseMenu (false, true); + if (openMenu != null) + SuperView.Remove (openMenu); + + for (int i = 0; i < index; i++) + pos += Menus [i].Title.Length + 2; + openMenu = new Menu (this, pos, 1, Menus [index]); + openCurrentMenu = openMenu; + openCurrentMenu.previousSubFocused = openMenu; + SuperView.Add (openMenu); + SuperView.SetFocus (openMenu); + break; + default: + if (openSubMenu == null) + openSubMenu = new List (); + if (sIndex > -1) { + RemoveSubMenu (sIndex); + } else { + var last = openSubMenu.Count > 0 ? openSubMenu.Last () : openMenu; + openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu); + openCurrentMenu.previousSubFocused = last.previousSubFocused; + openSubMenu.Add (openCurrentMenu); + SuperView.Add (openCurrentMenu); + } + selectedSub = openSubMenu.Count - 1; + SuperView?.SetFocus (openCurrentMenu); + break; + } + isMenuOpening = false; + isMenuClosed = false; + } + + // Starts the menu from a hotkey + void StartMenu () + { + if (openMenu != null) + return; + selected = 0; + SetNeedsDisplay (); + + previousFocused = SuperView.Focused; + OpenMenu (selected); + Application.GrabMouse (this); + } + + // Activates the menu, handles either first focus, or activating an entry when it was already active + // For mouse events. + internal void Activate (int idx, int sIdx = -1, MenuBarItem subMenu = null) + { + selected = idx; + selectedSub = sIdx; + if (openMenu == null) + previousFocused = SuperView.Focused; + + OpenMenu (idx, sIdx, subMenu); + SetNeedsDisplay (); + } + + internal void CloseMenu (bool reopen = false, bool isSubMenu = false) + { + isMenuClosing = true; + switch (isSubMenu) { + case false: + if (openMenu != null) + SuperView.Remove (openMenu); + SetNeedsDisplay (); + if (previousFocused != null && openMenu != null && previousFocused.ToString () != openCurrentMenu.ToString ()) + previousFocused?.SuperView?.SetFocus (previousFocused); + openMenu = null; + if (lastFocused is Menu) { + lastFocused = null; + } + LastFocused = lastFocused; + lastFocused = null; + if (LastFocused != null) { + if (!reopen) + selected = -1; + LastFocused.SuperView?.SetFocus (LastFocused); + } else { + SuperView.SetFocus (this); + isMenuClosed = true; + PositionCursor (); + } + isMenuClosed = true; + break; + + case true: + selectedSub = -1; + SetNeedsDisplay (); + RemoveAllOpensSubMenus (); + openCurrentMenu.previousSubFocused?.SuperView?.SetFocus (openCurrentMenu.previousSubFocused); + openSubMenu = null; + break; + } + isMenuClosing = false; + } + + void RemoveSubMenu (int index) + { + if (openSubMenu == null) + return; + for (int i = openSubMenu.Count - 1; i > index; i--) { + isMenuClosing = true; + if (openSubMenu.Count - 1 > 0) + SuperView.SetFocus (openSubMenu [i - 1]); + else + SuperView.SetFocus (openMenu); + if (openSubMenu != null) { + SuperView.Remove (openSubMenu [i]); + openSubMenu.Remove (openSubMenu [i]); + } + RemoveSubMenu (i); + } + if (openSubMenu.Count > 0) + openCurrentMenu = openSubMenu.Last (); + + //if (openMenu.Subviews.Count == 0) + // return; + //if (index == 0) { + // //SuperView.SetFocus (previousSubFocused); + // FocusPrev (); + // return; + //} + + //for (int i = openMenu.Subviews.Count - 1; i > index; i--) { + // isMenuClosing = true; + // if (openMenu.Subviews.Count - 1 > 0) + // SuperView.SetFocus (openMenu.Subviews [i - 1]); + // else + // SuperView.SetFocus (openMenu); + // if (openMenu != null) { + // Remove (openMenu.Subviews [i]); + // openMenu.Remove (openMenu.Subviews [i]); + // } + // RemoveSubMenu (i); + //} + isMenuClosing = false; + } + + internal void RemoveAllOpensSubMenus () + { + if (openSubMenu != null) { + foreach (var item in openSubMenu) { + SuperView.Remove (item); + } + } + } + + internal void CloseAllMenus () + { + if (!isMenuOpening && !isMenuClosing) { + if (openSubMenu != null) + CloseMenu (false, true); + CloseMenu (); + if (LastFocused != null && LastFocused != this) + selected = -1; + } + isMenuClosed = true; + } + + View FindDeepestMenu (View view, ref int count) + { + count = count > 0 ? count : 0; + foreach (var menu in view.Subviews) { + if (menu is Menu) { + count++; + return FindDeepestMenu ((Menu)menu, ref count); + } + } + return view; + } + + internal void PreviousMenu (bool isSubMenu = false) + { + switch (isSubMenu) { + case false: + if (selected <= 0) + selected = Menus.Length - 1; + else + selected--; + + if (selected > -1) + CloseMenu (true, false); + OpenMenu (selected); + break; + case true: + if (selectedSub > -1) { + selectedSub--; + RemoveSubMenu (selectedSub); + SetNeedsDisplay (); + } else + PreviousMenu (); + + break; + } + } + + internal void NextMenu (bool isSubMenu = false) + { + switch (isSubMenu) { + case false: + if (selected == -1) + selected = 0; + else if (selected + 1 == Menus.Length) + selected = 0; + else + selected++; + + if (selected > -1) + CloseMenu (true); + OpenMenu (selected); + break; + case true: + if (UseKeysUpDownAsKeysLeftRight) { + CloseMenu (false, true); + NextMenu (); + } else { + if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && openCurrentMenu.barItems.Children [openCurrentMenu.current].SubMenu == null) { + if (openSubMenu != null) + CloseMenu (false, true); + NextMenu (); + } else if (openCurrentMenu.barItems.Children [openCurrentMenu.current].SubMenu != null || + !openCurrentMenu.barItems.Children [openCurrentMenu.current].IsFromSubMenu) + selectedSub++; + else + return; + SetNeedsDisplay (); + openCurrentMenu.CheckSubMenu (); + } + break; + } + } + + internal bool FindAndOpenMenuByHotkey(KeyEvent kb) + { + int pos = 0; var c = ((uint)kb.Key & (uint)Key.CharMask); - for (int i = 0; i < Menus.Length; i++) - { - // TODO: this code is duplicated, hotkey should be part of the MenuBarItem - var mi = Menus[i]; - int p = mi.Title.IndexOf('_'); - if (p != -1 && p + 1 < mi.Title.Length) { + for (int i = 0; i < Menus.Length; i++) + { + // TODO: this code is duplicated, hotkey should be part of the MenuBarItem + var mi = Menus[i]; + int p = mi.Title.IndexOf('_'); + if (p != -1 && p + 1 < mi.Title.Length) { if (mi.Title[p + 1] == c) { - OpenMenu(i); - return true; - } - } + Application.GrabMouse (this); + selected = i; + OpenMenu (i); + return true; + } + } } - return false; + return false; } - public override bool ProcessHotKey (KeyEvent kb) - { - if (kb.Key == Key.F9) { - StartMenu (); - return true; - } - - if (kb.IsAlt) - { - if (FindAndOpenMenuByHotkey(kb)) return true; - } - var kc = kb.KeyValue; - - return base.ProcessHotKey (kb); - } - - public override bool ProcessKey (KeyEvent kb) - { - switch (kb.Key) { - case Key.CursorLeft: - selected--; - if (selected < 0) - selected = Menus.Length - 1; - break; - case Key.CursorRight: - selected = (selected + 1) % Menus.Length; - break; - - case Key.Esc: - case Key.ControlC: - //TODO: Running = false; - break; - - default: - var key = kb.KeyValue; - if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) { - char c = Char.ToUpper ((char)key); - - if (Menus [selected].Children == null) - return false; - - foreach (var mi in Menus [selected].Children) { - int p = mi.Title.IndexOf ('_'); - if (p != -1 && p + 1 < mi.Title.Length) { - if (mi.Title [p + 1] == c) { - Selected (mi); - return true; - } - } - } - } - - return false; - } - SetNeedsDisplay (); - return true; - } - - public override bool MouseEvent(MouseEvent me) - { - if (me.Flags == MouseFlags.Button1Clicked) { - int pos = 1; - int cx = me.X; - for (int i = 0; i < Menus.Length; i++) { - if (cx > pos && me.X < pos + 1 + Menus [i].TitleLength) { - Activate (i); - return true; - } - pos += 2 + Menus [i].TitleLength + 1; - } - } - return false; - } - } - -} + public override bool ProcessHotKey (KeyEvent kb) + { + if (kb.Key == Key.F9) { + StartMenu (); + return true; + } + + if (kb.IsAlt) + { + if (FindAndOpenMenuByHotkey(kb)) return true; + } + var kc = kb.KeyValue; + + return base.ProcessHotKey (kb); + } + + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.CursorLeft: + selected--; + if (selected < 0) + selected = Menus.Length - 1; + break; + case Key.CursorRight: + selected = (selected + 1) % Menus.Length; + break; + + case Key.Esc: + case Key.ControlC: + //TODO: Running = false; + CloseMenu (); + break; + + default: + var key = kb.KeyValue; + if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) { + char c = Char.ToUpper ((char)key); + + if (Menus [selected].Children == null) + return false; + + foreach (var mi in Menus [selected].Children) { + int p = mi.Title.IndexOf ('_'); + if (p != -1 && p + 1 < mi.Title.Length) { + if (mi.Title [p + 1] == c) { + Selected (mi); + return true; + } + } + } + } + + return false; + } + SetNeedsDisplay (); + return true; + } + + public override bool MouseEvent(MouseEvent me) + { + if (!handled && !HandleGrabView (me, this)) { + return false; + } + handled = false; + + if (me.Flags == MouseFlags.Button1Clicked || + (me.Flags == MouseFlags.ReportMousePosition && selected > -1)) { + int pos = 1; + int cx = me.X; + for (int i = 0; i < Menus.Length; i++) { + if (cx > pos && me.X < pos + 1 + Menus [i].TitleLength) { + if (selected == i && me.Flags == MouseFlags.Button1Clicked && !isMenuClosed) { + Application.UngrabMouse (); + CloseMenu (); + } else if (me.Flags == MouseFlags.Button1Clicked && isMenuClosed) { + Activate (i); + } + else if (selected != i && selected > -1 && me.Flags == MouseFlags.ReportMousePosition) { + if (!isMenuClosed) { + CloseMenu (); + Activate (i); + } + } else { + if (!isMenuClosed) + Activate (i); + } + return true; + } + pos += 2 + Menus [i].TitleLength + 1; + } + } + return false; + } + + internal bool handled; + + internal bool HandleGrabView (MouseEvent me, View current) + { + if (Application.mouseGrabView != null) { + if (me.View is MenuBar || me.View is Menu) { + if(me.View != current) { + Application.UngrabMouse (); + Application.GrabMouse (me.View); + me.View.MouseEvent (me); + } + } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags.HasFlag (MouseFlags.Button1Clicked)) { + Application.UngrabMouse (); + CloseAllMenus (); + handled = false; + return false; + } else { + handled = false; + return false; + } + } else if (isMenuClosed && me.Flags.HasFlag (MouseFlags.Button1Clicked)) { + Application.GrabMouse (current); + } else { + handled = false; + return false; + } + //if (me.View != this && me.Flags != MouseFlags.Button1Clicked) + // return true; + //else if (me.View != this && me.Flags == MouseFlags.Button1Clicked) { + // Application.UngrabMouse (); + // host.CloseAllMenus (); + // return true; + //} + + + //if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Clicked) + // return false; + + //if (Application.mouseGrabView != null) { + // if (me.View is MenuBar || me.View is Menu) { + // me.X -= me.OfX; + // me.Y -= me.OfY; + // me.View.MouseEvent (me); + // return true; + // } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Clicked) { + // Application.UngrabMouse (); + // CloseAllMenus (); + // } + //} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Clicked) { + // Application.GrabMouse (this); + // return true; + //} + + //if (Application.mouseGrabView != null) { + // if (Application.mouseGrabView == me.View && me.View == current) { + // me.X -= me.OfX; + // me.Y -= me.OfY; + // } else if (me.View != current && me.View is MenuBar && me.View is Menu) { + // Application.UngrabMouse (); + // Application.GrabMouse (me.View); + // } else if (me.Flags == MouseFlags.Button1Clicked) { + // Application.UngrabMouse (); + // CloseMenu (); + // } + //} else if ((!isMenuClosed && selected > -1)) { + // Application.GrabMouse (current); + //} + + handled = true; + + return true; + } + } + +} diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index ab37832fd..f28496c92 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -42,15 +42,20 @@ namespace Terminal.Gui { /// The item to be selected, the value is clamped to the number of items. public RadioGroup (string [] radioLabels, int selected = 0) : base () { - var r = MakeRect (0, 0, radioLabels); - Width = r.Width; - Height = radioLabels.Length; + SetWidthHeight(radioLabels); this.selected = selected; this.radioLabels = radioLabels; CanFocus = true; } + void SetWidthHeight(string[] radioLabels) + { + var r = MakeRect(0, 0, radioLabels); + Width = r.Width; + Height = radioLabels.Length; + } + static Rect MakeRect (int x, int y, string [] radioLabels) { int width = 0; @@ -81,6 +86,7 @@ namespace Terminal.Gui { public string [] RadioLabels { get => radioLabels; set { + Update(value); radioLabels = value; selected = 0; cursor = 0; @@ -88,6 +94,18 @@ namespace Terminal.Gui { } } + void Update(string [] newRadioLabels) + { + for (int i = 0; i < radioLabels.Length; i++) { + Move(0, i); + Driver.SetAttribute(ColorScheme.Normal); + Driver.AddStr(new string(' ', radioLabels[i].Length + 4)); + } + if (newRadioLabels.Length != radioLabels.Length) { + SetWidthHeight(newRadioLabels); + } + } + public override void Redraw (Rect region) { base.Redraw (region); diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index ce779706b..318fba3df 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -355,6 +355,7 @@ namespace Terminal.Gui { public override void Redraw(Rect region) { + SetViewsNeedsDisplay (); var oldClip = ClipToBounds (); Driver.SetAttribute (ColorScheme.Normal); Clear (); @@ -363,6 +364,13 @@ namespace Terminal.Gui { Driver.SetAttribute (ColorScheme.Normal); } + void SetViewsNeedsDisplay () + { + foreach (View view in contentView) { + view.SetNeedsDisplay (); + } + } + public override void PositionCursor() { if (InternalSubviews.Count == 0) diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs new file mode 100644 index 000000000..adb619c28 --- /dev/null +++ b/Terminal.Gui/Views/StatusBar.cs @@ -0,0 +1,126 @@ +// +// StatusBar.cs: a statusbar for an application +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// TODO: +// Add mouse support +// Uses internals of Application +using System; +using NStack; + +namespace Terminal.Gui +{ + /// + /// A statusbar item has a title, a shortcut aka hotkey, and an action to execute on activation. + /// Such an item is ment to be as part of the global hotkeys of the application, which are available in the current context of the screen. + /// The colour of the text will be changed after each ~. Having an statusbar item with a text of `~F1~ Help` will draw *F1* as shortcut and + /// *Help* as standard text. + /// + public class StatusItem + { + /// + /// Initializes a new . + /// + /// Shortcut to activate the item. + /// Title for the statusbar item. + /// Action to invoke when the staturbar item is activated. + public StatusItem(Key shortcut, ustring title, Action action) + { + Title = title ?? ""; + Shortcut = shortcut; + Action = action; + } + + /// + /// This is the global setting that can be used as a global shortcut to invoke the action on the menu. + /// + public Key Shortcut { get; } + + /// + /// Gets or sets the title. + /// + /// The title. + public ustring Title { get; } + + /// + /// Gets or sets the action to be invoked when the statusbar item is triggered + /// + /// Method to invoke. + public Action Action { get; } + }; + + /// + /// A statusbar for your application. + /// The statusbar should be context sensitive. This means, if the main menu and an open text editor are visible, the items probably shown will + /// be ~F1~ Help ~F2~ Save ~F3~ Load. While a dialog to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. + /// So for each context must be a new instance of a statusbar. + /// + public class StatusBar : View + { + public StatusItem [] Items { get; set; } + + /// + /// Initializes a new instance of the class with the specified set of statusbar items. + /// It will be drawn in the lowest column of the terminal. + /// + /// A list of statusbar items. + public StatusBar(StatusItem [] items) : base() + { + X = 0; + Y = Application.Driver.Rows - 1; // TODO: using internals of Application + Width = Dim.Fill (); + Height = 1; + Items = items; + CanFocus = false; + ColorScheme = Colors.Menu; + } + + Attribute ToggleScheme(Attribute scheme) + { + var result = scheme==ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; + Driver.SetAttribute(result); + return result; + } + + public override void Redraw(Rect region) { + if (Frame.Y != Driver.Rows - 1) { + Frame = new Rect (Frame.X, Driver.Rows - 1, Frame.Width, Frame.Height); + Y = Driver.Rows - 1; + SetNeedsDisplay (); + } + + Move (0, 0); + Driver.SetAttribute (ColorScheme.Normal); + for (int i = 0; i < Frame.Width; i++) + Driver.AddRune (' '); + + Move (1, 0); + var scheme = ColorScheme.Normal; + Driver.SetAttribute(scheme); + for(int i=0; iInitial text contents. public TextField (ustring text) { - if (text == null) - text = ""; - - this.text = TextModel.ToRunes (text); - point = text.Length; - CanFocus = true; + Initialize (text, Frame.Width); } /// @@ -68,6 +63,11 @@ namespace Terminal.Gui { /// The width. /// Initial text contents. public TextField (int x, int y, int w, ustring text) : base (new Rect (x, y, w, 1)) + { + Initialize (text, w); + } + + void Initialize (ustring text, int w) { if (text == null) text = ""; @@ -76,6 +76,15 @@ namespace Terminal.Gui { point = text.Length; first = point > w ? point - w : 0; CanFocus = true; + Used = true; + WantMousePositionReports = true; + OnLeave += TextField_OnLeave; + } + + void TextField_OnLeave (object sender, EventArgs e) + { + if (Application.mouseGrabView != null && Application.mouseGrabView == this) + Application.UngrabMouse (); } public override Rect Frame { @@ -98,9 +107,9 @@ namespace Terminal.Gui { } set { - ustring oldText = ustring.Make(text); + ustring oldText = ustring.Make (text); text = TextModel.ToRunes (value); - Changed?.Invoke(this, oldText); + Changed?.Invoke (this, oldText); if (point > text.Count) point = Math.Max (text.Count-1, 0); @@ -137,7 +146,7 @@ namespace Terminal.Gui { public override void PositionCursor () { var col = 0; - for (int idx = first; idx < text.Count; idx++) { + for (int idx = first < 0 ? 0 : first; idx < text.Count; idx++) { if (idx == point) break; var cols = Rune.ColumnWidth (text [idx]); @@ -148,6 +157,9 @@ namespace Terminal.Gui { public override void Redraw (Rect region) { + ColorScheme color = Colors.Menu; + SetSelectedStartSelectedLength (); + Driver.SetAttribute (ColorScheme.Focus); Move (0, 0); @@ -160,11 +172,16 @@ namespace Terminal.Gui { if (idx < first) continue; var cols = Rune.ColumnWidth (rune); + if (col == point && HasFocus && !Used && SelectedLength == 0) + Driver.SetAttribute (Colors.Menu.HotFocus); + else + Driver.SetAttribute (idx >= start && length > 0 && idx < start + length ? color.Focus : ColorScheme.Focus); if (col + cols < width) Driver.AddRune ((Rune)(Secret ? '*' : rune)); col += cols; } + Driver.SetAttribute (ColorScheme.Focus); for (int i = col; i < Frame.Width; i++) Driver.AddRune (' '); @@ -175,9 +192,9 @@ namespace Terminal.Gui { int DisplaySize (List t, int start) { int size = 0; - int tcount = text.Count; + int tcount = t.Count; for (int i = start; i < tcount; i++) { - var rune = text [i]; + var rune = t [i]; size += Rune.ColumnWidth (rune); } return size; @@ -195,7 +212,7 @@ namespace Terminal.Gui { void SetText (List newText) { - text = newText; + Text = ustring.Make (newText); } void SetText (IEnumerable newText) @@ -217,39 +234,50 @@ namespace Terminal.Gui { public override bool ProcessKey (KeyEvent kb) { // remember current cursor position - // because the new calculated cursor position is needed to be set BEFORE the change event is triggert + // because the new calculated cursor position is needed to be set BEFORE the change event is triggest // Needed for the Elmish Wrapper issue https://github.com/DieselMeister/Terminal.Gui.Elmish/issues/2 var oldCursorPos = point; - + switch (kb.Key) { case Key.DeleteChar: case Key.ControlD: - if (text.Count == 0 || text.Count== point) - return true; + if (SelectedLength == 0) { + if (text.Count == 0 || text.Count == point) + return true; - SetText (text.GetRange (0, point).Concat (text.GetRange (point + 1, text.Count - (point + 1)))); - Adjust (); + SetText (text.GetRange (0, point).Concat (text.GetRange (point + 1, text.Count - (point + 1)))); + Adjust (); + + } else { + DeleteSelectedText (); + } break; case Key.Delete: case Key.Backspace: - if (point == 0) - return true; + if (SelectedLength == 0) { + if (point == 0) + return true; - point--; - SetText(text.GetRange(0, oldCursorPos - 1).Concat(text.GetRange(oldCursorPos, text.Count - (oldCursorPos)))); - Adjust (); + point--; + SetText (text.GetRange (0, oldCursorPos - 1).Concat (text.GetRange (oldCursorPos, text.Count - (oldCursorPos)))); + Adjust (); + } else { + DeleteSelectedText (); + } break; // Home, C-A case Key.Home: case Key.ControlA: + ClearAllSelection (); point = 0; Adjust (); break; case Key.CursorLeft: case Key.ControlB: + ClearAllSelection (); if (point > 0) { point--; Adjust (); @@ -258,12 +286,14 @@ namespace Terminal.Gui { case Key.End: case Key.ControlE: // End + ClearAllSelection (); point = text.Count; Adjust (); break; case Key.CursorRight: case Key.ControlF: + ClearAllSelection (); if (point == text.Count) break; point++; @@ -271,6 +301,7 @@ namespace Terminal.Gui { break; case Key.ControlK: // kill-to-end + ClearAllSelection (); if (point >= text.Count) return true; SetClipboard (text.GetRange (point, text.Count - point)); @@ -296,6 +327,7 @@ namespace Terminal.Gui { break; case (Key)((int)'b' + Key.AltMask): + ClearAllSelection (); int bw = WordBackward (point); if (bw != -1) point = bw; @@ -303,40 +335,59 @@ namespace Terminal.Gui { break; case (Key)((int)'f' + Key.AltMask): + ClearAllSelection (); int fw = WordForward (point); if (fw != -1) point = fw; Adjust (); break; - // MISSING: - // Alt-D, Alt-backspace - // Alt-Y - // Delete adding to kill buffer + case Key.AltMask | Key.ControlI: + Used = !Used; + SetNeedsDisplay (); + break; + + case Key.AltMask | Key.ControlC: + Copy (); + break; + + case Key.AltMask | Key.ControlX: + Cut (); + break; + + case Key.AltMask | Key.ControlV: + Paste (); + break; + + // MISSING: + // Alt-D, Alt-backspace + // Alt-Y + // Delete adding to kill buffer default: // Ignore other control characters. if (kb.Key < Key.Space || kb.Key > Key.CharMask) return false; + if (SelectedLength != 0) { + DeleteSelectedText (); + oldCursorPos = point; + } var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key)); if (used) { point++; if (point == text.Count) { SetText (text.Concat (kbstr).ToList ()); } else { - SetText(text.GetRange(0, oldCursorPos).Concat(kbstr).Concat(text.GetRange(oldCursorPos, text.Count - oldCursorPos))); - } + SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (oldCursorPos, Math.Min (text.Count - oldCursorPos, text.Count)))); + } } else { - point = 1; - SetText (kbstr); - first = 0; + SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (Math.Min (oldCursorPos + 1, text.Count), Math.Max (text.Count - oldCursorPos - 1, 0)))); + point++; } - used = true; Adjust (); return true; } - used = true; return true; } @@ -402,25 +453,156 @@ namespace Terminal.Gui { return -1; } - public override bool MouseEvent (MouseEvent ev) + /// + /// Start position of the selected text. + /// + public int SelectedStart { get; set; } = -1; + + /// + /// Length of the selected text. + /// + public int SelectedLength { get; set; } = 0; + + /// + /// The selected text. + /// + public ustring SelectedText { get; set; } + + int start, length; + bool isButtonReleased = true; + + public override bool MouseEvent (MouseEvent ev) { - if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) + if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked) && !ev.Flags.HasFlag (MouseFlags.Button1Pressed) && + !ev.Flags.HasFlag (MouseFlags.ReportMousePosition)) return false; - if (!HasFocus) - SuperView.SetFocus (this); - - // We could also set the cursor position. - point = first + ev.X; - if (point > text.Count) - point = text.Count; - if (point < first) - point = 0; + if (ev.Flags == MouseFlags.Button1Clicked) { + if (!HasFocus) + SuperView.SetFocus (this); + int x = PositionCursor (ev); + if (isButtonReleased) + ClearAllSelection (); + isButtonReleased = true; + Application.UngrabMouse (); + } else if (ev.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + int x = PositionCursor (ev); + isButtonReleased = false; + PrepareSelection (x); + if (Application.mouseGrabView == null) { + Application.GrabMouse (this); + } + } else if (ev.Flags == MouseFlags.Button1Pressed) { + int x = PositionCursor (ev); + if (SelectedLength != 0) + ClearAllSelection (); + } SetNeedsDisplay (); return true; } + + int PositionCursor (MouseEvent ev) + { + // We could also set the cursor position. + int x; + if (Application.mouseGrabView == null) { + x = ev.X; + } else { + x = ev.X;// - (text.Count > Frame.Width ? text.Count - Frame.Width : 0); + } + + point = first + x; + if (point > text.Count) + point = text.Count; + if (point < first) + point = 0; + return x; + } + + void PrepareSelection (int x) + { + x = x + first < 0 ? 0 : x + first; + SelectedStart = SelectedStart == -1 && text.Count > 0 && x >= 0 && x <= text.Count ? x : SelectedStart; + if (SelectedStart > -1) { + SelectedLength = x <= text.Count ? x - SelectedStart : text.Count - SelectedStart; + SetSelectedStartSelectedLength (); + SelectedText = length > 0 ? ustring.Make (text).ToString ().Substring ( + start < 0 ? 0 : start, length > text.Count ? text.Count : length) : ""; + } + Adjust (); + } + + /// + /// Clear the selected text. + /// + public void ClearAllSelection () + { + if (SelectedLength == 0) + return; + SelectedStart = -1; + SelectedLength = 0; + SelectedText = ""; + start = 0; + } + + void SetSelectedStartSelectedLength () + { + if (SelectedLength < 0) { + start = SelectedLength + SelectedStart; + length = Math.Abs (SelectedLength); + } else { + start = SelectedStart; + length = SelectedLength; + } + } + + /// + /// Copy the selected text to the clipboard. + /// + public void Copy () + { + if (SelectedLength != 0) { + Clipboard.Contents = SelectedText; + } + } + + /// + /// Cut the selected text to the clipboard. + /// + public void Cut () + { + if (SelectedLength != 0) { + Clipboard.Contents = SelectedText; + DeleteSelectedText (); + } + } + + void DeleteSelectedText () + { + string actualText = Text.ToString (); + int selStart = SelectedLength < 0 ? SelectedLength + SelectedStart : SelectedStart; + int selLength = Math.Abs (SelectedLength); + Text = actualText.Substring (0, selStart) + + actualText.Substring (selStart + selLength, actualText.Length - selStart - selLength); + ClearAllSelection (); + CursorPosition = selStart >= Text.Length ? Text.Length : selStart; + SetNeedsDisplay (); + } + + /// + /// Paste the selected text from the clipboard. + /// + public void Paste () + { + string actualText = Text.ToString (); + int start = SelectedStart == -1 ? CursorPosition : SelectedStart; + Text = actualText.Substring (0, start) + + Clipboard.Contents?.ToString () + + actualText.Substring (start + SelectedLength, actualText.Length - start - SelectedLength); + SelectedLength = 0; + SetNeedsDisplay (); + } + } - - } diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index cacfa944c..0afc74826 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -50,7 +50,7 @@ namespace Terminal.Gui { Changed += TimeField_Changed; } - private void TimeField_Changed (object sender, ustring e) + void TimeField_Changed (object sender, ustring e) { if (!DateTime.TryParseExact (Text.ToString (), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) Text = e; diff --git a/Terminal.Gui/packages.config b/Terminal.Gui/packages.config index 7c57b8167..72fb5fb96 100644 --- a/Terminal.Gui/packages.config +++ b/Terminal.Gui/packages.config @@ -1,5 +1,4 @@ - - + diff --git a/packages.config b/packages.config index e18eee05a..72fb5fb96 100644 --- a/packages.config +++ b/packages.config @@ -1,4 +1,4 @@ - +