diff --git a/SadConsole/SadConsole.xml b/SadConsole/SadConsole.xml index 7aad1984..31c68688 100644 --- a/SadConsole/SadConsole.xml +++ b/SadConsole/SadConsole.xml @@ -11828,6 +11828,11 @@ The controls added which contain a value. + + + Constructs a ControlHost object. + + Gets a control by index. @@ -11961,6 +11966,11 @@ The control that should be focused. The control that currently has focus. + + + Event raised when the focused control is changed. + + Determins if a control is enabled and is . @@ -12060,64 +12070,6 @@ - - - A basic console that can contain controls. - - - - - The controls host holding all the controls. - - - - - Creates a new console. - - The width in cells of the surface. - The height in cells of the surface. - - - - Creates a new screen object that can render a surface. Uses the specified cells to generate the surface. - - The width in cells of the surface. - The height in cells of the surface. - The initial cells to seed the surface. - - - - Creates a new console with the specified width and height, with for the background and for the foreground. - - The visible width of the console in cells. - The visible height of the console in cells. - The total width of the console in cells. - The total height of the console in cells. - - - - Creates a console with the specified width and height, with for the background and for the foreground. - - The width of the console in cells. - The height of the console in cells. - The total width of the console in cells. - The total height of the console in cells. - The cells to seed the console with. If , creates the cells for you. - - - - Creates a new console using the existing surface. - - The surface. - The font to use with the surface. - The font size. - - - - Returns the value "Console (Controls)". - - The string "Console (Controls)". - Simple button control with a height of 1. @@ -12242,11 +12194,12 @@ When , focuses the button before clicking. - + Detects if the SPACE or ENTER keys are pressed and calls the method. + @@ -12742,8 +12695,19 @@ Called when the keyboard is used on this control. + This function is called only if the declines + to handle the keyboard input. + + The state of the keyboard. + + + + Called when the keyboard is used on this control. + Overriding this method is primarily for composite controls that need to + handle keyboard input The state of the keyboard. + @@ -13597,9 +13561,6 @@ - - - When is set to , changes the child controls to also be dirty. @@ -14694,6 +14655,74 @@ + + + A basic console that can contain controls. + + + + + The controls host holding all the controls. + + + + + Creates a new console. + + The width in cells of the surface. + The height in cells of the surface. + + + + Creates a new screen object that can render a surface. Uses the specified cells to generate the surface. + + The width in cells of the surface. + The height in cells of the surface. + The initial cells to seed the surface. + + + + Creates a new console with the specified width and height, with for the background and for the foreground. + + The visible width of the console in cells. + The visible height of the console in cells. + The total width of the console in cells. + The total height of the console in cells. + + + + Creates a console with the specified width and height, with for the background and for the foreground. + + The width of the console in cells. + The height of the console in cells. + The total width of the console in cells. + The total height of the console in cells. + The cells to seed the console with. If , creates the cells for you. + + + + Creates a new console using the existing surface. + + The surface. + The font to use with the surface. + The font size. + + + + Returns the value "Console (Controls)". + + The string "Console (Controls)". + + + + + + + + + The container this handler operates on. + + Event arguments to indicate that a key is being pressed on a control that allows keyboard key cancelling. diff --git a/SadConsole/UI/ControlHost.cs b/SadConsole/UI/ControlHost.cs index 9a228c09..f09b8570 100644 --- a/SadConsole/UI/ControlHost.cs +++ b/SadConsole/UI/ControlHost.cs @@ -2,12 +2,13 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Runtime.Serialization; using SadConsole.Input; using SadConsole.Renderers; using SadConsole.UI.Controls; - +using SadConsole.UI.Handlers; using SadRogue.Primitives; namespace SadConsole.UI; @@ -51,6 +52,15 @@ public class ControlHost : Components.IComponent, IList, IContainer private ControlBase? _controlWithMouse; private IRenderStep? _controlsRenderStep; private Rectangle _parentView; + private TabFocusHandler _tabHandler; + + /// + /// Constructs a ControlHost object. + /// + public ControlHost() + { + _tabHandler = new TabFocusHandler(this); + } #region Properties @@ -274,29 +284,24 @@ void Components.IComponent.ProcessKeyboard(IScreenObject host, Keyboard info, ou if (!host.UseKeyboard) return; - if (FocusedControl != null && FocusedControl.IsEnabled && FocusedControl.UseKeyboard) - handled = FocusedControl.ProcessKeyboard(info); + ControlBase? current = FocusedControl; + ControlBase? origin = current; - if (!handled) + while (current != null) { - if ( - (info.IsKeyDown(Keys.LeftShift) || - info.IsKeyDown(Keys.RightShift) || - info.IsKeyReleased(Keys.LeftShift) || - info.IsKeyReleased(Keys.RightShift)) && - info.IsKeyReleased(Keys.Tab)) + if (current.IsEnabled && current.UseKeyboard) { - TabPreviousControl(); - handled = true; - return; + handled = current.ProcessKeyboard(info, origin); + if (handled) return; } - if (info.IsKeyReleased(Keys.Tab)) - { - TabNextControl(); - handled = true; - return; - } + origin = current; + current = current.Parent as ControlBase; + } + + if (!handled) + { + handled = _tabHandler.ProcessKeyboard(info, FocusedControl); } } @@ -360,178 +365,12 @@ void Components.IComponent.Render(IScreenObject host, TimeSpan delta) { } /// /// Gives the focus to the next control in the tab order. /// - public void TabNextControl() - { - if (ControlsList.Count == 0) - return; - - ControlBase? control; - - if (_focusedControl == null) - { - if (FindTabControlForward(0, ControlsList.Count - 1, out control)) - { - FocusedControl = control; - return; - } - - TryTabNextConsole(); - } - else - { - int index = ControlsList.IndexOf(_focusedControl); - - // From first control - if (index == 0) - { - if (FindTabControlForward(index + 1, ControlsList.Count - 1, out control)) - { - FocusedControl = control; - return; - } - - TryTabNextConsole(); - } - - // From last control - else if (index == ControlsList.Count - 1) - { - if (!TryTabNextConsole()) - { - if (FindTabControlForward(0, ControlsList.Count - 1, out control)) - { - FocusedControl = control; - return; - } - } - } - - // Middle - else - { - // Middle > End - if (FindTabControlForward(index + 1, ControlsList.Count - 1, out control)) - { - FocusedControl = control; - return; - } - - // Next console - if (TryTabNextConsole()) - return; - - // Start > Middle - if (FindTabControlForward(0, index, out control)) - { - FocusedControl = control; - return; - } - } - } - } + public void TabNextControl() => _tabHandler.TabNextControl(FocusedControl); /// /// Gives focus to the previous control in the tab order. /// - public void TabPreviousControl() - { - if (ControlsList.Count == 0) - return; - - ControlBase? control; - - if (_focusedControl == null) - { - if (FindTabControlPrevious(ControlsList.Count - 1, 0, out control)) - { - FocusedControl = control; - return; - } - - TryTabPreviousConsole(); - } - else - { - int index = ControlsList.IndexOf(_focusedControl); - - // From first control - if (index == 0) - { - if (!TryTabPreviousConsole()) - { - if (FindTabControlPrevious(ControlsList.Count - 1, 0, out control)) - { - FocusedControl = control; - return; - } - } - } - - // From last control - else if (index == ControlsList.Count - 1) - { - if (FindTabControlPrevious(index - 1, 0, out control)) - { - FocusedControl = control; - return; - } - - TryTabPreviousConsole(); - } - - // Middle - else - { - // Middle -> Start - if (FindTabControlPrevious(index - 1, 0, out control)) - { - FocusedControl = control; - return; - } - - // Next console - if (TryTabPreviousConsole()) - return; - - // End -> Middle - if (FindTabControlPrevious(ControlsList.Count - 1, index, out control)) - { - FocusedControl = control; - return; - } - } - } - } - - private bool FindTabControlForward(int startingIndex, int endingIndex, [NotNullWhen(true)] out ControlBase? foundControl) - { - for (int i = startingIndex; i <= endingIndex; i++) - { - if (ControlsList[i].TabStop && ControlsList[i].IsEnabled && ControlsList[i].CanFocus) - { - foundControl = ControlsList[i]; - return true; - } - } - - foundControl = null; - return false; - } - - private bool FindTabControlPrevious(int startingIndex, int endingIndex, [NotNullWhen(true)] out ControlBase? foundControl) - { - for (int i = startingIndex; i >= endingIndex; i--) - { - if (ControlsList[i].TabStop && ControlsList[i].IsEnabled && ControlsList[i].CanFocus) - { - foundControl = ControlsList[i]; - return true; - } - } - - foundControl = null; - return false; - } + public void TabPreviousControl() => _tabHandler.TabPreviousControl(FocusedControl); private bool ParentHasComponent(IScreenSurface surface) => surface.HasSadComponent(out _); @@ -540,7 +379,7 @@ private bool ParentHasComponent(IScreenSurface surface) => /// Tries to tab to the console that comes before this one in the collection of . Sets focus to the target console if found. /// /// if the tab was successful; otherwise, . - protected bool TryTabPreviousConsole() + public bool TryTabPreviousConsole() { if (!CanTabToNextConsole || ParentConsole?.Parent == null) return false; @@ -592,7 +431,7 @@ protected bool TryTabPreviousConsole() /// Tries to tab to the console that comes after this one in the collection of . Sets focus to the target console if found. /// /// if the tab was successful; otherwise, . - protected bool TryTabNextConsole() + protected internal bool TryTabNextConsole() { if (!CanTabToNextConsole || ParentConsole?.Parent == null) return false; @@ -669,8 +508,15 @@ protected virtual void FocusedControlChanged(ControlBase? newControl, ControlBas if (newControl != null) newControl.IsFocused = true; + + FocusedControlChangedEvent?.Invoke(this, EventArgs.Empty); } + /// + /// Event raised when the focused control is changed. + /// + public event EventHandler? FocusedControlChangedEvent; + /// /// Determins if a control is enabled and is . /// diff --git a/SadConsole/UI/Controls/CompositeControl.cs b/SadConsole/UI/Controls/CompositeControl.cs index 8e6958f3..6a00a790 100644 --- a/SadConsole/UI/Controls/CompositeControl.cs +++ b/SadConsole/UI/Controls/CompositeControl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using SadConsole.Input; +using SadConsole.UI.Handlers; namespace SadConsole.UI.Controls; @@ -40,6 +41,8 @@ public abstract class CompositeControl : ControlBase, IContainer public CompositeControl(int width, int height) : base(width, height) { CreateChildControls(); + + KeyboardHandler = new TabFocusHandler(this); } /// @@ -47,6 +50,40 @@ public CompositeControl(int width, int height) : base(width, height) /// protected virtual void CreateChildControls() { } + public override bool AcquireFocus(FocusDirection direction) + { + switch (direction) + { + case FocusDirection.Previous: + return AcquireBackwardsFocus(); + default: + case FocusDirection.Next: + return AcquireForwardFocus(); + } + } + + bool AcquireForwardFocus() + { + for (int i = 0; i < Controls.Count; i++) + { + if (Controls[i].CanFocus && Controls[i].AcquireFocus(FocusDirection.Next)) + return true; + } + + return false; + } + + bool AcquireBackwardsFocus() + { + for (int i = Controls.Count - 1; i >= 0; i--) + { + if (Controls[i].CanFocus && Controls[i].AcquireFocus(FocusDirection.Previous)) + return true; + } + + return false; + } + /// /// Processes the mouse on each control hosted by this control. /// diff --git a/SadConsole/UI/Controls/ControlBase.cs b/SadConsole/UI/Controls/ControlBase.cs index a601694a..3105d558 100644 --- a/SadConsole/UI/Controls/ControlBase.cs +++ b/SadConsole/UI/Controls/ControlBase.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.Serialization; using SadConsole.Input; +using SadConsole.UI.Handlers; using SadRogue.Primitives; namespace SadConsole.UI.Controls; @@ -232,50 +233,54 @@ public bool IsFocused get => _isFocused; set { - if (Parent?.Host != null) + if (value) + AcquireFocus(FocusDirection.Next); + else { - // We're focused - if (value) - { - // Some other control is focused, swap - if (Parent.Host.FocusedControl != this) - { - Parent.Host.FocusedControl = this; - _isFocused = Parent.Host.FocusedControl == this; - DetermineState(); - - if (_isFocused) - OnFocused(); - } - - // We're focused, check internal flag and set properly - else if (!_isFocused) - { - _isFocused = true; - DetermineState(); - OnFocused(); - } - } - else - { - _isFocused = false; + _isFocused = false; - if (Parent.Host.FocusedControl == this) - Parent.Host.FocusedControl = null; + if (Parent?.Host?.FocusedControl == this) + Parent.Host.FocusedControl = null; - DetermineState(); - OnUnfocused();; - } + DetermineState(); + OnUnfocused(); } + } + } - // No parent/host and we're currently focused internally, clear it - else if (_isFocused) + public virtual bool AcquireFocus(FocusDirection direction) + { + if (Parent?.Host != null) + { + // Some other control is focused, swap + if (Parent.Host.FocusedControl != this) { - _isFocused = false; + Parent.Host.FocusedControl = this; + _isFocused = Parent.Host.FocusedControl == this; DetermineState(); - OnUnfocused(); + + if (_isFocused) + OnFocused(); + } + + // We're focused, check internal flag and set properly + else if (!_isFocused) + { + _isFocused = true; + DetermineState(); + OnFocused(); } } + + // No parent/host and we're currently focused internally, clear it + else if (_isFocused) + { + _isFocused = false; + DetermineState(); + OnUnfocused(); + } + + return _isFocused; } /// @@ -385,12 +390,30 @@ protected virtual void OnIsDirtyChanged() => IsDirtyChanged?.Invoke(this, EventArgs.Empty); #region Input + + /// + /// Gets or sets the handler used to process keyboard input for this control. + /// + public IKeyboardHandler? KeyboardHandler { get; set; } + /// /// Called when the keyboard is used on this control. + /// This function is called only if the declines + /// to handle the keyboard input. /// /// The state of the keyboard. public virtual bool ProcessKeyboard(Keyboard state) => false; + /// + /// Called when the keyboard is used on this control. + /// This method probably does not need to be overriden, instead override ProcessKeyboard(Keyboard) + /// or set the KeyboardHandler. + /// + /// The state of the keyboard. + /// + public virtual bool ProcessKeyboard(Keyboard state, ControlBase? origin) + => (KeyboardHandler?.ProcessKeyboard(state, origin) ?? false) || ProcessKeyboard(state); + /// /// Checks if the mouse is the control and calls the appropriate mouse methods. /// @@ -762,4 +785,5 @@ public ControlMouseState(ControlBase control, MouseScreenObjectState originalMou } } + } diff --git a/SadConsole/UI/Controls/Panel.cs b/SadConsole/UI/Controls/Panel.cs index 2e6e465c..376f532d 100644 --- a/SadConsole/UI/Controls/Panel.cs +++ b/SadConsole/UI/Controls/Panel.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; using SadConsole.Input; +using SadConsole.UI.Handlers; namespace SadConsole.UI.Controls; @@ -45,6 +46,7 @@ public partial class Panel : CompositeControl, IList public Panel(int width, int height) : base(width, height) { TabStop = false; + KeyboardHandler = new TabFocusHandler(this); } [OnDeserialized] @@ -142,29 +144,6 @@ protected override void OnMouseExit(ControlMouseState state) } } - /// - public override bool ProcessKeyboard(Keyboard state) - { - if (IsEnabled && UseKeyboard) - { - bool processResult = base.ProcessKeyboard(state); - - var controls = new List(Controls); - controls.Reverse(); - - for (int i = 0; i < controls.Count; i++) - { - ControlBase control = controls[i]; - if (control.ProcessKeyboard(state)) - return true; - } - - return processResult; - } - - return false; - } - /// /// When is set to , changes the child controls to also be dirty. /// diff --git a/SadConsole/UI/Handlers/FocusDirection.cs b/SadConsole/UI/Handlers/FocusDirection.cs new file mode 100644 index 00000000..c7330954 --- /dev/null +++ b/SadConsole/UI/Handlers/FocusDirection.cs @@ -0,0 +1,10 @@ +namespace SadConsole.UI.Handlers; + +/// +/// +/// +public enum FocusDirection +{ + Next = 0, + Previous, +} diff --git a/SadConsole/UI/Handlers/IKeyboardHandler.cs b/SadConsole/UI/Handlers/IKeyboardHandler.cs new file mode 100644 index 00000000..4de78927 --- /dev/null +++ b/SadConsole/UI/Handlers/IKeyboardHandler.cs @@ -0,0 +1,10 @@ +using SadConsole.Input; +using SadConsole.UI.Controls; + +namespace SadConsole.UI.Handlers; + +public interface IKeyboardHandler +{ + bool ProcessKeyboard(Keyboard state, ControlBase? origin); +} + diff --git a/SadConsole/UI/Handlers/TabFocusHandler.cs b/SadConsole/UI/Handlers/TabFocusHandler.cs new file mode 100644 index 00000000..c2ccbcfe --- /dev/null +++ b/SadConsole/UI/Handlers/TabFocusHandler.cs @@ -0,0 +1,221 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using SadConsole.Input; +using SadConsole.UI.Controls; + +namespace SadConsole.UI.Handlers; + +public class TabFocusHandler : IKeyboardHandler +{ + private IKeyboardHandler? _next; + + public TabFocusHandler(IContainer container, IKeyboardHandler? next = null) + { + Container = container; + _next = next; + } + + /// + /// The container this handler operates on. + /// + public IContainer Container { get; } + + private IList ControlsList => Container; + + public bool ProcessKeyboard(Keyboard info, ControlBase? origin) + { + if ( + (info.IsKeyDown(Keys.LeftShift) || + info.IsKeyDown(Keys.RightShift) || + info.IsKeyReleased(Keys.LeftShift) || + info.IsKeyReleased(Keys.RightShift)) && + info.IsKeyReleased(Keys.Tab)) + { + TabPreviousControl(origin); + return true; + } + + if (info.IsKeyReleased(Keys.Tab)) + { + TabNextControl(origin); + return true; + } + + return false; + } + + public void TabNextControl(ControlBase? startControl) + { + if (ControlsList.Count == 0) + return; + + ControlBase? control; + + if (startControl == null) + { + if (TabForward(0, ControlsList.Count - 1)) + { + return; + } + + if (Container is ControlHost ch) + ch.TryTabNextConsole(); + } + else + { + int index = ControlsList.IndexOf(startControl); + + // From first control + if (index == 0) + { + if (TabForward(index + 1, ControlsList.Count - 1)) + { + return; + } + + TryTabNextConsole(); + } + + // From last control + else if (index == ControlsList.Count - 1) + { + if (!TryTabNextConsole()) + { + if (TabForward(0, ControlsList.Count - 1)) + { + return; + } + } + } + + // Middle + else + { + // Middle > End + if (TabForward(index + 1, ControlsList.Count - 1)) + { + return; + } + + // Next console + if (TryTabNextConsole()) + return; + + // Start > Middle + if (TabForward(0, index)) + { + return; + } + } + } + } + + public void TabPreviousControl(ControlBase startControl) + { + if (ControlsList.Count == 0) + return; + + ControlBase? control; + + if (startControl == null) + { + if (TabBackward(ControlsList.Count - 1, 0)) + { + return; + } + + TryTabPreviousConsole(); + } + else + { + int index = ControlsList.IndexOf(startControl); + + // From first control + if (index == 0) + { + if (!TryTabPreviousConsole()) + { + if (TabBackward(ControlsList.Count - 1, 0)) + { + return; + } + } + } + + // From last control + else if (index == ControlsList.Count - 1) + { + if (TabBackward(index - 1, 0)) + { + return; + } + + TryTabPreviousConsole(); + } + + // Middle + else + { + // Middle -> Start + if (TabBackward(index - 1, 0)) + { + return; + } + + // Next console + if (TryTabPreviousConsole()) + return; + + // End -> Middle + if (TabBackward(ControlsList.Count - 1, index)) + { + return; + } + } + } + } + + private bool TabForward(int startingIndex, int endingIndex) + { + for (int i = startingIndex; i <= endingIndex; i++) + { + if (ControlsList[i].TabStop && ControlsList[i].IsEnabled && ControlsList[i].CanFocus) + { + if (ControlsList[i].AcquireFocus(FocusDirection.Next)) + return true; + } + } + + return false; + } + + private bool TabBackward(int startingIndex, int endingIndex) + { + for (int i = startingIndex; i >= endingIndex; i--) + { + if (ControlsList[i].TabStop && ControlsList[i].IsEnabled && ControlsList[i].CanFocus) + { + if (ControlsList[i].AcquireFocus(FocusDirection.Previous)) + return true; + } + } + + return false; + } + + private bool TryTabNextConsole() + { + if (Container is ControlHost ch) + return ch.TryTabNextConsole(); + + return false; + } + + private bool TryTabPreviousConsole() + { + if (Container is ControlHost ch) + return ch.TryTabPreviousConsole(); + + return false; + } +}