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