diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index e0ebe2671..34b63ceb7 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -321,7 +321,7 @@ public static partial class Application // Keyboard handling /// /// /// - /// This version of AddCommand is for commands that do not require a . + /// This version of AddCommand is for commands that do not require a . /// /// /// The command. diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index 613b35320..61c21f4e6 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -3,14 +3,9 @@ namespace Terminal.Gui; #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// -/// Provides context for a that is being invoked. +/// Provides context for a invocation. /// -/// -/// -/// To define a that is invoked with context, -/// use . -/// -/// +/// . #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved public record struct CommandContext : ICommandContext { @@ -19,22 +14,17 @@ public record struct CommandContext : ICommandContext /// /// /// - /// - public CommandContext (Command command, TBindingType? binding, object? data = null) + public CommandContext (Command command, TBindingType? binding) { Command = command; Binding = binding; - Data = data; } + /// + public Command Command { get; set; } + /// /// The keyboard or mouse minding that was used to invoke the , if any. /// public TBindingType? Binding { get; set; } - - /// - public Command Command { get; set; } - - /// - public object? Data { get; set; } } \ No newline at end of file diff --git a/Terminal.Gui/Input/CommandEventArgs.cs b/Terminal.Gui/Input/CommandEventArgs.cs index 9782c7dfd..70e2751d7 100644 --- a/Terminal.Gui/Input/CommandEventArgs.cs +++ b/Terminal.Gui/Input/CommandEventArgs.cs @@ -9,7 +9,10 @@ namespace Terminal.Gui; public class CommandEventArgs : CancelEventArgs { /// - /// The context for the command. + /// The context for the command, if any. /// - public required ICommandContext Context { get; init; } + /// + /// If the command was invoked without context. + /// + public required ICommandContext? Context { get; init; } } diff --git a/Terminal.Gui/Input/ICommandContext.cs b/Terminal.Gui/Input/ICommandContext.cs index 77124922a..1aa5c8660 100644 --- a/Terminal.Gui/Input/ICommandContext.cs +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -11,9 +11,9 @@ public interface ICommandContext /// public Command Command { get; set; } - // TODO: Remove this property. With CommandContext being a generic type, there should be no need for arbitrary data. - /// - /// Arbitrary data. - /// - public object? Data { get; set; } + //// TODO: Remove this property. With CommandContext being a generic type, there should be no need for arbitrary data. + ///// + ///// Arbitrary data. + ///// + //public object? Data { get; set; } } diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index e420698e3..a67aabb50 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -21,20 +21,20 @@ public record struct KeyBinding { Commands = commands; Scope = scope; - Context = context; + Data = context; } /// Initializes a new instance. /// The commands this key binding will invoke. /// The scope of the . /// The view the key binding is bound to. - /// Arbitrary context that can be associated with this key binding. - public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, object? context = null) + /// Arbitrary data that can be associated with this key binding. + public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, object? data = null) { Commands = commands; Scope = scope; BoundView = boundView; - Context = context; + Data = data; } /// The commands this key binding will invoke. @@ -46,7 +46,7 @@ public record struct KeyBinding /// /// The Key that is bound to the . /// - public Key Key { get; set; } + public Key? Key { get; set; } /// The view the key binding is bound to. public View? BoundView { get; set; } @@ -54,5 +54,5 @@ public record struct KeyBinding /// /// Arbitrary context that can be associated with this key binding. /// - public object? Context { get; set; } + public object? Data { get; set; } } diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index 1c989862a..e0cebe7cf 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -431,6 +431,7 @@ public class KeyBindings { if (scope.HasFlag (binding.Scope)) { + binding.Key = key; return true; } } diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 3ceee4aaf..93af18c25 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; /// -/// Provides a collection of objects bound to a combination of . +/// Provides a collection of objects bound to a combination of . /// /// /// @@ -15,15 +15,13 @@ public class MouseBindings public MouseBindings () { } /// Adds a to the collection. - /// + /// /// - public void Add (MouseFlags mouseFlag, MouseBinding binding) + public void Add (MouseEventArgs mouseEvent, MouseBinding binding) { - if (TryGet (mouseFlag, out MouseBinding _)) + if (TryGet (mouseEvent, out MouseBinding _)) { - throw new InvalidOperationException (@$"A binding for {mouseFlag} exists ({binding})."); - - //Bindings [key] = binding; + throw new InvalidOperationException (@$"A binding for {mouseEvent} exists ({binding})."); } @@ -31,7 +29,7 @@ public class MouseBindings // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus // IMPORTANT: Apply will update the Dictionary with the new key, but the old key will still be in the dictionary. // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - Bindings.Add (mouseFlag, binding); + Bindings.Add (mouseEvent, binding); } /// @@ -45,15 +43,15 @@ public class MouseBindings /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch /// focus to another view and perform multiple commands there). /// - /// The mouse flags to check. + /// The mouse flags to check. /// - /// The command to invoked on the when is received. When - /// multiple commands are provided,they will be applied in sequence. The bound event will be + /// The command to invoked on the when is received. When + /// multiple commands are provided,they will be applied in sequence. The bound event will be /// consumed if any took effect. /// - public void Add (MouseFlags mouseFlags, params Command [] commands) + public void Add (MouseEventArgs mouseEvents, params Command [] commands) { - if (mouseFlags == MouseFlags.None) + if (mouseEvents.Flags == MouseFlags.None) { throw new ArgumentException (@"Invalid MouseFlag", nameof (commands)); } @@ -63,24 +61,24 @@ public class MouseBindings throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } - if (TryGet (mouseFlags, out MouseBinding binding)) + if (TryGet (mouseEvents, out MouseBinding binding)) { - throw new InvalidOperationException (@$"A binding for {mouseFlags} exists ({binding})."); + throw new InvalidOperationException (@$"A binding for {mouseEvents} exists ({binding})."); } - Add (mouseFlags, new MouseBinding (commands)); + Add (mouseEvents, new MouseBinding (commands)); } // TODO: Add a dictionary comparer that ignores Scope // TODO: This should not be public! /// The collection of objects. - public Dictionary Bindings { get; } = new (); + public Dictionary Bindings { get; } = new (); /// - /// Gets the that are bound. + /// Gets the that are bound. /// /// - public IEnumerable GetBoundMouseFlags () + public IEnumerable GetBoundMouseEventArgs () { return Bindings.Keys; } @@ -95,38 +93,38 @@ public class MouseBindings /// public void Clear (params Command [] command) { - KeyValuePair [] kvps = Bindings + KeyValuePair [] kvps = Bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); - foreach (KeyValuePair kvp in kvps) + foreach (KeyValuePair kvp in kvps) { Remove (kvp.Key); } } - /// Gets the for the specified combination of . - /// + /// Gets the for the specified combination of . + /// /// - public MouseBinding Get (MouseFlags mouseFlags) + public MouseBinding Get (MouseEventArgs mouseEvents) { - if (TryGet (mouseFlags, out MouseBinding binding)) + if (TryGet (mouseEvents, out MouseBinding binding)) { return binding; } - throw new InvalidOperationException ($"{mouseFlags} is not bound."); + throw new InvalidOperationException ($"{mouseEvents} is not bound."); } - /// Gets the array of s bound to if it exists. - /// The key to check. + /// Gets the array of s bound to if it exists. + /// The key to check. /// - /// The array of s if is bound. An empty array + /// The array of s if is bound. An empty array /// if not. /// - public Command [] GetCommands (MouseFlags mouseFlags) + public Command [] GetCommands (MouseEventArgs mouseEvents) { - if (TryGet (mouseFlags, out MouseBinding bindings)) + if (TryGet (mouseEvents, out MouseBinding bindings)) { return bindings.Commands; } @@ -134,83 +132,83 @@ public class MouseBindings return []; } - /// Gets the first combination of bound to the set of commands specified by . + /// Gets the first combination of bound to the set of commands specified by . /// The set of commands to search. - /// The first combination of bound to the set of commands specified by . if the set of caommands was not found. - public MouseFlags? GetMouseFlagsFromCommands (params Command [] commands) + /// The first combination of bound to the set of commands specified by . if the set of caommands was not found. + public MouseEventArgs? GetMouseEventArgsFromCommands (params Command [] commands) { return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } - /// Gets combination of bound to the set of commands specified by . + /// Gets combination of bound to the set of commands specified by . /// The set of commands to search. - /// The combination of bound to the set of commands specified by . An empty list if the set of caommands was not found. - public IEnumerable GetAllMouseFlagsFromCommands (params Command [] commands) + /// The combination of bound to the set of commands specified by . An empty list if the set of caommands was not found. + public IEnumerable GetAllMouseEventArgsFromCommands (params Command [] commands) { return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); } /// Removes a from the collection. - /// - public void Remove (MouseFlags mouseFlags) + /// + public void Remove (MouseEventArgs mouseEvents) { - if (!TryGet (mouseFlags, out MouseBinding _)) + if (!TryGet (mouseEvents, out MouseBinding _)) { return; } - Bindings.Remove (mouseFlags); + Bindings.Remove (mouseEvents); } - /// Replaces the commands already bound to a combination of . + /// Replaces the commands already bound to a combination of . /// /// - /// If the combination of is not already bound, it will be added. + /// If the combination of is not already bound, it will be added. /// /// - /// The combination of bound to the command to be replaced. + /// The combination of bound to the command to be replaced. /// The set of commands to replace the old ones with. - public void ReplaceCommands (MouseFlags mouseFlags, params Command [] commands) + public void ReplaceCommands (MouseEventArgs mouseEvents, params Command [] commands) { - if (TryGet (mouseFlags, out MouseBinding binding)) + if (TryGet (mouseEvents, out MouseBinding binding)) { binding.Commands = commands; } else { - Add (mouseFlags, commands); + Add (mouseEvents, commands); } } - /// Replaces a combination already bound to a set of s. + /// Replaces a combination already bound to a set of s. /// - /// The to be replaced. - /// The new to be used. If no action will be taken. - public void ReplaceKey (MouseFlags oldMouseFlags, MouseFlags newMouseFlags) + /// The to be replaced. + /// The new to be used. If no action will be taken. + public void ReplaceKey (MouseEventArgs oldMouseEventArgs, MouseEventArgs newMouseEventArgs) { - if (!TryGet (oldMouseFlags, out MouseBinding _)) + if (!TryGet (oldMouseEventArgs, out MouseBinding _)) { - throw new InvalidOperationException ($"Key {oldMouseFlags} is not bound."); + throw new InvalidOperationException ($"Key {oldMouseEventArgs} is not bound."); } - MouseBinding value = Bindings [oldMouseFlags]; - Remove (oldMouseFlags); - Add (newMouseFlags, value); + MouseBinding value = Bindings [oldMouseEventArgs]; + Remove (oldMouseEventArgs); + Add (newMouseEventArgs, value); } - /// Gets the commands bound with the specified . + /// Gets the commands bound with the specified . /// - /// The key to check. + /// The key to check. /// /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are /// found; otherwise, null. This parameter is passed uninitialized. /// /// if the mouse flags are bound; otherwise . - public bool TryGet (MouseFlags mouseFlags, out MouseBinding binding) + public bool TryGet (MouseEventArgs mouseEvents, out MouseBinding binding) { binding = new ([]); - return Bindings.TryGetValue (mouseFlags, out binding); + return Bindings.TryGetValue (mouseEvents, out binding); } } diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index 06431eb73..c3a921bd6 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -30,7 +30,7 @@ public partial class View // Command APIs }); // Space or single-click - Raise Selecting - AddCommand (Command.Select, (ctx) => + AddCommand (Command.Select, ctx => { if (RaiseSelecting (ctx) is true) { @@ -54,7 +54,7 @@ public partial class View // Command APIs /// /// /// - /// The event should raised after the state of the View has changed (after is raised). + /// The event should be raised after the state of the View has changed (after is raised). /// /// /// If the Accepting event is not handled, will be invoked on the SuperView, enabling default Accept behavior. @@ -65,11 +65,11 @@ public partial class View // Command APIs /// /// /// - /// if no event was raised; input proessing should continue. - /// if the event was raised and was not handled (or cancelled); input proessing should continue. - /// if the event was raised and handled (or cancelled); input proessing should stop. + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. /// - protected bool? RaiseAccepting (ICommandContext ctx) + protected bool? RaiseAccepting (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; @@ -135,14 +135,14 @@ public partial class View // Command APIs /// event. The default handler calls this method. /// /// - /// The event should raised after the state of the View has been changed and before see . + /// The event should be raised after the state of the View has been changed and before see . /// /// - /// if no event was raised; input proessing should continue. - /// if the event was raised and was not handled (or cancelled); input proessing should continue. - /// if the event was raised and handled (or cancelled); input proessing should stop. + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. /// - protected bool? RaiseSelecting (ICommandContext ctx) + protected bool? RaiseSelecting (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; @@ -179,9 +179,9 @@ public partial class View // Command APIs /// event. The default handler calls this method. /// /// - /// if no event was raised; input proessing should continue. - /// if the event was raised and was not handled (or cancelled); input proessing should continue. - /// if the event was raised and handled (or cancelled); input proessing should stop. + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. /// protected bool? RaiseHandlingHotKey () { @@ -219,11 +219,11 @@ public partial class View // Command APIs /// /// Function signature commands. /// - /// Provides information about the circumstances of invoking the command (e.g. ) + /// Provides context about the circumstances of invoking the command. /// - /// if no command was found; input proessing should continue. - /// if the command was invoked and was not handled (or cancelled); input proessing should continue. - /// if the command was invoked the command was handled (or cancelled); input proessing should stop. + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. /// public delegate bool? CommandImplementation (ICommandContext? ctx); @@ -239,7 +239,7 @@ public partial class View // Command APIs /// /// /// - /// This version of AddCommand is for commands that require . + /// This version of AddCommand is for commands that require . /// /// /// The command. @@ -258,7 +258,7 @@ public partial class View // Command APIs /// /// /// - /// This version of AddCommand is for commands that do not require a . + /// This version of AddCommand is for commands that do not require context. /// If the command requires context, use /// /// @@ -275,12 +275,11 @@ public partial class View // Command APIs /// Invokes the specified commands. /// /// The set of commands to invoke. - /// The key that caused the command to be invoked, if any. This will be passed as context with the command. - /// The key binding that was bound to the key and caused the invocation, if any. This will be passed as context with the command. + /// The binding that caused the invocation, if any. This will be passed as context with the command. /// - /// if no command was found; input proessing should continue. - /// if at least one command was invoked and was not handled (or cancelled); input proessing should continue. - /// if at least one command was invoked the command was handled (or cancelled); input proessing should stop. + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. /// public bool? InvokeCommands (Command [] commands, TBindingType binding) { @@ -315,30 +314,34 @@ public partial class View // Command APIs /// Invokes the specified command. /// /// The command to invoke. - /// + /// The binding that caused the invocation, if any. This will be passed as context with the command. /// - /// if no command was found; input proessing should continue. - /// if the command was invoked and was not handled (or cancelled); input proessing should continue. - /// if the command was invoked the command was handled (or cancelled); input proessing should stop. + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. /// public bool? InvokeCommand (Command command, TBindingType binding) { if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { - return implementation (binding as ICommandContext); + return implementation (new CommandContext () + { + Command = command, + Binding = binding, + }); } return null; } /// - /// Invokes the specified command. + /// Invokes the specified command without context. /// /// The command to invoke. /// - /// if no command was found; input proessing should continue. - /// if the command was invoked and was not handled (or cancelled); input proessing should continue. - /// if the command was invoked the command was handled (or cancelled); input proessing should stop. + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. /// public bool? InvokeCommand (Command command) { diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index ea1a95f68..f699f5686 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -205,9 +205,10 @@ public class ColorPicker16 : View AddCommand (Command.Select, (ctx) => { bool set = false; - if (ctx.Data is MouseEventArgs me) + + if (ctx is CommandContext { Binding: { } } mouseCommandContext) { - Cursor = new (me.Position.X / _boxWidth, me.Position.Y / _boxHeight); + Cursor = new (mouseCommandContext.Binding.Position.X / _boxWidth, mouseCommandContext.Binding.Position.Y / _boxHeight); set = true; } return RaiseAccepting (ctx) == true || set; diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 4fcdc315a..2e3763a9b 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -80,7 +80,11 @@ public class ComboBox : View, IDesignable // Things this view knows how to do AddCommand (Command.Accept, (ctx) => { - if (ctx.Data == _search) + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + if (keyCommandContext.Binding.Data == _search) { return null; } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 89ad3c660..6cc15b338 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -143,8 +143,12 @@ public class ListView : View, IDesignable AddCommand (Command.SelectAll, (ctx) => { - // BUGBUG: This probably isn't right - return MarkAll ((bool)ctx!.Data); + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return keyCommandContext.Binding.Data is { } && MarkAll ((bool)keyCommandContext.Binding.Data); }); // Default keybindings for all ListViews diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 6e620c91d..9c7072538 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -1,7 +1,5 @@ #nullable enable -using static System.Formats.Asn1.AsnWriter; - namespace Terminal.Gui; /// @@ -10,76 +8,115 @@ namespace Terminal.Gui; /// internal sealed class Menu : View { - private readonly MenuBarItem? _barItems; - private readonly MenuBar _host; + public Menu () + { + if (Application.Top is { }) + { + Application.Top.DrawComplete += Top_DrawComplete; + Application.Top.SizeChanging += Current_TerminalResized; + } + + Application.MouseEvent += Application_RootMouseEvent; + + // Things this view knows how to do + AddCommand (Command.Up, () => MoveUp ()); + AddCommand (Command.Down, () => MoveDown ()); + + AddCommand ( + Command.Left, + () => + { + _host!.PreviousMenu (true); + + return true; + } + ); + + AddCommand ( + Command.Cancel, + () => + { + CloseAllMenus (); + + return true; + } + ); + + AddCommand ( + Command.Accept, + () => + { + RunSelected (); + + return true; + } + ); + + AddCommand ( + Command.Select, + ctx => + { + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return _host?.SelectItem ((keyCommandContext.Binding.Data as MenuItem)!); + }); + + AddCommand ( + Command.Toggle, + ctx => + { + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return ExpandCollapse ((keyCommandContext.Binding.Data as MenuItem)!); + }); + + AddCommand ( + Command.HotKey, + ctx => + { + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return _host?.SelectItem ((keyCommandContext.Binding.Data as MenuItem)!); + }); + + // Default key bindings for this view + KeyBindings.Add (Key.CursorUp, Command.Up); + KeyBindings.Add (Key.CursorDown, Command.Down); + KeyBindings.Add (Key.CursorLeft, Command.Left); + KeyBindings.Add (Key.CursorRight, Command.Right); + KeyBindings.Add (Key.Esc, Command.Cancel); + } + internal int _currentChild; internal View? _previousSubFocused; - - internal static Rectangle MakeFrame (int x, int y, MenuItem? []? items, Menu? parent = null) - { - if (items is null || items.Length == 0) - { - return Rectangle.Empty; - } - - int minX = x; - int minY = y; - const int borderOffset = 2; // This 2 is for the space around - int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset; - int maxH = items.Length + borderOffset; - - if (parent is { } && x + maxW > Driver.Cols) - { - minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); - } - - if (y + maxH > Driver.Rows) - { - minY = Math.Max (Driver.Rows - maxH, 0); - } - - return new (minX, minY, maxW, maxH); - } - - internal required MenuBar Host - { - get => _host; - init - { - ArgumentNullException.ThrowIfNull (value); - _host = value; - } - } - - internal required MenuBarItem? BarItems - { - get => _barItems!; - init - { - ArgumentNullException.ThrowIfNull (value); - _barItems = value; - - // Debugging aid so ToString() is helpful - Text = _barItems.Title; - } - } - - internal Menu? Parent { get; init; } + private readonly MenuBarItem? _barItems; + private readonly MenuBar _host; public override void BeginInit () { base.BeginInit (); - var frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent); + Rectangle frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent); if (Frame.X != frame.X) { X = frame.X; } + if (Frame.Y != frame.Y) { Y = frame.Y; } + Width = frame.Width; Height = frame.Height; @@ -94,6 +131,7 @@ internal sealed class Menu : View if (menuItem.ShortcutKey != Key.Empty) { KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem); + // Remove an existent ShortcutKey menuItem._menuBar.KeyBindings.Remove (menuItem.ShortcutKey!); menuItem._menuBar.KeyBindings.Add (menuItem.ShortcutKey!, keyBinding); @@ -155,61 +193,281 @@ internal sealed class Menu : View AddKeyBindingsHotKey (_barItems); } - public Menu () + public override Point? PositionCursor () { - if (Application.Top is { }) + if (_host.IsMenuOpen) { - Application.Top.DrawComplete += Top_DrawComplete; - Application.Top.SizeChanging += Current_TerminalResized; + if (_barItems!.IsTopLevel) + { + return _host.PositionCursor (); + } + + Move (2, 1 + _currentChild); + + return null; // Don't show the cursor } - Application.MouseEvent += Application_RootMouseEvent; - - // Things this view knows how to do - AddCommand (Command.Up, () => MoveUp ()); - AddCommand (Command.Down, () => MoveDown ()); - - AddCommand ( - Command.Left, - () => - { - _host!.PreviousMenu (true); - - return true; - } - ); - - AddCommand ( - Command.Cancel, - () => - { - CloseAllMenus (); - - return true; - } - ); - - AddCommand ( - Command.Accept, - () => - { - RunSelected (); - - return true; - } - ); - AddCommand (Command.Select, ctx => _host?.SelectItem ((ctx.Data as MenuItem)!)); - AddCommand (Command.Toggle, ctx => ExpandCollapse ((ctx.Data as MenuItem)!)); - AddCommand (Command.HotKey, ctx => _host?.SelectItem ((ctx.Data as MenuItem)!)); - - // Default key bindings for this view - KeyBindings.Add (Key.CursorUp, Command.Up); - KeyBindings.Add (Key.CursorDown, Command.Down); - KeyBindings.Add (Key.CursorLeft, Command.Left); - KeyBindings.Add (Key.CursorRight, Command.Right); - KeyBindings.Add (Key.Esc, Command.Cancel); + return _host.PositionCursor (); } + public void Run (Action? action) + { + if (action is null) + { + return; + } + + Application.UngrabMouse (); + _host.CloseAllMenus (); + Application.LayoutAndDraw (true); + + _host.Run (action); + } + + protected override void Dispose (bool disposing) + { + RemoveKeyBindingsHotKey (_barItems); + + if (Application.Top is { }) + { + Application.Top.DrawComplete -= Top_DrawComplete; + Application.Top.SizeChanging -= Current_TerminalResized; + } + + Application.MouseEvent -= Application_RootMouseEvent; + base.Dispose (disposing); + } + + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) + { + if (!newHasFocus) + { + _host.LostFocus (previousFocusedView!); + } + } + + /// + protected override bool OnKeyDownNotHandled (Key keyEvent) + { + // We didn't handle the key, pass it on to host + return _host.InvokeCommandsBoundToKey (keyEvent) == true; + } + + protected override bool OnMouseEvent (MouseEventArgs me) + { + if (!_host._handled && !_host.HandleGrabView (me, this)) + { + return false; + } + + _host._handled = false; + bool disabled; + + if (me.Flags == MouseFlags.Button1Clicked) + { + disabled = false; + + if (me.Position.Y < 0) + { + return me.Handled = true; + } + + if (me.Position.Y >= _barItems!.Children!.Length) + { + return me.Handled = true; + } + + MenuItem item = _barItems.Children [me.Position.Y]!; + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (item is null || !item.IsEnabled ()) + { + disabled = true; + } + + if (disabled) + { + return me.Handled = true; + } + + _currentChild = me.Position.Y; + RunSelected (); + + return me.Handled = true; + } + + if (me.Flags != MouseFlags.Button1Pressed + && me.Flags != MouseFlags.Button1DoubleClicked + && me.Flags != MouseFlags.Button1TripleClicked + && me.Flags != MouseFlags.ReportMousePosition + && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) + { + return false; + } + + { + disabled = false; + + if (me.Position.Y < 0 || me.Position.Y >= _barItems!.Children!.Length) + { + return me.Handled = true; + } + + MenuItem item = _barItems.Children [me.Position.Y]!; + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (item is null) + { + return me.Handled = true; + } + + if (item.IsEnabled () != true) + { + disabled = true; + } + + if (!disabled) + { + _currentChild = me.Position.Y; + } + + if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) + { + SetNeedsDraw (); + SetParentSetNeedsDisplay (); + + return me.Handled = true; + } + + _host.OnMenuOpened (); + + return me.Handled = true; + } + } + + /// + protected override void OnVisibleChanged () + { + base.OnVisibleChanged (); + + if (Visible) + { + Application.MouseEvent += Application_RootMouseEvent; + } + else + { + Application.MouseEvent -= Application_RootMouseEvent; + } + } + + internal required MenuBarItem? BarItems + { + get => _barItems!; + init + { + ArgumentNullException.ThrowIfNull (value); + _barItems = value; + + // Debugging aid so ToString() is helpful + Text = _barItems.Title; + } + } + + internal bool CheckSubMenu () + { + if (_currentChild == -1 || _barItems?.Children? [_currentChild] is null) + { + return true; + } + + MenuBarItem? subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]!); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (subMenu is { }) + { + int pos = -1; + + if (_host._openSubMenu is { }) + { + pos = _host._openSubMenu.FindIndex (o => o._barItems == subMenu); + } + + if (pos == -1 + && this != _host.OpenCurrentMenu + && subMenu.Children != _host.OpenCurrentMenu!._barItems!.Children + && !_host.CloseMenu (false, true)) + { + return false; + } + + _host.Activate (_host._selected, pos, subMenu); + } + else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems!.IsSubMenuOf (_barItems.Children [_currentChild]!) == false) + { + return _host.CloseMenu (false, true); + } + else + { + SetNeedsDraw (); + SetParentSetNeedsDisplay (); + } + + return true; + } + + internal Attribute DetermineColorSchemeFor (MenuItem? item, int index) + { + if (item is null) + { + return GetNormalColor (); + } + + if (index == _currentChild) + { + return GetFocusColor (); + } + + return !item.IsEnabled () ? ColorScheme!.Disabled : GetNormalColor (); + } + + internal required MenuBar Host + { + get => _host; + init + { + ArgumentNullException.ThrowIfNull (value); + _host = value; + } + } + + internal static Rectangle MakeFrame (int x, int y, MenuItem? []? items, Menu? parent = null) + { + if (items is null || items.Length == 0) + { + return Rectangle.Empty; + } + + int minX = x; + int minY = y; + const int borderOffset = 2; // This 2 is for the space around + int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset; + int maxH = items.Length + borderOffset; + + if (parent is { } && x + maxW > Driver.Cols) + { + minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); + } + + if (y + maxH > Driver.Rows) + { + minY = Math.Max (Driver.Rows - maxH, 0); + } + + return new (minX, minY, maxW, maxH); + } + + internal Menu? Parent { get; init; } + private void AddKeyBindingsHotKey (MenuBarItem? menuBarItem) { if (menuBarItem is null || menuBarItem.Children is null) @@ -233,22 +491,47 @@ internal sealed class Menu : View } } - private void RemoveKeyBindingsHotKey (MenuBarItem? menuBarItem) + private void Application_RootMouseEvent (object? sender, MouseEventArgs a) { - if (menuBarItem is null || menuBarItem.Children is null) + if (a.View is { } and (MenuBar or not Menu)) { return; } - IEnumerable menuItems = menuBarItem.Children.Where (m => m is { })!; - - foreach (MenuItem menuItem in menuItems) + if (!Visible) { - if (menuItem.HotKey != Key.Empty) - { - KeyBindings.Remove (menuItem.HotKey!); - KeyBindings.Remove (menuItem.HotKey!.WithAlt); - } + throw new InvalidOperationException ("This shouldn't running on a invisible menu!"); + } + + View view = a.View ?? this; + + Point boundsPoint = view.ScreenToViewport (new (a.Position.X, a.Position.Y)); + + var me = new MouseEventArgs + { + Position = boundsPoint, + Flags = a.Flags, + ScreenPosition = a.Position, + View = view + }; + + if (view.NewMouseEvent (me) == true || a.Flags == MouseFlags.Button1Pressed || a.Flags == MouseFlags.Button1Released) + { + a.Handled = true; + } + } + + private void CloseAllMenus () + { + Application.UngrabMouse (); + _host.CloseAllMenus (); + } + + private void Current_TerminalResized (object? sender, SizeChangedEventArgs e) + { + if (_host.IsMenuOpen) + { + _host.CloseAllMenus (); } } @@ -261,7 +544,6 @@ internal sealed class Menu : View return true; } - for (var c = 0; c < _barItems!.Children!.Length; c++) { if (_barItems.Children [c] == menuItem) @@ -318,341 +600,6 @@ internal sealed class Menu : View return true; } - /// - protected override bool OnKeyDownNotHandled (Key keyEvent) - { - // We didn't handle the key, pass it on to host - return _host.InvokeCommandsBoundToKey (keyEvent) == true; - } - - private void Current_TerminalResized (object? sender, SizeChangedEventArgs e) - { - if (_host.IsMenuOpen) - { - _host.CloseAllMenus (); - } - } - - /// - protected override void OnVisibleChanged () - { - base.OnVisibleChanged (); - - if (Visible) - { - Application.MouseEvent += Application_RootMouseEvent; - } - else - { - Application.MouseEvent -= Application_RootMouseEvent; - } - } - - private void Application_RootMouseEvent (object? sender, MouseEventArgs a) - { - if (a.View is { } and (MenuBar or not Menu)) - { - return; - } - - if (!Visible) - { - throw new InvalidOperationException ("This shouldn't running on a invisible menu!"); - } - - View view = a.View ?? this; - - Point boundsPoint = view.ScreenToViewport (new (a.Position.X, a.Position.Y)); - - var me = new MouseEventArgs - { - Position = boundsPoint, - Flags = a.Flags, - ScreenPosition = a.Position, - View = view - }; - - if (view.NewMouseEvent (me) == true || a.Flags == MouseFlags.Button1Pressed || a.Flags == MouseFlags.Button1Released) - { - a.Handled = true; - } - } - - internal Attribute DetermineColorSchemeFor (MenuItem? item, int index) - { - if (item is null) - { - return GetNormalColor (); - } - - if (index == _currentChild) - { - return GetFocusColor (); - } - - return !item.IsEnabled () ? ColorScheme!.Disabled : GetNormalColor (); - } - - // By doing this we draw last, over everything else. - private void Top_DrawComplete (object? sender, DrawEventArgs e) - { - if (!Visible) - { - return; - } - - if (_barItems!.Children is null) - { - return; - } - - DrawBorderAndPadding (); - RenderLineCanvas (); - - // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework. - Region? savedClip = View.SetClipToScreen (); - - SetAttribute (GetNormalColor ()); - - for (int i = Viewport.Y; i < _barItems!.Children.Length; i++) - { - if (i < 0) - { - continue; - } - - if (ViewportToScreen (Viewport).Y + i >= Driver.Rows) - { - break; - } - - MenuItem? item = _barItems.Children [i]; - - SetAttribute ( - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - item is null ? GetNormalColor () : - i == _currentChild ? GetFocusColor () : GetNormalColor () - ); - - if (item is null && BorderStyle != LineStyle.None) - { - Point s = ViewportToScreen (new Point (-1, i)); - Driver.Move (s.X, s.Y); - Driver.AddRune (Glyphs.LeftTee); - } - else if (Frame.X < Driver.Cols) - { - Move (0, i); - } - - SetAttribute (DetermineColorSchemeFor (item, i)); - - for (int p = Viewport.X; p < Frame.Width - 2; p++) - { - // This - 2 is for the border - if (p < 0) - { - continue; - } - - if (ViewportToScreen (Viewport).X + p >= Driver.Cols) - { - break; - } - - if (item is null) - { - Driver.AddRune (Glyphs.HLine); - } - else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) - { - Driver.AddRune (Glyphs.LeftArrow); - } - - // This `- 3` is left border + right border + one row in from right - else if (p == Frame.Width - 3 && _barItems?.SubMenu (_barItems.Children [i]!) is { }) - { - Driver.AddRune (Glyphs.RightArrow); - } - else - { - Driver.AddRune ((Rune)' '); - } - } - - if (item is null) - { - if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width) - { - Point s = ViewportToScreen (new Point (Frame.Width - 2, i)); - Driver.Move (s.X, s.Y); - Driver.AddRune (Glyphs.RightTee); - } - - continue; - } - - string? textToDraw; - Rune nullCheckedChar = Glyphs.CheckStateNone; - Rune checkChar = Glyphs.Selected; - Rune uncheckedChar = Glyphs.UnSelected; - - if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) - { - checkChar = Glyphs.CheckStateChecked; - uncheckedChar = Glyphs.CheckStateUnChecked; - } - - // Support Checked even though CheckType wasn't set - if (item is { CheckType: MenuItemCheckStyle.Checked, Checked: null }) - { - textToDraw = $"{nullCheckedChar} {item.Title}"; - } - else if (item.Checked == true) - { - textToDraw = $"{checkChar} {item.Title}"; - } - else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) - { - textToDraw = $"{uncheckedChar} {item.Title}"; - } - else - { - textToDraw = item.Title; - } - - Point screen = ViewportToScreen (new Point (0, i)); - - if (screen.X < Driver.Cols) - { - Driver.Move (screen.X + 1, screen.Y); - - if (!item.IsEnabled ()) - { - DrawHotString (textToDraw, ColorScheme!.Disabled, ColorScheme.Disabled); - } - else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) - { - var tf = new TextFormatter - { - ConstrainToWidth = Frame.Width - 3, - ConstrainToHeight = 1, - Alignment = Alignment.Center, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw - }; - - // The -3 is left/right border + one space (not sure what for) - tf.Draw ( - ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)), - i == _currentChild ? GetFocusColor () : GetNormalColor (), - i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal, - SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty - ); - } - else - { - DrawHotString ( - textToDraw, - i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal, - i == _currentChild ? GetFocusColor () : GetNormalColor () - ); - } - - // The help string - int l = item.ShortcutTag.GetColumns () == 0 - ? item.Help.GetColumns () - : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2; - int col = Frame.Width - l - 3; - screen = ViewportToScreen (new Point (col, i)); - - if (screen.X < Driver.Cols) - { - Driver.Move (screen.X, screen.Y); - Driver.AddStr (item.Help); - - // The shortcut tag string - if (!string.IsNullOrEmpty (item.ShortcutTag)) - { - Driver.Move (screen.X + l - item.ShortcutTag.GetColumns (), screen.Y); - Driver.AddStr (item.ShortcutTag); - } - } - } - } - - View.SetClip (savedClip); - } - - public override Point? PositionCursor () - { - if (_host.IsMenuOpen) - { - if (_barItems!.IsTopLevel) - { - return _host.PositionCursor (); - } - - Move (2, 1 + _currentChild); - - return null; // Don't show the cursor - } - - return _host.PositionCursor (); - } - - public void Run (Action? action) - { - if (action is null) - { - return; - } - - Application.UngrabMouse (); - _host.CloseAllMenus (); - Application.LayoutAndDraw (true); - - _host.Run (action); - } - - protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) - { - if (!newHasFocus) - { - _host.LostFocus (previousFocusedView!); - } - } - - private void RunSelected () - { - if (_barItems!.IsTopLevel) - { - Run (_barItems.Action); - } - else - { - switch (_currentChild) - { - case > -1 when _barItems.Children! [_currentChild]!.Action != null!: - Run (_barItems.Children [_currentChild]?.Action); - - break; - case 0 when _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild]?.Parent!.Parent != null: - _host.PreviousMenu (_barItems.Children [_currentChild]!.Parent!.IsFromSubMenu, true); - - break; - case > -1 when _barItems.SubMenu (_barItems.Children [_currentChild]) != null!: - CheckSubMenu (); - - break; - } - } - } - - private void CloseAllMenus () - { - Application.UngrabMouse (); - _host.CloseAllMenus (); - } - private bool MoveDown () { if (_barItems!.IsTopLevel) @@ -801,6 +748,51 @@ internal sealed class Menu : View return true; } + private void RemoveKeyBindingsHotKey (MenuBarItem? menuBarItem) + { + if (menuBarItem is null || menuBarItem.Children is null) + { + return; + } + + IEnumerable menuItems = menuBarItem.Children.Where (m => m is { })!; + + foreach (MenuItem menuItem in menuItems) + { + if (menuItem.HotKey != Key.Empty) + { + KeyBindings.Remove (menuItem.HotKey!); + KeyBindings.Remove (menuItem.HotKey!.WithAlt); + } + } + } + + private void RunSelected () + { + if (_barItems!.IsTopLevel) + { + Run (_barItems.Action); + } + else + { + switch (_currentChild) + { + case > -1 when _barItems.Children! [_currentChild]!.Action != null!: + Run (_barItems.Children [_currentChild]?.Action); + + break; + case 0 when _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild]?.Parent!.Parent != null: + _host.PreviousMenu (_barItems.Children [_currentChild]!.Parent!.IsFromSubMenu, true); + + break; + case > -1 when _barItems.SubMenu (_barItems.Children [_currentChild]) != null!: + CheckSubMenu (); + + break; + } + } + } + private void SetParentSetNeedsDisplay () { if (_host._openSubMenu is { }) @@ -815,151 +807,193 @@ internal sealed class Menu : View _host.SetNeedsDraw (); } - protected override bool OnMouseEvent (MouseEventArgs me) + // By doing this we draw last, over everything else. + private void Top_DrawComplete (object? sender, DrawEventArgs e) { - if (!_host._handled && !_host.HandleGrabView (me, this)) + if (!Visible) { - return false; + return; } - _host._handled = false; - bool disabled; - - if (me.Flags == MouseFlags.Button1Clicked) + if (_barItems!.Children is null) { - disabled = false; - - if (me.Position.Y < 0) - { - return me.Handled = true; - } - - if (me.Position.Y >= _barItems!.Children!.Length) - { - return me.Handled = true; - } - - MenuItem item = _barItems.Children [me.Position.Y]!; - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (item is null || !item.IsEnabled ()) - { - disabled = true; - } - - if (disabled) - { - return me.Handled = true; - } - - _currentChild = me.Position.Y; - RunSelected (); - - return me.Handled = true; + return; } - if (me.Flags != MouseFlags.Button1Pressed - && me.Flags != MouseFlags.Button1DoubleClicked - && me.Flags != MouseFlags.Button1TripleClicked - && me.Flags != MouseFlags.ReportMousePosition - && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) - { - return false; - } + DrawBorderAndPadding (); + RenderLineCanvas (); - { - disabled = false; + // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework. + Region? savedClip = SetClipToScreen (); - if (me.Position.Y < 0 || me.Position.Y >= _barItems!.Children!.Length) + SetAttribute (GetNormalColor ()); + + for (int i = Viewport.Y; i < _barItems!.Children.Length; i++) + { + if (i < 0) { - return me.Handled = true; + continue; } - MenuItem item = _barItems.Children [me.Position.Y]!; + if (ViewportToScreen (Viewport).Y + i >= Driver.Rows) + { + break; + } + + MenuItem? item = _barItems.Children [i]; + + SetAttribute ( + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + item is null ? GetNormalColor () : + i == _currentChild ? GetFocusColor () : GetNormalColor () + ); + + if (item is null && BorderStyle != LineStyle.None) + { + Point s = ViewportToScreen (new Point (-1, i)); + Driver.Move (s.X, s.Y); + Driver.AddRune (Glyphs.LeftTee); + } + else if (Frame.X < Driver.Cols) + { + Move (0, i); + } + + SetAttribute (DetermineColorSchemeFor (item, i)); + + for (int p = Viewport.X; p < Frame.Width - 2; p++) + { + // This - 2 is for the border + if (p < 0) + { + continue; + } + + if (ViewportToScreen (Viewport).X + p >= Driver.Cols) + { + break; + } + + if (item is null) + { + Driver.AddRune (Glyphs.HLine); + } + else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) + { + Driver.AddRune (Glyphs.LeftArrow); + } + + // This `- 3` is left border + right border + one row in from right + else if (p == Frame.Width - 3 && _barItems?.SubMenu (_barItems.Children [i]!) is { }) + { + Driver.AddRune (Glyphs.RightArrow); + } + else + { + Driver.AddRune ((Rune)' '); + } + } - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (item is null) { - return me.Handled = true; + if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width) + { + Point s = ViewportToScreen (new Point (Frame.Width - 2, i)); + Driver.Move (s.X, s.Y); + Driver.AddRune (Glyphs.RightTee); + } + + continue; } - if (item.IsEnabled () != true) + string? textToDraw; + Rune nullCheckedChar = Glyphs.CheckStateNone; + Rune checkChar = Glyphs.Selected; + Rune uncheckedChar = Glyphs.UnSelected; + + if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) { - disabled = true; + checkChar = Glyphs.CheckStateChecked; + uncheckedChar = Glyphs.CheckStateUnChecked; } - if (!disabled) + // Support Checked even though CheckType wasn't set + if (item is { CheckType: MenuItemCheckStyle.Checked, Checked: null }) { - _currentChild = me.Position.Y; + textToDraw = $"{nullCheckedChar} {item.Title}"; } - - if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) + else if (item.Checked == true) { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); - - return me.Handled = true; + textToDraw = $"{checkChar} {item.Title}"; } - - _host.OnMenuOpened (); - - return me.Handled = true; - } - } - - internal bool CheckSubMenu () - { - if (_currentChild == -1 || _barItems?.Children? [_currentChild] is null) - { - return true; - } - - MenuBarItem? subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]!); - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (subMenu is { }) - { - int pos = -1; - - if (_host._openSubMenu is { }) + else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) { - pos = _host._openSubMenu.FindIndex (o => o._barItems == subMenu); + textToDraw = $"{uncheckedChar} {item.Title}"; } - - if (pos == -1 - && this != _host.OpenCurrentMenu - && subMenu.Children != _host.OpenCurrentMenu!._barItems!.Children - && !_host.CloseMenu (false, true)) + else { - return false; + textToDraw = item.Title; } - _host.Activate (_host._selected, pos, subMenu); - } - else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems!.IsSubMenuOf (_barItems.Children [_currentChild]!) == false) - { - return _host.CloseMenu (false, true); - } - else - { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); + Point screen = ViewportToScreen (new Point (0, i)); + + if (screen.X < Driver.Cols) + { + Driver.Move (screen.X + 1, screen.Y); + + if (!item.IsEnabled ()) + { + DrawHotString (textToDraw, ColorScheme!.Disabled, ColorScheme.Disabled); + } + else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) + { + var tf = new TextFormatter + { + ConstrainToWidth = Frame.Width - 3, + ConstrainToHeight = 1, + Alignment = Alignment.Center, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw + }; + + // The -3 is left/right border + one space (not sure what for) + tf.Draw ( + ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)), + i == _currentChild ? GetFocusColor () : GetNormalColor (), + i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal, + SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty + ); + } + else + { + DrawHotString ( + textToDraw, + i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal, + i == _currentChild ? GetFocusColor () : GetNormalColor () + ); + } + + // The help string + int l = item.ShortcutTag.GetColumns () == 0 + ? item.Help.GetColumns () + : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2; + int col = Frame.Width - l - 3; + screen = ViewportToScreen (new Point (col, i)); + + if (screen.X < Driver.Cols) + { + Driver.Move (screen.X, screen.Y); + Driver.AddStr (item.Help); + + // The shortcut tag string + if (!string.IsNullOrEmpty (item.ShortcutTag)) + { + Driver.Move (screen.X + l - item.ShortcutTag.GetColumns (), screen.Y); + Driver.AddStr (item.ShortcutTag); + } + } + } } - return true; - } - - protected override void Dispose (bool disposing) - { - RemoveKeyBindingsHotKey (_barItems); - - if (Application.Top is { }) - { - Application.Top.DrawComplete -= Top_DrawComplete; - Application.Top.SizeChanging -= Current_TerminalResized; - } - - Application.MouseEvent -= Application_RootMouseEvent; - base.Dispose (disposing); + SetClip (savedClip); } } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index cb2407001..fb3aedd7b 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -133,16 +133,24 @@ public class MenuBar : View, IDesignable { CloseOtherOpenedMenuBar (); - return Select (Menus.IndexOf (ctx.Data)); + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + return Select (Menus.IndexOf (keyCommandContext.Binding.Data)); }); AddCommand (Command.Select, ctx => { - if (ctx.Data is MouseEventArgs) + if (ctx is not CommandContext keyCommandContext) + { + return false ; + } + if (keyCommandContext.Binding.Data is MouseEventArgs) { // HACK: Work around the fact that View.MouseClick always invokes Select return false; } - var res = Run ((ctx.Data as MenuItem)?.Action!); + var res = Run ((keyCommandContext.Binding.Data as MenuItem)?.Action!); CloseAllMenus (); return res; diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 60384b9e5..eff0b62c3 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -358,14 +358,19 @@ public static class MessageBox if (count == defaultButton) { b.IsDefault = true; - b.Accepting += (s, e) => + b.Accepting += (_, e) => { + if (e.Context is not CommandContext keyCommandContext) + { + return; + } + // TODO: With https://github.com/gui-cs/Terminal.Gui/issues/3778 we can simplify this - if (e.Context.Data is Button button) + if (keyCommandContext.Binding.Data is Button button) { Clicked = (int)button.Data!; - } - else if (e.Context is CommandContext { Binding.BoundView: Button btn }) + } + else if (keyCommandContext.Binding.BoundView is Button btn) { Clicked = (int)btn.Data!; } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 01e759029..5a8dad689 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -64,7 +64,7 @@ public class RadioGroup : View, IDesignable, IOrientation return false; } - var item = keyCommandContext.Data as int?; + var item = keyCommandContext.Binding.Data as int?; if (HasFocus) @@ -248,7 +248,7 @@ public class RadioGroup : View, IDesignable, IOrientation if (c > -1) { // Just like the user pressing the items' hotkey - e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, context: c)) == true; + e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, data: c)) == true; } } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index d35c46850..069519494 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -300,17 +300,18 @@ public class Shortcut : View, IOrientation, IDesignable AddCommand (Command.Select, DispatchCommand); } - private bool? DispatchCommand (ICommandContext commandContext) + private bool? DispatchCommand (ICommandContext? commandContext) { if (commandContext is not CommandContext ctx) { return false; } - if (ctx.Data != this) + + if (ctx.Binding.Data != this) { // Invoke Select on the command view to cause it to change state if it wants to // If this causes CommandView to raise Accept, we eat it - ctx.Data = this; + ctx.Binding = ctx.Binding with { Data = this }; CommandView.InvokeCommand (Command.Select, ctx); } @@ -497,7 +498,7 @@ public class Shortcut : View, IOrientation, IDesignable void CommandViewOnSelecting (object? sender, CommandEventArgs e) { - if (e.Context.Data != this) + if (e.Context is CommandContext keyCommandContext && keyCommandContext.Binding.Data != this) { // Forward command to ourselves InvokeCommand (Command.Select, new ([Command.Select], KeyBindingScope.Focused, null, this)); diff --git a/UICatalog/Scenarios/Editors/EventLog.cs b/UICatalog/Scenarios/Editors/EventLog.cs index e9ce2efba..cf0042482 100644 --- a/UICatalog/Scenarios/Editors/EventLog.cs +++ b/UICatalog/Scenarios/Editors/EventLog.cs @@ -86,15 +86,24 @@ public class EventLog : ListView _viewToLog.HandlingHotKey += (s, args) => { - Log ($"HandlingHotKey: {args.Context.Command} {args.Context.Data}"); + if (args.Context is CommandContext keyCommandContext) + { + Log ($"HandlingHotKey: {args.Context.Command} {keyCommandContext.Binding.Data}"); + } }; _viewToLog.Selecting += (s, args) => { - Log ($"Selecting: {args.Context.Command} {args.Context.Data}"); + if (args.Context is CommandContext keyCommandContext) + { + Log ($"Selecting: {args.Context.Command} {keyCommandContext.Binding.Data}"); + } }; _viewToLog.Accepting += (s, args) => { - Log ($"Accepting: {args.Context.Command} {args.Context.Data}"); + if (args.Context is CommandContext keyCommandContext) + { + Log ($"Accepting: {args.Context.Command} {keyCommandContext.Binding.Data}"); + } }; } } diff --git a/UnitTests/Views/TextViewTests.cs b/UnitTests/Views/TextViewTests.cs index 3d0dd2185..ab04015ec 100644 --- a/UnitTests/Views/TextViewTests.cs +++ b/UnitTests/Views/TextViewTests.cs @@ -6999,7 +6999,7 @@ TAB to jump between text field", { Width = Dim.Fill (), Height = Dim.Fill (), Text = "This is the first line.\nThis is the second line.\n" }; - tv.UnwrappedCursorPosition += (s, e) => { cp = e.Point; }; + tv.UnwrappedCursorPosition += (s, e) => { cp = e; }; var top = new Toplevel (); top.Add (tv); Application.Begin (top);