From 11445abf7896467047b22a8c8504376b82d1d6f4 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Sat, 18 Apr 2020 11:11:57 -0600 Subject: [PATCH 01/18] Gitattributes (#383) * Added .gitattributes to deal with cross platform line endings correctly --- .gitattributes | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..7818f5079 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Set the default behavior for all files. +* text=auto + +# Normalized and converts to +# native line endings on checkout. +*.cs text + +# Convert to CRLF line endings on checkout. +*.sln text eol=crlf + +# Convert to LF line endings on checkout. +*.sh text eol=lf + +# Binary files. +*.png binary +*.jpg binary \ No newline at end of file From 230b7ab70b5ec122886806406eea68cc5ab3fe84 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 18 Apr 2020 18:13:25 +0100 Subject: [PATCH 02/18] Fixes #389 Window layouting breaks when resizing. (#390) Fixed #389 horizontal resizing issue. Vertical resizing was fine. Posting a complete code to verify these two situations. --- Terminal.Gui/Core.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index b37d76d4a..2546abd65 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -1482,7 +1482,7 @@ namespace Terminal.Gui { { if (this != Application.Top) { EnsureVisibleBounds (this, Frame.X, Frame.Y, out int nx, out int ny); - if (nx != Frame.X || ny != Frame.Y) { + if ((nx != Frame.X || ny != Frame.Y) && LayoutStyle != LayoutStyle.Computed) { X = nx; Y = ny; } @@ -1490,7 +1490,7 @@ namespace Terminal.Gui { 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) { + if ((nx != top.Frame.X || ny != top.Frame.Y) && top.LayoutStyle != LayoutStyle.Computed) { top.X = nx; top.Y = ny; } From c7ce61909212b0c0c60d095f07bebcdd6ca43859 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Sat, 18 Apr 2020 11:21:06 -0600 Subject: [PATCH 03/18] MenuItems on MenuBar (#388) This PR enhances MenuBar with the ability to have top-level (on the MenuBar itself), clickable, actionable, MenuItems. Along the way it fixes a few MenuBar bugs and... alt activates/closes the menu just like on Windows (alt and f9 work the same). Cursor shows consistently across the MenuBar as the focus changes Top-level menu items show accelerator keys. demo.cs has been enhanced to include an example top-level menuitems. I also tested top-level menuitem as the first menu item as well as in the middle. I didn't want to over complicate demo.cs with tons of examples, FWIW. --- Example/demo.cs | 59 +- Terminal.Gui/Core.cs | 82 +- Terminal.Gui/Drivers/ConsoleDriver.cs | 2 +- Terminal.Gui/Drivers/CursesDriver.cs | 3 +- Terminal.Gui/Drivers/NetDriver.cs | 3 +- Terminal.Gui/Drivers/WindowsDriver.cs | 117 +- Terminal.Gui/Event.cs | 8 +- Terminal.Gui/Views/Menu.cs | 2094 +++++++++++++------------ Terminal.Gui/Views/StatusBar.cs | 50 +- 9 files changed, 1333 insertions(+), 1085 deletions(-) diff --git a/Example/demo.cs b/Example/demo.cs index 3cc63d77c..c44b23ae7 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -93,10 +93,10 @@ static class Demo { int i = 0; string txt = "Hello world, how are you doing today"; container.Add ( - new Label(new Rect(0, 1, 40, 3), $"{i+1}-{txt}") { TextAlignment = TextAlignment.Left }, - new Label(new Rect(0, 3, 40, 3), $"{i+2}-{txt}") { TextAlignment = TextAlignment.Right }, - new Label(new Rect(0, 5, 40, 3), $"{i+3}-{txt}") { TextAlignment = TextAlignment.Centered }, - new Label(new Rect(0, 7, 40, 3), $"{i+4}-{txt}") { TextAlignment = TextAlignment.Justified } + new Label (new Rect (0, 1, 40, 3), $"{i+1}-{txt}") { TextAlignment = TextAlignment.Left }, + new Label (new Rect (0, 3, 40, 3), $"{i+2}-{txt}") { TextAlignment = TextAlignment.Right }, + new Label (new Rect (0, 5, 40, 3), $"{i+3}-{txt}") { TextAlignment = TextAlignment.Centered }, + new Label (new Rect (0, 7, 40, 3), $"{i+4}-{txt}") { TextAlignment = TextAlignment.Justified } ); Application.Run (container); @@ -408,6 +408,34 @@ static class Demo { #endregion + #region OnKeyDown / OnKeyUp Demo + private static void OnKeyDownUpDemo () + { + var container = new Dialog ( + "OnKeyDown & OnKeyUp demo", 50, 20, + new Button ("Ok", is_default: true) { Clicked = () => { Application.RequestStop (); } }, + new Button ("Cancel") { Clicked = () => { Application.RequestStop (); } }); + + var kl = new Label (new Rect (3, 3, 40, 1), "Keyboard: "); + container.OnKeyDown += (KeyEvent keyEvent) => KeyUpDown (keyEvent, kl, "Down"); + container.OnKeyUp += (KeyEvent keyEvent) => KeyUpDown (keyEvent, kl, "Up"); + container.Add (kl); + Application.Run (container); + } + + private static void KeyUpDown (KeyEvent keyEvent, Label kl, string updown) + { + kl.TextColor = Colors.TopLevel.Normal; + if ((keyEvent.Key & Key.CtrlMask) != 0) { + kl.Text = $"Keyboard: Ctrl Key{updown}"; + } else if ((keyEvent.Key & Key.AltMask) != 0) { + kl.Text = $"Keyboard: Alt Key{updown}"; + } else { + kl.Text = $"Keyboard: {(char)keyEvent.KeyValue} Key{updown}"; + } + } +#endregion + public static Label ml; public static MenuBar menu; public static CheckBox menuKeysStyle; @@ -430,7 +458,7 @@ static class Demo { X = 1, Y = 1, Width = Dim.Fill (), - Height = Dim.Fill ()-1 + Height = Dim.Fill () - 1 }; #else var tframe = top.Frame; @@ -451,7 +479,7 @@ static class Demo { menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("Text Editor Demo", "", () => { Editor (top); }), + new MenuItem ("Text _Editor Demo", "", () => { Editor (top); }), new MenuItem ("_New", "Creates new file", NewFile), new MenuItem ("_Open", "", Open), new MenuItem ("_Hex", "", () => ShowHex (top)), @@ -469,18 +497,19 @@ static class Demo { menuItems[3] }), new MenuBarItem ("_List Demos", new MenuItem [] { - new MenuItem ("Select Multiple Items", "", () => ListSelectionDemo (true)), - new MenuItem ("Select Single Item", "", () => ListSelectionDemo (false)), + new MenuItem ("Select _Multiple Items", "", () => ListSelectionDemo (true)), + new MenuItem ("Select _Single Item", "", () => ListSelectionDemo (false)), }), - new MenuBarItem ("Assorted", new MenuItem [] { - new MenuItem ("Show text alignments", "", () => ShowTextAlignments ()) + new MenuBarItem ("A_ssorted", new MenuItem [] { + new MenuItem ("_Show text alignments", "", () => ShowTextAlignments ()), + new MenuItem ("_OnKeyDown/Up", "", () => OnKeyDownUpDemo ()) }), - new MenuBarItem ("Test Menu and SubMenus", new MenuItem [] { - new MenuItem ("SubMenu1Item1", + new MenuBarItem ("_Test Menu and SubMenus", new MenuItem [] { + new MenuItem ("SubMenu1Item_1", new MenuBarItem (new MenuItem[] { - new MenuItem ("SubMenu2Item1", + new MenuItem ("SubMenu2Item_1", new MenuBarItem (new MenuItem [] { - new MenuItem ("SubMenu3Item1", + new MenuItem ("SubMenu3Item_1", new MenuBarItem (new MenuItem [] { menuItems [2] }) ) }) @@ -488,6 +517,7 @@ static class Demo { }) ) }), + new MenuBarItem ("_About...", "Demonstrates top-level menu item", () => MessageBox.ErrorQuery (50, 7, "About Demo", "This is a demo app for gui.cs", "Ok")), }); menuKeysStyle = new CheckBox (3, 25, "UseKeysUpDownAsKeysLeftRight", true); @@ -535,6 +565,7 @@ static class Demo { }; #endif + top.Add (win); //top.Add (menu); top.Add (menu, statusBar, ml); diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index 2546abd65..c93af8908 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -121,6 +121,27 @@ namespace Terminal.Gui { return false; } + /// + /// Method invoked when a key is pressed. + /// + /// Contains the details about the key that produced the event. + /// true if the event was handled + public virtual bool KeyDown (KeyEvent keyEvent) + { + return false; + } + + /// + /// Method invoked when a key is released. + /// + /// Contains the details about the key that produced the event. + /// true if the event was handled + public virtual bool KeyUp (KeyEvent keyEvent) + { + return false; + } + + /// /// Method invoked when a mouse event is generated /// @@ -1011,6 +1032,41 @@ namespace Terminal.Gui { return false; } + /// + /// Invoked when a key is pressed + /// + public Action OnKeyDown; + + /// Contains the details about the key that produced the event. + public override bool KeyDown (KeyEvent keyEvent) + { + OnKeyDown?.Invoke (keyEvent); + if (subviews == null || subviews.Count == 0) + return false; + foreach (var view in subviews) + if (view.KeyDown (keyEvent)) + return true; + + return false; + } + + /// + /// Invoked when a key is released + /// + public Action OnKeyUp; + + /// Contains the details about the key that produced the event. + public override bool KeyUp (KeyEvent keyEvent) + { + OnKeyUp?.Invoke (keyEvent); + if (subviews == null || subviews.Count == 0) + return false; + foreach (var view in subviews) + if (view.KeyUp (keyEvent)) + return true; + + return false; + } /// /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, it does nothing. /// @@ -1943,6 +1999,7 @@ namespace Terminal.Gui { static void ProcessKeyEvent (KeyEvent ke) { + var chain = toplevels.ToList(); foreach (var topLevel in chain) { if (topLevel.ProcessHotKey (ke)) @@ -1967,6 +2024,29 @@ namespace Terminal.Gui { } } + static void ProcessKeyDownEvent (KeyEvent ke) + { + var chain = toplevels.ToList (); + foreach (var topLevel in chain) { + if (topLevel.KeyDown (ke)) + return; + if (topLevel.Modal) + break; + } + } + + + static void ProcessKeyUpEvent (KeyEvent ke) + { + var chain = toplevels.ToList (); + foreach (var topLevel in chain) { + if (topLevel.KeyUp (ke)) + return; + if (topLevel.Modal) + break; + } + } + static View FindDeepestView (View start, int x, int y, out int resx, out int resy) { var startFrame = start.Frame; @@ -2092,7 +2172,7 @@ namespace Terminal.Gui { } toplevels.Push (toplevel); Current = toplevel; - Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessMouseEvent); + Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessKeyDownEvent, ProcessKeyUpEvent, ProcessMouseEvent); if (toplevel.LayoutStyle == LayoutStyle.Computed) toplevel.RelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows)); toplevel.LayoutSubviews (); diff --git a/Terminal.Gui/Drivers/ConsoleDriver.cs b/Terminal.Gui/Drivers/ConsoleDriver.cs index 19cdc4e48..9e9511e85 100644 --- a/Terminal.Gui/Drivers/ConsoleDriver.cs +++ b/Terminal.Gui/Drivers/ConsoleDriver.cs @@ -469,7 +469,7 @@ namespace Terminal.Gui { /// /// /// - public abstract void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action mouseHandler); + public abstract void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler); /// /// Updates the screen to reflect all the changes that have been done to the display buffer diff --git a/Terminal.Gui/Drivers/CursesDriver.cs b/Terminal.Gui/Drivers/CursesDriver.cs index 2508051db..c51b37b6f 100644 --- a/Terminal.Gui/Drivers/CursesDriver.cs +++ b/Terminal.Gui/Drivers/CursesDriver.cs @@ -203,8 +203,9 @@ namespace Terminal.Gui { keyHandler (new KeyEvent ((Key)wch)); } - public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action mouseHandler) + public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) { + // Note: Curses doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called Curses.timeout (-1); (mainLoop.Driver as Mono.Terminal.UnixMainLoop).AddWatch (0, Mono.Terminal.UnixMainLoop.Condition.PollIn, x => { diff --git a/Terminal.Gui/Drivers/NetDriver.cs b/Terminal.Gui/Drivers/NetDriver.cs index 0b5217910..188b7b30c 100644 --- a/Terminal.Gui/Drivers/NetDriver.cs +++ b/Terminal.Gui/Drivers/NetDriver.cs @@ -320,8 +320,9 @@ namespace Terminal.Gui { return (Key)(0xffffffff); } - public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action mouseHandler) + public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) { + // Note: Net doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called (mainLoop.Driver as NetMainLoop).WindowsKeyPressed = delegate (ConsoleKeyInfo consoleKey) { var map = MapKey (consoleKey); if (map == (Key)0xffffffff) diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index f48eb6418..1dcaf34fc 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -26,6 +26,7 @@ // SOFTWARE. // using System; +using System.CodeDom; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; @@ -102,7 +103,7 @@ namespace Terminal.Gui { } if (ScreenBuffer != IntPtr.Zero) - CloseHandle(ScreenBuffer); + CloseHandle (ScreenBuffer); ScreenBuffer = IntPtr.Zero; } @@ -358,8 +359,8 @@ namespace Terminal.Gui { [DllImport ("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle (int nStdHandle); - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool CloseHandle(IntPtr handle); + [DllImport ("kernel32.dll", SetLastError = true)] + static extern bool CloseHandle (IntPtr handle); [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] public static extern bool ReadConsoleInput ( @@ -424,8 +425,8 @@ namespace Terminal.Gui { internal class WindowsDriver : ConsoleDriver, Mono.Terminal.IMainLoopDriver { static bool sync; - ManualResetEventSlim eventReady = new ManualResetEventSlim(false); - ManualResetEventSlim waitForProbe = new ManualResetEventSlim(false); + ManualResetEventSlim eventReady = new ManualResetEventSlim (false); + ManualResetEventSlim waitForProbe = new ManualResetEventSlim (false); MainLoop mainLoop; WindowsConsole.CharInfo [] OutputBuffer; int cols, rows; @@ -456,17 +457,17 @@ namespace Terminal.Gui { Task.Run ((Action)WindowsInputHandler); } - [StructLayout(LayoutKind.Sequential)] + [StructLayout (LayoutKind.Sequential)] public struct ConsoleKeyInfoEx { public ConsoleKeyInfo consoleKeyInfo; public bool CapsLock; public bool NumLock; - public ConsoleKeyInfoEx(ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock) + public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock) { - this.consoleKeyInfo = consoleKeyInfo; - CapsLock = capslock; - NumLock = numlock; + this.consoleKeyInfo = consoleKeyInfo; + CapsLock = capslock; + NumLock = numlock; } } @@ -476,8 +477,8 @@ namespace Terminal.Gui { void WindowsInputHandler () { while (true) { - waitForProbe.Wait(); - waitForProbe.Reset(); + waitForProbe.Wait (); + waitForProbe.Reset (); uint numberEventsRead = 0; @@ -487,7 +488,7 @@ namespace Terminal.Gui { else result = records; - eventReady.Set(); + eventReady.Set (); } } @@ -498,7 +499,7 @@ namespace Terminal.Gui { void IMainLoopDriver.Wakeup () { - tokenSource.Cancel(); + tokenSource.Cancel (); } bool IMainLoopDriver.EventsPending (bool wait) @@ -517,31 +518,35 @@ namespace Terminal.Gui { waitTimeout = 0; result = null; - waitForProbe.Set(); + waitForProbe.Set (); try { - if(!tokenSource.IsCancellationRequested) - eventReady.Wait(waitTimeout, tokenSource.Token); + if (!tokenSource.IsCancellationRequested) + eventReady.Wait (waitTimeout, tokenSource.Token); } catch (OperationCanceledException) { return true; } finally { - eventReady.Reset(); + eventReady.Reset (); } if (!tokenSource.IsCancellationRequested) return result != null; - tokenSource.Dispose(); - tokenSource = new CancellationTokenSource(); + tokenSource.Dispose (); + tokenSource = new CancellationTokenSource (); return true; } Action keyHandler; + Action keyDownHandler; + Action keyUpHandler; Action mouseHandler; - public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action mouseHandler) + public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) { this.keyHandler = keyHandler; + this.keyDownHandler = keyDownHandler; + this.keyUpHandler = keyUpHandler; this.mouseHandler = mouseHandler; } @@ -554,12 +559,37 @@ namespace Terminal.Gui { var inputEvent = result [0]; switch (inputEvent.EventType) { case WindowsConsole.EventType.Key: - if (inputEvent.KeyEvent.bKeyDown == false) - return; var map = MapKey (ToConsoleKeyInfoEx (inputEvent.KeyEvent)); - if (inputEvent.KeyEvent.UnicodeChar == 0 && map == (Key)0xffffffff) - return; - keyHandler (new KeyEvent (map)); + if (map == (Key)0xffffffff) { + KeyEvent key; + // Shift = VK_SHIFT = 0x10 + // Ctrl = VK_CONTROL = 0x11 + // Alt = VK_MENU = 0x12 + switch (inputEvent.KeyEvent.wVirtualKeyCode) { + case 0x11: + key = new KeyEvent (Key.CtrlMask); + break; + case 0x12: + key = new KeyEvent (Key.AltMask); + break; + default: + key = new KeyEvent (Key.Unknown); + break; + } + + if (inputEvent.KeyEvent.bKeyDown) + keyDownHandler (key); + else + keyUpHandler (key); + } else { + if (inputEvent.KeyEvent.bKeyDown) { + // Key Down - Fire KeyDown Event and KeyStroke (ProcessKey) Event + keyDownHandler (new KeyEvent (map)); + keyHandler (new KeyEvent (map)); + } else { + keyUpHandler (new KeyEvent (map)); + } + } break; case WindowsConsole.EventType.Mouse: @@ -571,7 +601,7 @@ namespace Terminal.Gui { rows = inputEvent.WindowBufferSizeEvent.size.Y; ResizeScreen (); UpdateOffScreen (); - TerminalResized?.Invoke(); + TerminalResized?.Invoke (); break; } result = null; @@ -734,8 +764,8 @@ namespace Terminal.Gui { bool capslock = (state & (WindowsConsole.ControlKeyState.CapslockOn)) != 0; bool numlock = (state & (WindowsConsole.ControlKeyState.NumlockOn)) != 0; - var ConsoleKeyInfo = new ConsoleKeyInfo(keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); - return new ConsoleKeyInfoEx(ConsoleKeyInfo, capslock, numlock); + var ConsoleKeyInfo = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); + return new ConsoleKeyInfoEx (ConsoleKeyInfo, capslock, numlock); } public Key MapKey (ConsoleKeyInfoEx keyInfoEx) @@ -837,6 +867,7 @@ namespace Terminal.Gui { return (Key)((int)Key.F1 + delta); } + return (Key)(0xffffffff); } @@ -871,7 +902,7 @@ namespace Terminal.Gui { Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); - Colors.Menu.Disabled = MakeColor(ConsoleColor.DarkGray, ConsoleColor.Cyan); + Colors.Menu.Disabled = MakeColor (ConsoleColor.DarkGray, ConsoleColor.Cyan); Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); @@ -925,10 +956,10 @@ namespace Terminal.Gui { } ccol++; - var runeWidth = Rune.ColumnWidth(rune); + var runeWidth = Rune.ColumnWidth (rune); if (runeWidth > 1) { for (int i = 1; i < runeWidth; i++) { - AddStr(" "); + AddStr (" "); } } if (ccol == Cols) { @@ -947,7 +978,7 @@ namespace Terminal.Gui { } int currentAttribute; - CancellationTokenSource tokenSource = new CancellationTokenSource(); + CancellationTokenSource tokenSource = new CancellationTokenSource (); public override void SetAttribute (Attribute c) { @@ -995,39 +1026,39 @@ namespace Terminal.Gui { if (damageRegion.Left == -1) return; - var bufferCoords = new WindowsConsole.Coord (){ + var bufferCoords = new WindowsConsole.Coord () { X = (short)Clip.Width, Y = (short)Clip.Height }; - var window = new WindowsConsole.SmallRect (){ + var window = new WindowsConsole.SmallRect () { Top = 0, Left = 0, Right = (short)Clip.Right, Bottom = (short)Clip.Bottom }; - UpdateCursor(); + UpdateCursor (); winConsole.WriteToConsole (OutputBuffer, bufferCoords, damageRegion); -// System.Diagnostics.Debugger.Log(0, "debug", $"Region={damageRegion.Right - damageRegion.Left},{damageRegion.Bottom - damageRegion.Top}\n"); + // System.Diagnostics.Debugger.Log(0, "debug", $"Region={damageRegion.Right - damageRegion.Left},{damageRegion.Bottom - damageRegion.Top}\n"); WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); } - public override void UpdateCursor() + public override void UpdateCursor () { - var position = new WindowsConsole.Coord(){ + var position = new WindowsConsole.Coord () { X = (short)ccol, Y = (short)crow }; - winConsole.SetCursorPosition(position); + winConsole.SetCursorPosition (position); } public override void End () { - winConsole.Cleanup(); + winConsole.Cleanup (); } -#region Unused + #region Unused public override void SetColors (ConsoleColor foreground, ConsoleColor background) { } @@ -1055,7 +1086,7 @@ namespace Terminal.Gui { public override void CookMouse () { } -#endregion + #endregion } diff --git a/Terminal.Gui/Event.cs b/Terminal.Gui/Event.cs index aea8d9780..f2c9c6f1b 100644 --- a/Terminal.Gui/Event.cs +++ b/Terminal.Gui/Event.cs @@ -178,6 +178,12 @@ namespace Terminal.Gui { /// AltMask = 0x80000000, + /// + /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. + /// And the actual value must be extracted by removing the CtrlMask. + /// + CtrlMask = 0x40000000, + /// /// Backspace key. /// @@ -297,7 +303,7 @@ namespace Terminal.Gui { public bool IsAlt => (Key & Key.AltMask) != 0; /// - /// Determines whether the value is a control key + /// Determines whether the value is a control key (and NOT just the ctrl key) /// /// true if is ctrl; otherwise, false. public bool IsCtrl => ((uint)Key >= 1) && ((uint)Key <= 26); diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index 95f44b45c..27bf8546b 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -1,998 +1,1096 @@ -// -// 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; - public event EventHandler OnCloseMenu; - internal Menu openMenu; - Menu openCurrentMenu; - internal List openSubMenu; - View previousFocused; - internal bool isMenuOpening; - internal bool isMenuClosing; - internal bool isMenuClosed; - public bool MenuOpen; - 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; - MenuOpen = true; - } - - // 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; - OnCloseMenu?.Invoke (this, null); - 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; - MenuOpen = 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) { - if (mi.Title[p + 1] == c) { - Application.GrabMouse (this); - selected = i; - OpenMenu (i); - return true; - } - } - } - 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; - } - } - -} +// +// 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; +using System.Reflection; + +namespace Terminal.Gui { + + /// + /// A menu item has a title, an associated help text, and an action to execute on activation. + /// + public class MenuItem { + + public MenuItem () + { + Title = ""; + Help = ""; + } + + /// + /// 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 : MenuItem { + /// + /// Initializes a new as a . + /// + /// 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 MenuBarItem (ustring title, string help, Action action, Func canExecute = null) : base (title, help, action, canExecute) + { + SetTitle (title ?? ""); + Children = null; + } + + /// + /// 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; } + + internal bool IsTopLevel { get => (Children == null || Children.Length == 0); } + + } + + class Menu : View { + internal MenuBarItem barItems; + MenuBar host; + internal int current; + internal View previousSubFocused; + + static Rect MakeFrame (int x, int y, MenuItem [] items) + { + if (items == null || items.Length == 0) { + return new Rect (); + } + 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; + if (barItems.IsTopLevel) { + // This is a standalone MenuItem on a MenuBar + ColorScheme = Colors.Menu; + CanFocus = true; + } else { + + 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 == null || !host.isMenuClosed) + if (barItems.IsTopLevel) { + host.PositionCursor (); + } else + Move (2, 1 + current); + else + host.PositionCursor (); + } + + public 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 (barItems.IsTopLevel || 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: + if (barItems.IsTopLevel) { + break; + } + + 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.IsTopLevel || barItems.Children [current].IsFromSubMenu ? true : false); + break; + case Key.Esc: + Application.UngrabMouse (); + host.CloseAllMenus (); + break; + case Key.Enter: + if (barItems.IsTopLevel) { + Run (barItems.Action); + } else { + CheckSubMenu (); + Run (barItems.Children [current].Action); + } + break; + default: + // TODO: rune-ify + if (barItems.Children != null && 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 = true; + selected = -1; + selectedSub = -1; + ColorScheme = Colors.Menu; + WantMousePositionReports = true; + isMenuClosed = true; + } + + public override bool KeyDown (KeyEvent keyEvent) + { + if (keyEvent.IsAlt) { + openedByHotKey = false; + } + return false; + } + + /// + /// Track Alt key-up events. On Windows, when a user releases Alt (without another key), the menu gets focus but doesn't open. + /// We mimic that behavior here. + /// + /// + /// + public override bool KeyUp (KeyEvent keyEvent) + { + if (keyEvent.IsAlt) { + // User pressed Alt - this may be a precursor to a menu accellerator (e.g. Alt-F) + if (openMenu == null) { + // There's no open menu, the first menu item should be highlight. + // The right way to do this is to SetFocus(MenuBar), but for some reason + // that faults. + + Activate (0); + SetNeedsDisplay (); + } else { + // There's an open menu. If this Alt key-up is a pre-cursor to an acellerator + // we don't want to close the menu because it'll flash. + // How to deal with that? + if (!openedByHotKey) { + CloseAllMenus (); + } + } + return true; + } + return false; + } + + 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 = ColorScheme.HotNormal; + normalColor = ColorScheme.Normal; + } + DrawHotString ($" {menu.Title} ", hotColor, normalColor); + pos += 1 + menu.TitleLength + 2; + } + PositionCursor (); + } + + public override void PositionCursor () + { + int pos = 0; + for (int i = 0; i < Menus.Length; i++) { + if (i == selected) { + pos++; + if (!isMenuClosed) + Move (pos + 1, 0); + else + Move (pos + 1, 0); + return; + } else { + if (!isMenuClosed) + pos += 1 + Menus [i].TitleLength + 2; + else + pos += 2 + Menus [i].TitleLength + 1; + } + } + //Move (0, 0); + } + + void Selected (MenuItem item) + { + // TODO: Running = false; + action = item.Action; + } + + public event EventHandler OnOpenMenu; + public event EventHandler OnCloseMenu; + internal Menu openMenu; + Menu openCurrentMenu; + internal List openSubMenu; + View previousFocused; + internal bool isMenuOpening; + internal bool isMenuClosing; + internal bool isMenuClosed; + public bool MenuOpen; + 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; + MenuOpen = true; + } + + // 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; + OnCloseMenu?.Invoke (this, null); + 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; + MenuOpen = 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; + openedByHotKey = false; + } + + 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; + } + } + + bool openedByHotKey = false; + 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) { + if (Char.ToUpperInvariant ((char)mi.Title [p + 1]) == c) { + if (mi.IsTopLevel) { + var menu = new Menu (this, i, 0, mi); + menu.Run (mi.Action); + } else { + openedByHotKey = true; + Application.GrabMouse (this); + selected = i; + OpenMenu (i); + } + return true; + } + } + } + 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].IsTopLevel) + 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 (); + if (Menus [i].IsTopLevel) { + var menu = new Menu (this, i, 0, Menus [i]); + menu.Run (Menus [i].Action); + } else { + CloseMenu (); + } + } else if (me.Flags == MouseFlags.Button1Clicked && isMenuClosed) { + if (Menus [i].IsTopLevel) { + var menu = new Menu (this, i, 0, Menus [i]); + menu.Run (Menus [i].Action); + } else { + 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/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index adb619c28..d6a6b886a 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -6,27 +6,24 @@ // // TODO: // Add mouse support -// Uses internals of Application using System; using NStack; -namespace Terminal.Gui -{ +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 - { + 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) + public StatusItem (Key shortcut, ustring title, Action action) { Title = title ?? ""; Shortcut = shortcut; @@ -57,8 +54,7 @@ namespace Terminal.Gui /// 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 class StatusBar : View { public StatusItem [] Items { get; set; } /// @@ -66,25 +62,29 @@ namespace Terminal.Gui /// It will be drawn in the lowest column of the terminal. /// /// A list of statusbar items. - public StatusBar(StatusItem [] items) : base() + 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; + + Application.OnResized += () => { + this.X = Pos.Left (Application.Top); + this.Y = Pos.Bottom (Application.Top); + }; } - Attribute ToggleScheme(Attribute scheme) + Attribute ToggleScheme (Attribute scheme) { - var result = scheme==ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; - Driver.SetAttribute(result); + var result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; + Driver.SetAttribute (result); return result; } - public override void Redraw(Rect region) { + 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; @@ -98,15 +98,15 @@ namespace Terminal.Gui Move (1, 0); var scheme = ColorScheme.Normal; - Driver.SetAttribute(scheme); - for(int i=0; i Date: Sat, 18 Apr 2020 13:36:58 -0400 Subject: [PATCH 04/18] Bring patch from Charlie Kindle without the newline changes #381 from https://github.com/migueldeicaza/gui.cs/pull/381/files (#391) From @tig - Fixed #380 -F9 should close menu as well as open it #381 --- Terminal.Gui/Views/Menu.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index 27bf8546b..2c118c237 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -919,7 +919,10 @@ namespace Terminal.Gui { public override bool ProcessHotKey (KeyEvent kb) { if (kb.Key == Key.F9) { - StartMenu (); + if (isMenuClosed) + StartMenu (); + else + CloseAllMenus (); return true; } From df5bc9f0b8696978f635c71f7512e2936a8d4c5b Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 18 Apr 2020 18:37:50 +0100 Subject: [PATCH 05/18] Fixed a moving window issue. Added OnLoad Action because there are settings that need to be accessed only once. (#375) * Fixed a moving window issue. Added OnLoad Action because there are settings that need to be accessed only once. * Fixes a layout issue that does not updated the Pos outside the bounds. * Fixes a issue with other top-levels. Co-authored-by: Miguel de Icaza --- Example/demo.cs | 26 +++++++++++++++----------- Terminal.Gui/Core.cs | 23 +++++++++++++++++------ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Example/demo.cs b/Example/demo.cs index c44b23ae7..0ec5cc9ae 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -446,7 +446,6 @@ static class Demo { CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); //Application.UseSystemConsole = true; - Console.WindowHeight = 35; Application.Init (); @@ -454,11 +453,13 @@ static class Demo { //Open (); #if true + int margin = 3; var win = new Window ("Hello") { X = 1, Y = 1, - Width = Dim.Fill (), - Height = Dim.Fill () - 1 + + Width = Dim.Fill () - margin, + Height = Dim.Fill () - margin }; #else var tframe = top.Frame; @@ -528,7 +529,7 @@ static class Demo { ShowEntries (win); int count = 0; - ml = new Label (new Rect (3, 18, 47, 1), "Mouse: "); + ml = new Label (new Rect (3, 17, 47, 1), "Mouse: "); Application.RootMouseEvent += delegate (MouseEvent me) { ml.TextColor = Colors.TopLevel.Normal; ml.Text = $"Mouse: ({me.X},{me.Y}) - {me.Flags} {count++}"; @@ -554,21 +555,24 @@ static class Demo { win.Add (drag, dragText); #if true - // This currently causes a stack overflow, because it is referencing a window that has not had its size allocated yet + // FIXED: This currently causes a stack overflow, because it is referencing a window that has not had its size allocated yet - var bottom = new Label ("This should go on the bottom!"); + var bottom = new Label ("This should go on the bottom of the same top-level!"); win.Add (bottom); + var bottom2 = new Label ("This should go on the bottom of another top-level!"); + top.Add (bottom2); - Application.OnResized = () => { - bottom.X = Pos.Left (win); - bottom.Y = Pos.Bottom (win); + Application.OnLoad = () => { + bottom.X = win.X; + bottom.Y = Pos.Bottom (win) - Pos.Top (win) - margin; + bottom2.X = Pos.Left (win); + bottom2.Y = Pos.Bottom (win); }; #endif - top.Add (win); //top.Add (menu); - top.Add (menu, statusBar, ml); + top.Add (menu, statusBar); Application.Run (); } } diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index c93af8908..36b18410e 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -1248,7 +1248,7 @@ namespace Terminal.Gui { } // https://en.wikipedia.org/wiki/Topological_sorting - static List TopologicalSort (HashSet nodes, HashSet<(View, View)> edges) + List TopologicalSort (HashSet nodes, HashSet<(View, View)> edges) { var result = new List (); @@ -1261,7 +1261,8 @@ namespace Terminal.Gui { S.Remove (n); // add n to tail of L - result.Add (n); + if (n != this?.SuperView) + result.Add (n); // for each node m with an edge e from n to m do foreach (var e in edges.Where (e => e.Item1.Equals (n)).ToList ()) { @@ -1271,7 +1272,7 @@ namespace Terminal.Gui { edges.Remove (e); // if m has no other incoming edges then - if (edges.All (me => me.Item2.Equals (m) == false)) { + if (edges.All (me => me.Item2.Equals (m) == false) && m != this?.SuperView) { // insert m into S S.Add (m); } @@ -1325,11 +1326,15 @@ namespace Terminal.Gui { if (v.LayoutStyle == LayoutStyle.Computed) v.RelativeLayout (Frame); - if (this?.SuperView != v) - v.LayoutSubviews (); + v.LayoutSubviews (); v.layoutNeeded = false; } + + if (SuperView == Application.Top && layoutNeeded && ordered.Count == 0 && LayoutStyle == LayoutStyle.Computed) { + RelativeLayout (Frame); + } + layoutNeeded = false; } @@ -1569,7 +1574,7 @@ namespace Terminal.Gui { } foreach (var view in Subviews) { if (view.Frame.IntersectsWith (region)) { - //view.SetNeedsLayout (); + view.SetNeedsLayout (); view.SetNeedsDisplay (view.Bounds); } } @@ -2142,6 +2147,11 @@ namespace Terminal.Gui { } } + /// + /// Action that is invoked once at beginning. + /// + static public Action OnLoad; + /// /// Building block API: Prepares the provided toplevel for execution. /// @@ -2176,6 +2186,7 @@ namespace Terminal.Gui { if (toplevel.LayoutStyle == LayoutStyle.Computed) toplevel.RelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows)); toplevel.LayoutSubviews (); + OnLoad?.Invoke (); toplevel.WillPresent (); Redraw (toplevel); toplevel.PositionCursor (); From 6bdd02fff123b06ec10e5378047e2efceadef718 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 18 Apr 2020 23:46:23 +0100 Subject: [PATCH 06/18] Pos and Dim should only be configured once for each change. Otherwise, a recursive loop occurs resulting in a stack overflow. For that, you have to use the OnLoad action and just use OnResized for other situations that are not always for the same definitions. (#392) --- Terminal.Gui/Views/StatusBar.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index d6a6b886a..85c6de3fa 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -13,7 +13,7 @@ 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 + /// 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 { @@ -49,10 +49,10 @@ namespace Terminal.Gui { }; /// - /// A statusbar for your application. + /// 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. + /// 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; } @@ -70,7 +70,7 @@ namespace Terminal.Gui { CanFocus = false; ColorScheme = Colors.Menu; - Application.OnResized += () => { + Application.OnLoad += () => { this.X = Pos.Left (Application.Top); this.Y = Pos.Bottom (Application.Top); }; From 4522097c027cf9a968b9bca6f4d0a72a9b7ed6d9 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 20 Apr 2020 20:26:05 +0100 Subject: [PATCH 07/18] Forces the height of the TextField to 1 which is just the valid height instead of driver rows value. (#400) * Forces the height of the TextField to 1 which is just the valid height instead of driver rows value. * Fixes an issue in the sln file that despite not having been changed, git reports as changed. --- .gitattributes | 3 --- Terminal.Gui/Views/TextField.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index 7818f5079..35720cb73 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,9 +5,6 @@ # native line endings on checkout. *.cs text -# Convert to CRLF line endings on checkout. -*.sln text eol=crlf - # Convert to LF line endings on checkout. *.sh text eol=lf diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 788967b06..81a8613cb 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -43,7 +43,7 @@ namespace Terminal.Gui { /// Initial text contents. public TextField (string text) : this (ustring.Make (text)) { - + Height = 1; } /// From 8a6c2a9fc15cefee63a9a9e44ba3b16adc8ad669 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 20 Apr 2020 20:26:32 +0100 Subject: [PATCH 08/18] Mouse events menu (#401) * Fixes an issue in the sln file that despite not having been changed, git reports as changed. * Adding some settings for crlf and user specific. * Prevent button clicked event if the point of the pressed and released don't match. Also decreases the delay for the triple click. * Changes the menu button clicked event to button pressed to improve mouse clicks. * Removed the action that was running all the time after the first running. --- .editorconfig | 1 + .gitignore | 5 ++-- Terminal.Gui/Drivers/WindowsDriver.cs | 40 ++++++++++++++++++--------- Terminal.Gui/Views/Menu.cs | 26 ++++++++--------- 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/.editorconfig b/.editorconfig index 576839082..040e7abd9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,7 @@ csharp_new_line_before_open_brace = methods,local_functions csharp_new_line_before_else = false csharp_new_line_before_catch = false csharp_new_line_before_finally = false +end_of_line = crlf csharp_indent_case_contents = true csharp_indent_switch_labels = false diff --git a/.gitignore b/.gitignore index c4865c0ac..632d369b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ bin obj -*~ +~$* *.userprefs *~ packages .vs -*.csproj.user +# User-specific files +*.user diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index 1dcaf34fc..de779c614 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -610,6 +610,7 @@ namespace Terminal.Gui { WindowsConsole.ButtonState? LastMouseButtonPressed = null; bool IsButtonReleased = false; bool IsButtonDoubleClicked = false; + Point point; MouseEvent ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent) { @@ -617,8 +618,8 @@ namespace Terminal.Gui { if (IsButtonDoubleClicked) { Task.Run (async () => { - await Task.Delay (300); - _ = new Action (() => IsButtonDoubleClicked = false); + await Task.Delay (100); + IsButtonDoubleClicked = false; }); } @@ -649,8 +650,15 @@ namespace Terminal.Gui { break; } - if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) + if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) { mouseFlag |= MouseFlags.ReportMousePosition; + point = new Point (); + } else { + point = new Point () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y + }; + } LastMouseButtonPressed = mouseEvent.ButtonState; } else if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed != null && !IsButtonReleased && !IsButtonDoubleClicked) { @@ -670,18 +678,24 @@ namespace Terminal.Gui { IsButtonReleased = true; } else if ((mouseEvent.EventFlags == 0 || mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) && IsButtonReleased) { - switch (LastMouseButtonPressed) { - case WindowsConsole.ButtonState.Button1Pressed: - mouseFlag = MouseFlags.Button1Clicked; - break; + var p = new Point () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y + }; + if (p == point) { + switch (LastMouseButtonPressed) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Clicked; + break; - case WindowsConsole.ButtonState.Button2Pressed: - mouseFlag = MouseFlags.Button2Clicked; - break; + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Clicked; + break; - case WindowsConsole.ButtonState.RightmostButtonPressed: - mouseFlag = MouseFlags.Button4Clicked; - break; + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button4Clicked; + break; + } } LastMouseButtonPressed = null; IsButtonReleased = false; diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index 2c118c237..3b6bc1ccd 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -432,7 +432,7 @@ namespace Terminal.Gui { } host.handled = false; bool disabled; - if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1Released) { + if (me.Flags == MouseFlags.Button1Pressed) { disabled = false; if (me.Y < 1) return true; @@ -525,7 +525,7 @@ namespace Terminal.Gui { Width = Dim.Fill (); Height = 1; Menus = menus; - CanFocus = true; + //CanFocus = true; selected = -1; selectedSub = -1; ColorScheme = Colors.Menu; @@ -984,13 +984,13 @@ namespace Terminal.Gui { } handled = false; - if (me.Flags == MouseFlags.Button1Clicked || + if (me.Flags == MouseFlags.Button1Pressed || (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) { + if (selected == i && me.Flags == MouseFlags.Button1Pressed && !isMenuClosed) { Application.UngrabMouse (); if (Menus [i].IsTopLevel) { var menu = new Menu (this, i, 0, Menus [i]); @@ -998,7 +998,7 @@ namespace Terminal.Gui { } else { CloseMenu (); } - } else if (me.Flags == MouseFlags.Button1Clicked && isMenuClosed) { + } else if (me.Flags == MouseFlags.Button1Pressed && isMenuClosed) { if (Menus [i].IsTopLevel) { var menu = new Menu (this, i, 0, Menus [i]); menu.Run (Menus [i].Action); @@ -1033,7 +1033,7 @@ namespace Terminal.Gui { Application.GrabMouse (me.View); me.View.MouseEvent (me); } - } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags.HasFlag (MouseFlags.Button1Clicked)) { + } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags.HasFlag (MouseFlags.Button1Pressed)) { Application.UngrabMouse (); CloseAllMenus (); handled = false; @@ -1042,22 +1042,22 @@ namespace Terminal.Gui { handled = false; return false; } - } else if (isMenuClosed && me.Flags.HasFlag (MouseFlags.Button1Clicked)) { + } else if (isMenuClosed && me.Flags.HasFlag (MouseFlags.Button1Pressed)) { Application.GrabMouse (current); } else { handled = false; return false; } - //if (me.View != this && me.Flags != MouseFlags.Button1Clicked) + //if (me.View != this && me.Flags != MouseFlags.Button1Pressed) // return true; - //else if (me.View != this && me.Flags == MouseFlags.Button1Clicked) { + //else if (me.View != this && me.Flags == MouseFlags.Button1Pressed) { // Application.UngrabMouse (); // host.CloseAllMenus (); // return true; //} - //if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Clicked) + //if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Pressed) // return false; //if (Application.mouseGrabView != null) { @@ -1066,11 +1066,11 @@ namespace Terminal.Gui { // me.Y -= me.OfY; // me.View.MouseEvent (me); // return true; - // } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Clicked) { + // } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Pressed) { // Application.UngrabMouse (); // CloseAllMenus (); // } - //} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Clicked) { + //} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Pressed) { // Application.GrabMouse (this); // return true; //} @@ -1082,7 +1082,7 @@ namespace Terminal.Gui { // } 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) { + // } else if (me.Flags == MouseFlags.Button1Pressed) { // Application.UngrabMouse (); // CloseMenu (); // } From ab91de504131a47a46c0b33d4a46fa0ae69c91fc Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 20 Apr 2020 21:07:29 +0100 Subject: [PATCH 09/18] Fixes an issue in the sln file that despite not having been changed, git reports as changed. (#393) * Fixes an issue in the sln file that despite not having been changed, git reports as changed. * Adding some settings for crlf and user specific. From 5df8500862dbc65f35843a084b9888d33cdbcf73 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Mon, 20 Apr 2020 14:08:34 -0600 Subject: [PATCH 10/18] Status bar snap (#384) * KeyDown/Up support * updated demo * defined styles * Smarter StatusBar bottom tracking. * Prepping for https://github.com/migueldeicaza/gui.cs/issues/376 * Fixed StatusBar 'snap to bottom' --- Example/demo.cs | 5 +- Terminal.Gui/Drivers/WindowsDriver.cs.orig | 840 --------------------- Terminal.Gui/Views/StatusBar.cs | 55 +- Terminal.sln | 118 +-- 4 files changed, 115 insertions(+), 903 deletions(-) delete mode 100644 Terminal.Gui/Drivers/WindowsDriver.cs.orig diff --git a/Example/demo.cs b/Example/demo.cs index 0ec5cc9ae..f138bc329 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -551,7 +551,9 @@ static class Demo { new StatusItem(Key.F2, "~F2~ Load", null), new StatusItem(Key.F3, "~F3~ Save", null), new StatusItem(Key.ControlX, "~^X~ Quit", () => { if (Quit ()) top.Running = false; }), - }); + }) { + Parent = null, + }; win.Add (drag, dragText); #if true @@ -570,6 +572,7 @@ static class Demo { }; #endif + top.Add (win); //top.Add (menu); top.Add (menu, statusBar); diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs.orig b/Terminal.Gui/Drivers/WindowsDriver.cs.orig deleted file mode 100644 index 1b9084f52..000000000 --- a/Terminal.Gui/Drivers/WindowsDriver.cs.orig +++ /dev/null @@ -1,840 +0,0 @@ -// -// WindowsDriver.cs: Windows specific driver -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// Nick Van Dyck (vandyck.nick@outlook.com) -// -// Copyright (c) 2018 -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// 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 -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -using System; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Mono.Terminal; -using NStack; - -namespace Terminal.Gui { - - internal class WindowsConsole { - public const int STD_OUTPUT_HANDLE = -11; - public const int STD_INPUT_HANDLE = -10; - public const int STD_ERROR_HANDLE = -12; - - internal IntPtr InputHandle, OutputHandle; - IntPtr ScreenBuffer; - uint originalConsoleMode; - - public WindowsConsole () - { - InputHandle = GetStdHandle (STD_INPUT_HANDLE); - OutputHandle = GetStdHandle (STD_OUTPUT_HANDLE); - originalConsoleMode = ConsoleMode; - var newConsoleMode = originalConsoleMode; - newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); - newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; - ConsoleMode = newConsoleMode; - } - - public CharInfo[] OriginalStdOutChars; - - public bool WriteToConsole (CharInfo[] charInfoBuffer, Coord coords, SmallRect window) - { - if (ScreenBuffer == IntPtr.Zero) - { - ScreenBuffer = CreateConsoleScreenBuffer ( - DesiredAccess.GenericRead | DesiredAccess.GenericWrite, - ShareMode.FileShareRead | ShareMode.FileShareWrite, - IntPtr.Zero, - 1, - IntPtr.Zero - ); - if (ScreenBuffer == INVALID_HANDLE_VALUE){ - var err = Marshal.GetLastWin32Error (); - - if (err != 0) - throw new System.ComponentModel.Win32Exception(err); - } - - if (!SetConsoleActiveScreenBuffer (ScreenBuffer)){ - var err = Marshal.GetLastWin32Error(); - throw new System.ComponentModel.Win32Exception(err); - } - - OriginalStdOutChars = new CharInfo[Console.WindowHeight * Console.WindowWidth]; - - ReadConsoleOutput (OutputHandle, OriginalStdOutChars, coords, new Coord () { X = 0, Y = 0 }, ref window); - } - - return WriteConsoleOutput (ScreenBuffer, charInfoBuffer, coords, new Coord () { X = 0, Y = 0 }, ref window); - } - - public bool SetCursorPosition(Coord position) - { - return SetConsoleCursorPosition (ScreenBuffer, position); - } - - public void Cleanup () - { - ContinueListeningForConsoleEvents = false; - if (!SetConsoleActiveScreenBuffer (OutputHandle)){ - var err = Marshal.GetLastWin32Error (); - Console.WriteLine("Error: {0}", err); - } - } - - private bool ContinueListeningForConsoleEvents = true; - - public uint ConsoleMode { - get { - uint v; - GetConsoleMode (InputHandle, out v); - return v; - } - - set { - SetConsoleMode (InputHandle, value); - } - } - - [Flags] - public enum ConsoleModes : uint - { - EnableMouseInput = 16, - EnableQuickEditMode = 64, - EnableExtendedFlags = 128, - } - - [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] - public struct KeyEventRecord { - [FieldOffset (0), MarshalAs (UnmanagedType.Bool)] - public bool bKeyDown; - [FieldOffset (4), MarshalAs (UnmanagedType.U2)] - public ushort wRepeatCount; - [FieldOffset (6), MarshalAs (UnmanagedType.U2)] - public ushort wVirtualKeyCode; - [FieldOffset (8), MarshalAs (UnmanagedType.U2)] - public ushort wVirtualScanCode; - [FieldOffset (10)] - public char UnicodeChar; - [FieldOffset (12), MarshalAs (UnmanagedType.U4)] - public ControlKeyState dwControlKeyState; - } - - [Flags] - public enum ButtonState { - Button1Pressed = 1, - Button2Pressed = 4, - Button3Pressed = 8, - Button4Pressed = 16, - RightmostButtonPressed = 2, - - } - - [Flags] - public enum ControlKeyState { - RightAltPressed = 1, - LeftAltPressed = 2, - RightControlPressed = 4, - LeftControlPressed = 8, - ShiftPressed = 16, - NumlockOn = 32, - ScrolllockOn = 64, - CapslockOn = 128, - EnhancedKey = 256 - } - - [Flags] - public enum EventFlags { - MouseMoved = 1, - DoubleClick = 2, - MouseWheeled = 4, - MouseHorizontalWheeled = 8 - } - - [StructLayout (LayoutKind.Explicit)] - public struct MouseEventRecord { - [FieldOffset (0)] - public Coordinate MousePosition; - [FieldOffset (4)] - public ButtonState ButtonState; - [FieldOffset (8)] - public ControlKeyState ControlKeyState; - [FieldOffset (12)] - public EventFlags EventFlags; - - public override string ToString () - { - return $"[Mouse({MousePosition},{ButtonState},{ControlKeyState},{EventFlags}"; - } - } - - [StructLayout (LayoutKind.Sequential)] - public struct Coordinate { - public short X; - public short Y; - - public Coordinate (short X, short Y) - { - this.X = X; - this.Y = Y; - } - - public override string ToString () => $"({X},{Y})"; - }; - - internal struct WindowBufferSizeRecord { - public Coordinate size; - - public WindowBufferSizeRecord (short x, short y) - { - this.size = new Coordinate (x, y); - } - - public override string ToString () => $"[WindowBufferSize{size}"; - } - - [StructLayout (LayoutKind.Sequential)] - public struct MenuEventRecord { - public uint dwCommandId; - } - - [StructLayout (LayoutKind.Sequential)] - public struct FocusEventRecord { - public uint bSetFocus; - } - - public enum EventType { - Focus = 0x10, - Key = 0x1, - Menu = 0x8, - Mouse = 2, - WindowBufferSize = 4 - } - - [StructLayout (LayoutKind.Explicit)] - public struct InputRecord { - [FieldOffset (0)] - public EventType EventType; - [FieldOffset (4)] - public KeyEventRecord KeyEvent; - [FieldOffset (4)] - public MouseEventRecord MouseEvent; - [FieldOffset (4)] - public WindowBufferSizeRecord WindowBufferSizeEvent; - [FieldOffset (4)] - public MenuEventRecord MenuEvent; - [FieldOffset (4)] - public FocusEventRecord FocusEvent; - - public override string ToString () - { - switch (EventType) { - case EventType.Focus: - return FocusEvent.ToString (); - case EventType.Key: - return KeyEvent.ToString (); - case EventType.Menu: - return MenuEvent.ToString (); - case EventType.Mouse: - return MouseEvent.ToString (); - case EventType.WindowBufferSize: - return WindowBufferSizeEvent.ToString (); - default: - return "Unknown event type: " + EventType; - } - } - }; - - [Flags] - enum ShareMode : uint - { - FileShareRead = 1, - FileShareWrite = 2, - } - - [Flags] - enum DesiredAccess : uint - { - GenericRead = 2147483648, - GenericWrite = 1073741824, - } - - [StructLayout(LayoutKind.Sequential)] - public struct ConsoleScreenBufferInfo - { - public Coord dwSize; - public Coord dwCursorPosition; - public ushort wAttributes; - public SmallRect srWindow; - public Coord dwMaximumWindowSize; - } - - [StructLayout(LayoutKind.Sequential)] - public struct Coord - { - public short X; - public short Y; - - public Coord(short X, short Y) - { - this.X = X; - this.Y = Y; - } - }; - - [StructLayout(LayoutKind.Explicit, CharSet=CharSet.Unicode)] - public struct CharUnion - { - [FieldOffset(0)] public char UnicodeChar; - [FieldOffset(0)] public byte AsciiChar; - } - - [StructLayout(LayoutKind.Explicit, CharSet=CharSet.Unicode)] - public struct CharInfo - { - [FieldOffset(0)] public CharUnion Char; - [FieldOffset(2)] public ushort Attributes; - } - - [StructLayout(LayoutKind.Sequential)] - public struct SmallRect - { - public short Left; - public short Top; - public short Right; - public short Bottom; - } - - [DllImport ("kernel32.dll", SetLastError = true)] - static extern IntPtr GetStdHandle (int nStdHandle); - - [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] - public static extern bool ReadConsoleInput ( - IntPtr hConsoleInput, - [Out] InputRecord [] lpBuffer, - uint nLength, - out uint lpNumberOfEventsRead); - - [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] - static extern bool ReadConsoleOutput( - IntPtr hConsoleOutput, - [Out] CharInfo[] lpBuffer, - Coord dwBufferSize, - Coord dwBufferCoord, - ref SmallRect lpReadRegion - ); - - [DllImport("kernel32.dll", EntryPoint="WriteConsoleOutput", SetLastError=true, CharSet=CharSet.Unicode)] - static extern bool WriteConsoleOutput( - IntPtr hConsoleOutput, - CharInfo[] lpBuffer, - Coord dwBufferSize, - Coord dwBufferCoord, - ref SmallRect lpWriteRegion - ); - - [DllImport ("kernel32.dll")] - static extern bool SetConsoleCursorPosition(IntPtr hConsoleOutput, Coord dwCursorPosition); - - [DllImport ("kernel32.dll")] - static extern bool GetConsoleMode (IntPtr hConsoleHandle, out uint lpMode); - - - [DllImport ("kernel32.dll")] - static extern bool SetConsoleMode (IntPtr hConsoleHandle, uint dwMode); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern IntPtr CreateConsoleScreenBuffer( - DesiredAccess dwDesiredAccess, - ShareMode dwShareMode, - IntPtr secutiryAttributes, - UInt32 flags, - IntPtr screenBufferData - ); - - internal static IntPtr INVALID_HANDLE_VALUE = new IntPtr (-1); - - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool SetConsoleActiveScreenBuffer(IntPtr Handle); - - [DllImport ("kernel32.dll", SetLastError = true)] - static extern bool GetNumberOfConsoleInputEvents (IntPtr handle, out uint lpcNumberOfEvents); - public uint InputEventCount { - get { - uint v; - GetNumberOfConsoleInputEvents (InputHandle, out v); - return v; - } - } - } - - internal class WindowsDriver : ConsoleDriver, Mono.Terminal.IMainLoopDriver { - static bool sync; - AutoResetEvent eventReady = new AutoResetEvent (false); - AutoResetEvent waitForProbe = new AutoResetEvent (false); - MainLoop mainLoop; - Action TerminalResized; - WindowsConsole.CharInfo[] OutputBuffer; - int cols, rows; - WindowsConsole winConsole; - - public override int Cols => cols; - public override int Rows => rows; - - public WindowsDriver () - { - winConsole = new WindowsConsole(); - - cols = Console.WindowWidth; - rows = Console.WindowHeight - 1; - - ResizeScreen (); - UpdateOffScreen (); - - Task.Run ((Action)WindowsInputHandler); - } - - // The records that we keep fetching - WindowsConsole.InputRecord [] result, records = new WindowsConsole.InputRecord [1]; - - void WindowsInputHandler () - { - while (true) { - waitForProbe.WaitOne (); - - uint numberEventsRead = 0; - - WindowsConsole.ReadConsoleInput (winConsole.InputHandle, records, 1, out numberEventsRead); - if (numberEventsRead == 0) - result = null; - else - result = records; - - eventReady.Set (); - } - } - - void IMainLoopDriver.Setup (MainLoop mainLoop) - { - this.mainLoop = mainLoop; - } - - void IMainLoopDriver.Wakeup () - { - } - - bool IMainLoopDriver.EventsPending (bool wait) - { - long now = DateTime.UtcNow.Ticks; - - int waitTimeout; - if (mainLoop.timeouts.Count > 0) { - waitTimeout = (int)((mainLoop.timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); - if (waitTimeout < 0) - return true; - } else - waitTimeout = -1; - - if (!wait) - waitTimeout = 0; - - result = null; - waitForProbe.Set (); - eventReady.WaitOne (waitTimeout); - return result != null; - } - - Action keyHandler; - Action mouseHandler; - - public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action mouseHandler) - { - this.keyHandler = keyHandler; - this.mouseHandler = mouseHandler; - } - - - void IMainLoopDriver.MainIteration () - { - if (result == null) - return; - - var inputEvent = result [0]; - switch (inputEvent.EventType) { - case WindowsConsole.EventType.Key: - if (inputEvent.KeyEvent.bKeyDown == false) - return; - var map = MapKey (ToConsoleKeyInfo (inputEvent.KeyEvent)); - if (map == (Key)0xffffffff) - return; - keyHandler (new KeyEvent (map)); - break; - - case WindowsConsole.EventType.Mouse: - mouseHandler (ToDriverMouse (inputEvent.MouseEvent)); - break; - - case WindowsConsole.EventType.WindowBufferSize: - cols = inputEvent.WindowBufferSizeEvent.size.X; - rows = inputEvent.WindowBufferSizeEvent.size.Y - 1; - ResizeScreen (); - UpdateOffScreen (); - TerminalResized (); - break; - } - result = null; - } - - private WindowsConsole.ButtonState? LastMouseButtonPressed = null; - - private MouseEvent ToDriverMouse(WindowsConsole.MouseEventRecord mouseEvent) - { - MouseFlags mouseFlag = MouseFlags.AllEvents; - - // 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) - { - LastMouseButtonPressed = null; - } - - if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed == null){ - switch (mouseEvent.ButtonState){ - case WindowsConsole.ButtonState.Button1Pressed: - mouseFlag = MouseFlags.Button1Pressed; - break; - - case WindowsConsole.ButtonState.Button2Pressed: - mouseFlag = MouseFlags.Button2Pressed; - break; - - case WindowsConsole.ButtonState.Button3Pressed: - mouseFlag = MouseFlags.Button3Pressed; - break; - } - LastMouseButtonPressed = mouseEvent.ButtonState; - } else if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed != null){ - switch (LastMouseButtonPressed){ - case WindowsConsole.ButtonState.Button1Pressed: - mouseFlag = MouseFlags.Button1Clicked; - break; - - case WindowsConsole.ButtonState.Button2Pressed: - mouseFlag = MouseFlags.Button2Clicked; - break; - - case WindowsConsole.ButtonState.Button3Pressed: - mouseFlag = MouseFlags.Button3Clicked; - break; - } - LastMouseButtonPressed = null; - } else if(mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved){ - mouseFlag = MouseFlags.ReportMousePosition; - } - - return new MouseEvent () { - X = mouseEvent.MousePosition.X, - Y = mouseEvent.MousePosition.Y, - Flags = mouseFlag - }; - } - - private ConsoleKeyInfo ToConsoleKeyInfo (WindowsConsole.KeyEventRecord keyEvent) - { - var state = keyEvent.dwControlKeyState; - - bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0; - bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0; - bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0; - - return new ConsoleKeyInfo(keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); - } - - public Key MapKey (ConsoleKeyInfo keyInfo) - { - switch (keyInfo.Key) { - case ConsoleKey.Escape: - return Key.Esc; - case ConsoleKey.Tab: - return Key.Tab; - case ConsoleKey.Home: - return Key.Home; - case ConsoleKey.End: - return Key.End; - case ConsoleKey.LeftArrow: - return Key.CursorLeft; - case ConsoleKey.RightArrow: - return Key.CursorRight; - case ConsoleKey.UpArrow: - return Key.CursorUp; - case ConsoleKey.DownArrow: - return Key.CursorDown; - case ConsoleKey.PageUp: - return Key.PageUp; - case ConsoleKey.PageDown: - return Key.PageDown; - case ConsoleKey.Enter: - return Key.Enter; - case ConsoleKey.Spacebar: - return Key.Space; - case ConsoleKey.Backspace: - return Key.Backspace; - case ConsoleKey.Delete: - return Key.DeleteChar; - - case ConsoleKey.Oem1: - case ConsoleKey.Oem2: - case ConsoleKey.Oem3: - case ConsoleKey.Oem4: - case ConsoleKey.Oem5: - case ConsoleKey.Oem6: - case ConsoleKey.Oem7: - case ConsoleKey.Oem8: - case ConsoleKey.Oem102: - case ConsoleKey.OemPeriod: - case ConsoleKey.OemComma: - case ConsoleKey.OemPlus: - case ConsoleKey.OemMinus: - return (Key)((uint)keyInfo.KeyChar); - } - - var key = keyInfo.Key; - 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.Shift) - return (Key)((uint)'A' + delta); - else - return (Key)((uint)'a' + 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 (keyInfo.Modifiers == ConsoleModifiers.Shift) - return (Key)((uint)keyInfo.KeyChar); - return (Key)((uint)'0' + delta); - } - if (key >= ConsoleKey.F1 && key <= ConsoleKey.F10) { - var delta = key - ConsoleKey.F1; - - return (Key)((int)Key.F1 + delta); - } - return (Key)(0xffffffff); - } - - public override void Init (Action terminalResized) - { - TerminalResized = terminalResized; - - Colors.Base = new ColorScheme (); - Colors.Dialog = new ColorScheme (); - Colors.Menu = new ColorScheme (); - Colors.Error = new ColorScheme (); - - HLine = '\u2500'; - VLine = '\u2502'; - Stipple = '\u2592'; - Diamond = '\u25c6'; - ULCorner = '\u250C'; - LLCorner = '\u2514'; - URCorner = '\u2510'; - LRCorner = '\u2518'; - LeftTee = '\u251c'; - RightTee = '\u2524'; - TopTee = '\u22a4'; - BottomTee = '\u22a5'; - - Colors.Base.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Blue); - Colors.Base.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); - Colors.Base.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Blue); - Colors.Base.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); - - Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); - 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.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); - Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); - Colors.Dialog.HotNormal = MakeColor (ConsoleColor.Blue, ConsoleColor.Gray); - Colors.Dialog.HotFocus = MakeColor (ConsoleColor.Blue, ConsoleColor.Cyan); - - Colors.Error.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Red); - Colors.Error.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); - Colors.Error.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Red); - Colors.Error.HotFocus = Colors.Error.HotNormal; - Console.Clear (); - } - - void ResizeScreen () - { - OutputBuffer = new WindowsConsole.CharInfo[Rows * Cols]; - Clip = new Rect (0, 0, Cols, Rows); - } - - void UpdateOffScreen () - { - 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].Char.UnicodeChar = ' '; - } - } - - int ccol, crow; - public override void Move (int col, int row) - { - ccol = col; - crow = row; - } - - public override void AddRune (Rune rune) - { - var position = crow * Cols + ccol; - - if (Clip.Contains (ccol, crow)){ - OutputBuffer[position].Attributes = (ushort)currentAttribute; - OutputBuffer[position].Char.UnicodeChar = (char)rune; - } - - ccol++; - if (ccol == Cols) { - ccol = 0; - if (crow + 1 < Rows) - crow++; - } - if (sync) - UpdateScreen (); - } - - public override void AddStr (ustring str) - { - foreach (var rune in str) - AddRune (rune); - } - - int currentAttribute; - public override void SetAttribute (Attribute c) - { - currentAttribute = c.value; - } - - private Attribute MakeColor (ConsoleColor f, ConsoleColor b) - { - // Encode the colors into the int value. - return new Attribute (){ - value = ((int)f | (int)b << 4) - }; - } - - public override void Refresh() - { - var bufferCoords = new WindowsConsole.Coord (){ - X = (short)Clip.Width, - Y = (short)Clip.Height - }; - - var window = new WindowsConsole.SmallRect (){ - Top = 0, - Left = 0, - Right = (short)Clip.Right, - Bottom = (short)Clip.Bottom - }; - - UpdateCursor(); - winConsole.WriteToConsole (OutputBuffer, bufferCoords, window); - } - - public override void UpdateScreen () - { - var bufferCoords = new WindowsConsole.Coord (){ - X = (short)Clip.Width, - Y = (short)Clip.Height - }; - - var window = new WindowsConsole.SmallRect (){ - Top = 0, - Left = 0, - Right = (short)Clip.Right, - Bottom = (short)Clip.Bottom - }; - - UpdateCursor(); - winConsole.WriteToConsole (OutputBuffer, bufferCoords, window); - } - - public override void UpdateCursor() - { - var position = new WindowsConsole.Coord(){ - X = (short)ccol, - Y = (short)crow - }; - winConsole.SetCursorPosition(position); - } - public override void End () - { - winConsole.Cleanup(); - } - - #region Unused - public override void SetColors (ConsoleColor foreground, ConsoleColor background) - { - } - - public override void SetColors (short foregroundColorId, short backgroundColorId) - { - } - - public override void Suspend () - { - } - - public override void StartReportingMouseMoves () - { - } - - public override void StopReportingMouseMoves () - { - } - - public override void UncookMouse () - { - } - - public override void CookMouse () - { - } - #endregion - - } - - -} diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index 85c6de3fa..fe1573a91 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -55,11 +55,43 @@ namespace Terminal.Gui { /// So for each context must be a new instance of a statusbar. /// public class StatusBar : View { +// After attempting to implement this, I noticed that there are hard dependencies +// on StatusBar and MenuBars within core. They will need to be refactored for having the +// StatusBar work at the top +#if SNAP_TO_TOP + /// + /// The style supported by StatusBar + /// + public enum StatusBarStyle { + Default = 0, + /// + /// The StatusBar will snap at the the bottom line of the Parent view. + /// If the console window is made larger while the app is runing, the StatusBar + /// will continue to snap to the bottom line of the Parent, staying visible. + /// On consoles that support resizing of console apps (e.g. Windows Terminal and ConEmu), + /// if the console window is subsequently made shorter, the status bar will remain visible + /// as the Parent view resizes. If Parent is null, the StatusBar will snap to the bottom line + /// of the console window. + /// This is the default. + /// + SnapToBottom = Default, + + /// + /// The StatusBar will act identically to MenuBar, snapping to the first line of the + /// console window. + /// + SnapToTop = 1, + } + + public StatusBarStyle Style { get; set; } = StatusBarStyle.Default; +#endif + public View Parent { get; set; } + 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. + /// It will be drawn in the lowest line of the terminal. /// /// A list of statusbar items. public StatusBar (StatusItem [] items) : base () @@ -71,8 +103,25 @@ namespace Terminal.Gui { ColorScheme = Colors.Menu; Application.OnLoad += () => { - this.X = Pos.Left (Application.Top); - this.Y = Pos.Bottom (Application.Top); + X = 0; + Height = 1; +#if SNAP_TO_TOP + switch (Style) { + case StatusBarStyle.SnapToTop: + X = 0; + Y = 0; + break; + case StatusBarStyle.SnapToBottom: +#endif + if (Parent == null) { + Y = Application.Driver.Rows - 1; // TODO: using internals of Application + } else { + Y = Pos.Bottom (Parent); + } +#if SNAP_TO_TOP + break; + } +#endif }; } diff --git a/Terminal.sln b/Terminal.sln index c5070ec8a..5bace6618 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -1,59 +1,59 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{B0A602CD-E176-449D-8663-64238D54F857}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui", "Terminal.Gui\Terminal.Gui.csproj", "{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Designer", "Designer\Designer.csproj", "{1228D992-C801-49BB-839A-7BD28A3FFF0A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.ActiveCfg = Debug|x86 - {B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.Build.0 = Debug|x86 - {B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.ActiveCfg = Release|x86 - {B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.Build.0 = Release|x86 - {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|x86.ActiveCfg = Debug|Any CPU - {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|x86.Build.0 = Debug|Any CPU - {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|x86.ActiveCfg = Release|Any CPU - {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|x86.Build.0 = Release|Any CPU - {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Debug|x86.ActiveCfg = Debug|x86 - {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Debug|x86.Build.0 = Debug|x86 - {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Release|x86.ActiveCfg = Release|x86 - {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - Policies = $0 - $0.TextStylePolicy = $1 - $1.FileWidth = 80 - $1.scope = text/x-csharp - $1.TabWidth = 8 - $1.IndentWidth = 8 - $0.CSharpFormattingPolicy = $2 - $2.scope = text/x-csharp - $2.IndentSwitchSection = False - $2.NewLinesForBracesInTypes = False - $2.NewLinesForBracesInProperties = False - $2.NewLinesForBracesInAccessors = False - $2.NewLinesForBracesInAnonymousMethods = False - $2.NewLinesForBracesInControlBlocks = False - $2.NewLinesForBracesInAnonymousTypes = False - $2.NewLinesForBracesInObjectCollectionArrayInitializers = False - $2.NewLinesForBracesInLambdaExpressionBody = False - $2.NewLineForElse = False - $2.NewLineForCatch = False - $2.NewLineForFinally = False - $2.NewLineForMembersInObjectInit = False - $2.NewLineForMembersInAnonymousTypes = False - $2.NewLineForClausesInQuery = False - $2.SpacingAfterMethodDeclarationName = True - $2.SpaceAfterMethodCallName = True - $2.SpaceBeforeOpenSquareBracket = True - $0.DotNetNamingPolicy = $3 - $0.StandardHeader = $4 - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{B0A602CD-E176-449D-8663-64238D54F857}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui", "Terminal.Gui\Terminal.Gui.csproj", "{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Designer", "Designer\Designer.csproj", "{1228D992-C801-49BB-839A-7BD28A3FFF0A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.ActiveCfg = Debug|x86 + {B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.Build.0 = Debug|x86 + {B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.ActiveCfg = Release|x86 + {B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.Build.0 = Release|x86 + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|x86.Build.0 = Debug|Any CPU + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|x86.ActiveCfg = Release|Any CPU + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|x86.Build.0 = Release|Any CPU + {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Debug|x86.ActiveCfg = Debug|x86 + {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Debug|x86.Build.0 = Debug|x86 + {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Release|x86.ActiveCfg = Release|x86 + {1228D992-C801-49BB-839A-7BD28A3FFF0A}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.TextStylePolicy = $1 + $1.FileWidth = 80 + $1.scope = text/x-csharp + $1.TabWidth = 8 + $1.IndentWidth = 8 + $0.CSharpFormattingPolicy = $2 + $2.scope = text/x-csharp + $2.IndentSwitchSection = False + $2.NewLinesForBracesInTypes = False + $2.NewLinesForBracesInProperties = False + $2.NewLinesForBracesInAccessors = False + $2.NewLinesForBracesInAnonymousMethods = False + $2.NewLinesForBracesInControlBlocks = False + $2.NewLinesForBracesInAnonymousTypes = False + $2.NewLinesForBracesInObjectCollectionArrayInitializers = False + $2.NewLinesForBracesInLambdaExpressionBody = False + $2.NewLineForElse = False + $2.NewLineForCatch = False + $2.NewLineForFinally = False + $2.NewLineForMembersInObjectInit = False + $2.NewLineForMembersInAnonymousTypes = False + $2.NewLineForClausesInQuery = False + $2.SpacingAfterMethodDeclarationName = True + $2.SpaceAfterMethodCallName = True + $2.SpaceBeforeOpenSquareBracket = True + $0.DotNetNamingPolicy = $3 + $0.StandardHeader = $4 + EndGlobalSection +EndGlobal From e66c391aeb95c958eaf285fb86d4ec9b2f12e7c5 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 20 Apr 2020 22:13:45 +0100 Subject: [PATCH 11/18] Fixes the functions of the Edit->Copy-Cut-Paste menu for the TextField that was not working well. --- Terminal.Gui/Views/TextField.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 81a8613cb..9b233cf99 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -473,18 +473,17 @@ namespace Terminal.Gui { public override bool MouseEvent (MouseEvent ev) { - if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked) && !ev.Flags.HasFlag (MouseFlags.Button1Pressed) && - !ev.Flags.HasFlag (MouseFlags.ReportMousePosition)) + if (!ev.Flags.HasFlag (MouseFlags.Button1Pressed) && !ev.Flags.HasFlag (MouseFlags.ReportMousePosition) && + !ev.Flags.HasFlag (MouseFlags.Button1Released)) return false; - if (ev.Flags == MouseFlags.Button1Clicked) { + if (ev.Flags == MouseFlags.Button1Pressed) { if (!HasFocus) SuperView.SetFocus (this); - int x = PositionCursor (ev); + PositionCursor (ev); if (isButtonReleased) ClearAllSelection (); isButtonReleased = true; - Application.UngrabMouse (); } else if (ev.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { int x = PositionCursor (ev); isButtonReleased = false; @@ -496,6 +495,9 @@ namespace Terminal.Gui { int x = PositionCursor (ev); if (SelectedLength != 0) ClearAllSelection (); + } else if (ev.Flags == MouseFlags.Button1Released) { + isButtonReleased = true; + Application.UngrabMouse (); } SetNeedsDisplay (); From 07a92f989e2c25f9d0169a696c031f1bdf313bf7 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 20 Apr 2020 22:25:45 +0100 Subject: [PATCH 12/18] Clear the TextField selected text when loses focus. --- Terminal.Gui/Views/TextField.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 9b233cf99..267e299e4 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -85,6 +85,8 @@ namespace Terminal.Gui { { if (Application.mouseGrabView != null && Application.mouseGrabView == this) Application.UngrabMouse (); + if (SelectedLength != 0) + ClearAllSelection (); } public override Rect Frame { From a9e62c06265efd05065a0f4e965a0c747cb24cb1 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 23 Apr 2020 02:55:35 +0100 Subject: [PATCH 13/18] Improves mouse performance #386. Provides a better async call. Ensures all the cycles of a simple click (Pressed, Released and Clicked). Only clears selected text if other view than MenuBar get focus. (#404) --- Terminal.Gui/Drivers/WindowsDriver.cs | 13 +++++++--- Terminal.Gui/MonoCurses/mainloop.cs | 35 +++++++++++++-------------- Terminal.Gui/Views/Menu.cs | 32 ++++++++++++++---------- Terminal.Gui/Views/TextField.cs | 2 +- 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index de779c614..c884868df 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -594,6 +594,8 @@ namespace Terminal.Gui { case WindowsConsole.EventType.Mouse: mouseHandler (ToDriverMouse (inputEvent.MouseEvent)); + if (IsButtonReleased) + mouseHandler (ToDriverMouse (inputEvent.MouseEvent)); break; case WindowsConsole.EventType.WindowBufferSize: @@ -635,7 +637,7 @@ namespace Terminal.Gui { if ((mouseEvent.EventFlags == 0 && LastMouseButtonPressed == null && !IsButtonDoubleClicked) || (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved && - mouseEvent.ButtonState != 0 && !IsButtonDoubleClicked)) { + mouseEvent.ButtonState != 0 && !IsButtonReleased && !IsButtonDoubleClicked)) { switch (mouseEvent.ButtonState) { case WindowsConsole.ButtonState.Button1Pressed: mouseFlag = MouseFlags.Button1Pressed; @@ -653,6 +655,7 @@ namespace Terminal.Gui { if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) { mouseFlag |= MouseFlags.ReportMousePosition; point = new Point (); + IsButtonReleased = false; } else { point = new Point () { X = mouseEvent.MousePosition.X, @@ -660,8 +663,8 @@ namespace Terminal.Gui { }; } LastMouseButtonPressed = mouseEvent.ButtonState; - } else if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed != null && !IsButtonReleased && - !IsButtonDoubleClicked) { + } else if ((mouseEvent.EventFlags == 0 || mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) && + LastMouseButtonPressed != null && !IsButtonReleased && !IsButtonDoubleClicked) { switch (LastMouseButtonPressed) { case WindowsConsole.ButtonState.Button1Pressed: mouseFlag = MouseFlags.Button1Released; @@ -677,7 +680,7 @@ namespace Terminal.Gui { } IsButtonReleased = true; } else if ((mouseEvent.EventFlags == 0 || mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) && - IsButtonReleased) { + IsButtonReleased) { var p = new Point () { X = mouseEvent.MousePosition.X, Y = mouseEvent.MousePosition.Y @@ -696,6 +699,8 @@ namespace Terminal.Gui { mouseFlag = MouseFlags.Button4Clicked; break; } + } else { + mouseFlag = 0; } LastMouseButtonPressed = null; IsButtonReleased = false; diff --git a/Terminal.Gui/MonoCurses/mainloop.cs b/Terminal.Gui/MonoCurses/mainloop.cs index f2927f8e0..21bd1571a 100644 --- a/Terminal.Gui/MonoCurses/mainloop.cs +++ b/Terminal.Gui/MonoCurses/mainloop.cs @@ -13,10 +13,10 @@ // 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 @@ -138,7 +138,7 @@ namespace Mono.Terminal { AddWatch (wakeupPipes [0], Condition.PollIn, ml => { read (wakeupPipes [0], ignore, (IntPtr)1); return true; - }); + }); } /// @@ -192,7 +192,7 @@ namespace Mono.Terminal { } } - bool IMainLoopDriver.EventsPending (bool wait) + bool IMainLoopDriver.EventsPending (bool wait) { long now = DateTime.UtcNow.Ticks; @@ -214,10 +214,10 @@ namespace Mono.Terminal { int ic; lock (mainLoop.idleHandlers) ic = mainLoop.idleHandlers.Count; - return n > 0 || mainLoop.timeouts.Count > 0 && ((mainLoop.timeouts.Keys [0] - DateTime.UtcNow.Ticks) < 0) || ic > 0; + return n > 0 || mainLoop.timeouts.Count > 0 && ((mainLoop.timeouts.Keys [0] - DateTime.UtcNow.Ticks) < 0) || ic > 0; } - void IMainLoopDriver.MainIteration () + void IMainLoopDriver.MainIteration () { if (pollmap != null) { foreach (var p in pollmap) { @@ -231,7 +231,7 @@ namespace Mono.Terminal { if (!watch.Callback (this.mainLoop)) descriptorWatchers.Remove (p.fd); } - } + } } } @@ -247,7 +247,7 @@ namespace Mono.Terminal { public Action WindowsKeyPressed; MainLoop mainLoop; - public NetMainLoop () + public NetMainLoop () { } @@ -258,16 +258,16 @@ namespace Mono.Terminal { windowsKeyResult = Console.ReadKey (true); keyReady.Set (); } - } + } void IMainLoopDriver.Setup (MainLoop mainLoop) { this.mainLoop = mainLoop; Thread readThread = new Thread (WindowsKeyReader); - readThread.Start (); + readThread.Start (); } - void IMainLoopDriver.Wakeup () + void IMainLoopDriver.Wakeup () { } @@ -298,7 +298,7 @@ namespace Mono.Terminal { if (WindowsKeyPressed!= null) WindowsKeyPressed (windowsKeyResult.Value); windowsKeyResult = null; - } + } } } @@ -346,7 +346,6 @@ namespace Mono.Terminal { action (); return false; }); - driver.Wakeup (); } /// @@ -373,7 +372,7 @@ namespace Mono.Terminal { { timeouts.Add ((DateTime.UtcNow + time).Ticks, timeout); } - + /// /// Adds a timeout to the mainloop. /// @@ -440,9 +439,9 @@ namespace Mono.Terminal { idleHandlers.Add (idle); } } - + bool running; - + /// /// Stops the mainloop. /// @@ -458,7 +457,7 @@ namespace Mono.Terminal { /// /// You can use this method if you want to probe if events are pending. /// Typically used if you need to flush the input queue while still - /// running some of your own code in your main thread. + /// running some of your own code in your main thread. /// public bool EventsPending (bool wait = false) { @@ -486,7 +485,7 @@ namespace Mono.Terminal { RunIdle(); } } - + /// /// Runs the mainloop. /// diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index 3b6bc1ccd..f940a77a5 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -432,7 +432,7 @@ namespace Terminal.Gui { } host.handled = false; bool disabled; - if (me.Flags == MouseFlags.Button1Pressed) { + if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { disabled = false; if (me.Y < 1) return true; @@ -444,7 +444,8 @@ namespace Terminal.Gui { if (item != null && !disabled) Run (barItems.Children [meY].Action); return true; - } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.ReportMousePosition) { + } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || + me.Flags == MouseFlags.ReportMousePosition) { disabled = false; if (me.Y < 1) return true; @@ -984,13 +985,14 @@ namespace Terminal.Gui { } handled = false; - if (me.Flags == MouseFlags.Button1Pressed || + if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || (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.Button1Pressed && !isMenuClosed) { + if (selected == i && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) && + !isMenuClosed) { Application.UngrabMouse (); if (Menus [i].IsTopLevel) { var menu = new Menu (this, i, 0, Menus [i]); @@ -998,7 +1000,8 @@ namespace Terminal.Gui { } else { CloseMenu (); } - } else if (me.Flags == MouseFlags.Button1Pressed && isMenuClosed) { + } else if ((me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) && + isMenuClosed) { if (Menus [i].IsTopLevel) { var menu = new Menu (this, i, 0, Menus [i]); menu.Run (Menus [i].Action); @@ -1033,7 +1036,8 @@ namespace Terminal.Gui { Application.GrabMouse (me.View); me.View.MouseEvent (me); } - } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags.HasFlag (MouseFlags.Button1Pressed)) { + } else if (!(me.View is MenuBar || me.View is Menu) && (me.Flags.HasFlag (MouseFlags.Button1Pressed) || + me.Flags == MouseFlags.Button1DoubleClicked)) { Application.UngrabMouse (); CloseAllMenus (); handled = false; @@ -1042,22 +1046,23 @@ namespace Terminal.Gui { handled = false; return false; } - } else if (isMenuClosed && me.Flags.HasFlag (MouseFlags.Button1Pressed)) { + } else if (isMenuClosed && (me.Flags.HasFlag (MouseFlags.Button1Pressed) || me.Flags == MouseFlags.Button1DoubleClicked)) { Application.GrabMouse (current); } else { handled = false; return false; } - //if (me.View != this && me.Flags != MouseFlags.Button1Pressed) + //if (me.View != this && (me.Flags != MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked)) // return true; - //else if (me.View != this && me.Flags == MouseFlags.Button1Pressed) { + //else if (me.View != this && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked)) { // Application.UngrabMouse (); // host.CloseAllMenus (); // return true; //} - //if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Pressed) + //if (!(me.View is MenuBar) && !(me.View is Menu) && (me.Flags != MouseFlags.Button1Pressed || + // me.Flags != MouseFlags.Button1DoubleClicked)) // return false; //if (Application.mouseGrabView != null) { @@ -1066,11 +1071,12 @@ namespace Terminal.Gui { // me.Y -= me.OfY; // me.View.MouseEvent (me); // return true; - // } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Pressed) { + // } else if (!(me.View is MenuBar || me.View is Menu) && (me.Flags == MouseFlags.Button1Pressed || + // me.Flags == MouseFlags.Button1DoubleClicked)) { // Application.UngrabMouse (); // CloseAllMenus (); // } - //} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Pressed) { + //} else if (!isMenuClosed && selected == -1 && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked)) { // Application.GrabMouse (this); // return true; //} @@ -1082,7 +1088,7 @@ namespace Terminal.Gui { // } else if (me.View != current && me.View is MenuBar && me.View is Menu) { // Application.UngrabMouse (); // Application.GrabMouse (me.View); - // } else if (me.Flags == MouseFlags.Button1Pressed) { + // } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { // Application.UngrabMouse (); // CloseMenu (); // } diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 267e299e4..70e9964ec 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -85,7 +85,7 @@ namespace Terminal.Gui { { if (Application.mouseGrabView != null && Application.mouseGrabView == this) Application.UngrabMouse (); - if (SelectedLength != 0) + if (SelectedLength != 0 && !(Application.mouseGrabView is MenuBar)) ClearAllSelection (); } From c310716fcdfffd2df8bee59cf9b4e68c68078b83 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 29 Apr 2020 16:08:25 +0100 Subject: [PATCH 14/18] Faster handling of the mouse in ScrollView with the addition of an uninterrupted click on the mouse features. Changed the Wakeup method to reset the events to update the screen. (#409) --- Terminal.Gui/Drivers/WindowsDriver.cs | 27 ++++++++++++++++++- Terminal.Gui/Views/ScrollView.cs | 39 +++++++++++++++++++-------- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index c884868df..44c865c0e 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -499,7 +499,9 @@ namespace Terminal.Gui { void IMainLoopDriver.Wakeup () { - tokenSource.Cancel (); + //tokenSource.Cancel (); + eventReady.Reset (); + eventReady.Set (); } bool IMainLoopDriver.EventsPending (bool wait) @@ -610,6 +612,7 @@ namespace Terminal.Gui { } WindowsConsole.ButtonState? LastMouseButtonPressed = null; + bool IsButtonPressed = false; bool IsButtonReleased = false; bool IsButtonDoubleClicked = false; Point point; @@ -632,6 +635,7 @@ namespace Terminal.Gui { // map to the correct clicked event. if ((LastMouseButtonPressed != null || IsButtonReleased) && mouseEvent.ButtonState != 0) { LastMouseButtonPressed = null; + IsButtonPressed = false; IsButtonReleased = false; } @@ -663,6 +667,26 @@ namespace Terminal.Gui { }; } LastMouseButtonPressed = mouseEvent.ButtonState; + IsButtonPressed = true; + + if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) { + Task.Run (async () => { + while (IsButtonPressed) { + await Task.Delay (200); + var me = new MouseEvent () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y, + Flags = mouseFlag + }; + + if (IsButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0) { + mouseHandler (me); + mainLoop.Driver.Wakeup (); + } + } + }); + } + } else if ((mouseEvent.EventFlags == 0 || mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) && LastMouseButtonPressed != null && !IsButtonReleased && !IsButtonDoubleClicked) { switch (LastMouseButtonPressed) { @@ -678,6 +702,7 @@ namespace Terminal.Gui { mouseFlag = MouseFlags.Button4Released; break; } + IsButtonPressed = false; IsButtonReleased = true; } else if ((mouseEvent.EventFlags == 0 || mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) && IsButtonReleased) { diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index 318fba3df..58faca45f 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -114,21 +114,19 @@ namespace Terminal.Gui { var by1 = position * bh / Size; var by2 = (position + bh) * bh / Size; - Move (col, 0); Driver.AddRune ('^'); Move (col, Bounds.Height - 1); Driver.AddRune ('v'); for (int y = 0; y < bh; y++) { Move (col, y+1); - - if (y < by1 || y > by2) + if (y < by1 - 1 || y > by2) special = Driver.Stipple; else { - if (by2 - by1 == 0) + if (by2 - by1 == 0 && by1 < bh - 1) special = Driver.Diamond; else { - if (y == by1) + if (y == by1 - 1) special = Driver.TopTee; else if (y == by2) special = Driver.BottomTee; @@ -192,7 +190,8 @@ namespace Terminal.Gui { public override bool MouseEvent(MouseEvent me) { - if (me.Flags != MouseFlags.Button1Clicked) + if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && + !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) return false; int location = vertical ? me.Y : me.X; @@ -208,11 +207,23 @@ namespace Terminal.Gui { if (location == 0) { if (pos > 0) SetPosition (pos - 1); - } else if (location == barsize + 1){ + } else if (location == barsize + 1) { if (pos + 1 + barsize < Size) SetPosition (pos + 1); } else { - Console.WriteLine ("TODO at ScrollBarView"); + var b1 = pos * barsize / Size; + var b2 = (pos + barsize) * barsize / Size; + + if (b2 == 0 && location == 1 && pos == 0 || + (b2 == barsize && location == barsize) || + (location > b1 && location < b2)) { + return true; + } else if (location <= barsize) { + if (location > 1 && location >= b2) + SetPosition (Math.Min (pos + barsize, Size)); + else if (location <= b2 && pos > 0 || pos > 0) + SetPosition (Math.Max (pos - barsize, 0)); + } } } @@ -309,7 +320,7 @@ namespace Terminal.Gui { set { if (value == showHorizontalScrollIndicator) return; - + showHorizontalScrollIndicator = value; SetNeedsDisplay (); if (value) @@ -338,7 +349,7 @@ namespace Terminal.Gui { set { if (value == showVerticalScrollIndicator) return; - + showVerticalScrollIndicator = value; SetNeedsDisplay (); if (value) @@ -431,7 +442,7 @@ namespace Terminal.Gui { var nx = Math.Max (-contentSize.Width, contentOffset.X - cols); if (nx == contentOffset.X) return false; - + ContentOffset = new Point (nx, contentOffset.Y); return true; } @@ -461,6 +472,12 @@ namespace Terminal.Gui { case Key.CursorRight: return ScrollRight (1); + case Key.Home: + return ScrollUp (contentSize.Height); + + case Key.End: + return ScrollDown (contentSize.Height); + } return false; } From 42fe890072f2ca1b28344fe70f1395b44ecf3e11 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 29 Apr 2020 16:08:42 +0100 Subject: [PATCH 15/18] Fsharp example updated with the Demo version. (#407) * Added solution file to the FSharpExample project. * Updated Terminal.Gui and FSharp.Core nuget packages * Removed the Terminal.Gui nuget package and added the project itself. * Added a new FrameView constructor with an array of views. * Changed FSharp Example with the Example Demo file version. * Fixes the Box10x() issue. --- FSharpExample/.editorconfig | 4 + FSharpExample/FSharpExample.fsproj | 6 +- FSharpExample/FSharpExample.sln | 31 ++ FSharpExample/Program.fs | 456 +++++++++++++++++++++++++++-- Terminal.Gui/Views/FrameView.cs | 24 +- 5 files changed, 491 insertions(+), 30 deletions(-) create mode 100644 FSharpExample/.editorconfig create mode 100644 FSharpExample/FSharpExample.sln diff --git a/FSharpExample/.editorconfig b/FSharpExample/.editorconfig new file mode 100644 index 000000000..924c8fe00 --- /dev/null +++ b/FSharpExample/.editorconfig @@ -0,0 +1,4 @@ +[*.fs] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/FSharpExample/FSharpExample.fsproj b/FSharpExample/FSharpExample.fsproj index 3e9230e44..d251896a4 100644 --- a/FSharpExample/FSharpExample.fsproj +++ b/FSharpExample/FSharpExample.fsproj @@ -10,7 +10,11 @@ - + + + + + diff --git a/FSharpExample/FSharpExample.sln b/FSharpExample/FSharpExample.sln new file mode 100644 index 000000000..90e5c5be2 --- /dev/null +++ b/FSharpExample/FSharpExample.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30011.22 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpExample", "FSharpExample.fsproj", "{6E4DF691-FA5F-4D7C-8DBC-8656103C5CB1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Gui", "..\Terminal.Gui\Terminal.Gui.csproj", "{FA48E777-1308-489D-95A0-89DE46B65A93}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6E4DF691-FA5F-4D7C-8DBC-8656103C5CB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E4DF691-FA5F-4D7C-8DBC-8656103C5CB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E4DF691-FA5F-4D7C-8DBC-8656103C5CB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E4DF691-FA5F-4D7C-8DBC-8656103C5CB1}.Release|Any CPU.Build.0 = Release|Any CPU + {FA48E777-1308-489D-95A0-89DE46B65A93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA48E777-1308-489D-95A0-89DE46B65A93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA48E777-1308-489D-95A0-89DE46B65A93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA48E777-1308-489D-95A0-89DE46B65A93}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A023D2E3-EF0F-4986-8E6C-323F967788B7} + EndGlobalSection +EndGlobal diff --git a/FSharpExample/Program.fs b/FSharpExample/Program.fs index 6a9418fa8..a2ec888c5 100644 --- a/FSharpExample/Program.fs +++ b/FSharpExample/Program.fs @@ -1,37 +1,441 @@ // Learn more about F# at http://fsharp.org -open System open Terminal.Gui +open System +open Mono.Terminal +open System.Collections.Generic +open System.Diagnostics +open System.Globalization +open System.Reflection open NStack +type Demo() = class end + let ustr (x:string) = ustring.Make(x) + let mutable ml2 = Unchecked.defaultof