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;
+ }
+}