i'm bad at git, large commit with lots of changes

main
resneptacle 4 weeks ago
parent 04677ab97a
commit a6377d1a57

@ -57,14 +57,14 @@ public class AsciiArt {
return return
@" @"
.;lllllllllc, .;c,:oddoc;. 'coddoc' ,clllllllll:. .;lllllllllc, .;c,:oddoc;. 'coddoc' ,clllllllll:.
kMMMMWKOdc;'xx cWMN' 'lkXMMO .dNMM0l' kMN: :MMMMMX0xl:':K kMMMMWKOdc;'xx cWMN' 'lkXMMO .dNMM0l' kMN: :MMMMMX0xl:':K
,;'..':ok0kc ;NMMN' .cNKl .NXx;. NMMW. ';,...;lxOOo. ,;'..':ok0kc ;NMMN' .cNKl .NXx;. NMMW. ';,...;lxOOo.
l0NMMM0c. .:o0lldl, xMo,. dKKo ;kXWMMNd' l0NMMM0c. .:o0lldl, xMo,. dKKo ;kXWMMNd'
'WMMWo::oOXX0' :kO0kl;;lkK0k, OMMMMWK, 0MMMOc;lxKNKc 'WMMWo::oOXX0' :kO0kl;;lkK0k, OMMMMWK, 0MMMOc;lxKNKc
.,;;clodkXMc :MKo. .l0W. cMMMW0o. ;0WN, .',;:lodxKM0 .,;;clodkXMc :MKo. .l0W. cMMMW0o. ;0WN, .',;:lodxKM0
cOkkxdollc::l: .XOddddxxxxxxkO cNMx ,0MMXo 'kOkxddolc:::l cOkkxdollc::l: .XOddddxxxxxxkO cNMx ,0MMXo 'kOkxddolc:::l
.lxkkkkxoc,. ':odxkkkxoc' ,lc;lxo:. :dxkkkxdl;. .lxkkkkxoc,. ':odxkkkxoc' ,lc;lxo:. :dxkkkxdl;.
"; ";
}} }}

@ -17,22 +17,22 @@ public class Helper {
/// <returns>nint - Pointer of new window</returns> /// <returns>nint - Pointer of new window</returns>
/// <exception cref="ArgumentOutOfRangeException">Throws exception if window is outside of screen bounds</exception> /// <exception cref="ArgumentOutOfRangeException">Throws exception if window is outside of screen bounds</exception>
public static nint CreateCenteredWindow (nint screen, int width, int height, bool borders = true) { public static nint CreateCenteredWindow (nint screen, int width, int height, bool borders = true) {
NCurses.GetMaxYX (screen, out int screenHeight, out int screenWidth); NCurses.GetMaxYX (screen, out int screenHeight, out int screenWidth);
int originY = (screenHeight / 2) - (height / 2); int originY = (screenHeight / 2) - (height / 2);
int originX = (screenWidth / 2) - (width / 2); int originX = (screenWidth / 2) - (width / 2);
if (originX + width > screenWidth) throw new ArgumentOutOfRangeException ("width"); if (originX + width > screenWidth) throw new ArgumentOutOfRangeException ("width");
if (originY + height > screenHeight) throw new ArgumentOutOfRangeException ("height"); if (originY + height > screenHeight) throw new ArgumentOutOfRangeException ("height");
if (originX < 0) throw new ArgumentOutOfRangeException ("originX"); if (originX < 0) throw new ArgumentOutOfRangeException ("originX");
if (originY < 0) throw new ArgumentOutOfRangeException ("originY"); if (originY < 0) throw new ArgumentOutOfRangeException ("originY");
nint window = NCurses.NewWindow (height, width, originY, originX); nint window = NCurses.NewWindow (height, width, originY, originX);
if (borders) NCurses.Box (window, (char) 0, (char) 0); if (borders) NCurses.Box (window, (char) 0, (char) 0);
return window; return window;
} }
/// <summary> /// <summary>
/// Prints text horizontally centered to window (on a specific line); /// Prints text horizontally centered to window (on a specific line);
@ -70,13 +70,13 @@ public class MessageBox {
// then calculate the message box and inner pad origin X coords // then calculate the message box and inner pad origin X coords
var boxWidth = 50; var boxWidth = 50;
if (boxWidth + 3 >= screenWidth) boxWidth = screenWidth - 3; if (boxWidth + 3 >= screenWidth) boxWidth = screenWidth - 3;
int originX = (screenWidth / 2) - (boxWidth / 2); int originX = (screenWidth / 2) - (boxWidth / 2);
var padOriginX = originX + 2; // Add one for the border and for a single character padding var padOriginX = originX + 2; // Add one for the border and for a single character padding
// Calculate sizes for the inner pad element // Calculate sizes for the inner pad element
var padWidth = boxWidth - 4; // Add four to the total box width due to the two character padding on both sides var padWidth = boxWidth - 4; // Add four to the total box width due to the two character padding on both sides
var totalTextLines = (int) Math.Ceiling ((double) text.Length / padWidth); // Get total amount of lines required to hold all text limited by box width var totalTextLines = (int) Math.Ceiling ((double) text.Length / padWidth); // Get total amount of lines required to hold all text limited by box width
var padHeight = totalTextLines; var padHeight = totalTextLines;
if (padHeight + 8 > screenHeight) { // If height required to show all text lines plus padding is larger than screen, reduce size of message box if (padHeight + 8 > screenHeight) { // If height required to show all text lines plus padding is larger than screen, reduce size of message box
padHeight = screenHeight - 8; padHeight = screenHeight - 8;
} }
@ -187,81 +187,3 @@ public class MessageBox {
return index; return index;
} }
} }
public class ColorPairs {
public static int Ch_Topmenu_Label = 101;
public static int Ch_Topmenu_Label_Active = 102;
public static void InitColors () {
NCurses.InitPair (101, CursesColor.WHITE, CursesColor.BLUE);
NCurses.InitPair (102, CursesColor.BLACK, CursesColor.BLUE);
}
}
public class TopMenu {
private nint parentScreen;
private nint childWindow;
public List<MenuItem> MenuItems { get; set; } = new List<MenuItem> ();
public TopMenu (nint screen) {
parentScreen = screen;
NCurses.GetMaxYX (screen, out _, out int width);
childWindow = NCurses.NewWindow (1, width, 0, 0);
}
public void Render (int? activeItem = null) {
int index = 0;
uint
normalColorPair = 0,
activeColorPair = 0;
var hasColor = NCurses.HasColors ();
if (hasColor) {
normalColorPair = NCurses.ColorPair (ColorPairs.Ch_Topmenu_Label);
activeColorPair = NCurses.ColorPair (ColorPairs.Ch_Topmenu_Label_Active);
NCurses.WindowBackground (childWindow, normalColorPair);
NCurses.WindowRefresh (childWindow);
}
NCurses.MoveWindowAddString (childWindow, 0, 0, " ");
foreach (var item in MenuItems) {
var itemIsActive = activeItem is not null && index == (int) activeItem;
if (itemIsActive && hasColor) {
NCurses.WindowAttributeOn (childWindow, activeColorPair);
}
if (itemIsActive) {
NCurses.WindowAddString (childWindow, $"[{item.Label}] ");
} else {
NCurses.WindowAddString (childWindow, $" {item.Label} ");
}
if (itemIsActive && hasColor) {
NCurses.WindowAttributeOff (childWindow, activeColorPair);
}
index ++;
}
NCurses.WindowRefresh (childWindow);
}
public class MenuItem {
public required string Label { get; set; }
public string? HotKey { get; set; }
public MenuType Type { get; set; } = MenuType.Simple;
public List<MenuItem> SubMenuItems = new List<MenuItem> ();
public enum MenuType {
Simple,
TopMenu,
SubMenu,
Spacer
}
}
}

@ -3,39 +3,54 @@ using Mindmagma.Curses;
namespace SCI.CursesWrapper; namespace SCI.CursesWrapper;
public class CursesWrapper { public class CursesWrapper {
/// <summary> /// <summary>
/// Initializes NCurses and creates a screen /// Initializes NCurses and creates a screen
/// </summary> /// </summary>
public static nint InitNCurses () { public static nint InitNCurses () {
var screen = NCurses.InitScreen (); var screen = NCurses.InitScreen ();
NCurses.SetCursor (0); NCurses.SetCursor (0);
NCurses.UseDefaultColors ();
NCurses.NoEcho (); NCurses.NoEcho ();
NCurses.Raw ();
NCurses.Keypad (screen, true);
NCurses.NoDelay (screen, false);
if (NCurses.HasColors ()) { if (NCurses.HasColors ()) {
NCurses.StartColor (); NCurses.StartColor ();
ColorSchemes.InitAll (); ColorSchemes.InitAll ();
} }
return screen; return screen;
} }
/// <summary> /// <summary>
/// Get the total width of a window /// Get the total width of a window/screen
/// </summary> /// </summary>
/// <param name="window">Window to query</param> /// <param name="window">Window to query</param>
/// <returns>Width of window in columns</returns> /// <returns>Width of window in columns</returns>
public static int GetWidth (nint window) { public static int GetWidth (nint window) {
NCurses.GetMaxYX (window, out int _, out int width); NCurses.GetMaxYX (window, out int _, out int width);
return width; return width;
} }
/// <summary> /// <summary>
/// Get the total height of a window /// Get the total height of a window
/// </summary> /// </summary>
/// <param name="window">Window to query</param> /// <param name="window">Window to query</param>
/// <returns>Height of window in rows</returns> /// <returns>Height of window in rows</returns>
public static int GetHeight (nint window) { public static int GetHeight (nint window) {
NCurses.GetMaxYX (window, out int height, out int _); NCurses.GetMaxYX (window, out int height, out int _);
return height; return height;
} }
/// <summary>
/// Compares a key code to a character to test for a held
/// Control key during the key press event
/// </summary>
/// <param name="keyCode"></param>
/// <param name="testChar"></param>
/// <returns></returns>
public static bool KeyPressIsCtrl (int keyCode, int testChar) {
return keyCode == (testChar & 0x1f);
}
} }

@ -0,0 +1,12 @@
namespace SCI.CursesWrapper;
public class InnerWindow : Window {
public InnerWindow (nint rootScreen, InputHandler inputHandler) :
base (
0, 1,
CursesWrapper.GetWidth (rootScreen),
CursesWrapper.GetHeight (rootScreen) - 2,
inputHandler
)
{ }
}

@ -1,3 +1,4 @@
using System.Diagnostics;
using Mindmagma.Curses; using Mindmagma.Curses;
namespace SCI.CursesWrapper; namespace SCI.CursesWrapper;
@ -33,11 +34,6 @@ public class InputHandler {
} }
} }
/// <summary>
/// Root screen to fall back to when no window is active
/// </summary>
private nint rootScreen;
/// <summary> /// <summary>
/// Task running the endless input handler routine /// Task running the endless input handler routine
/// </summary> /// </summary>
@ -48,14 +44,6 @@ public class InputHandler {
/// </summary> /// </summary>
private CancellationTokenSource? listeningRoutineCts; private CancellationTokenSource? listeningRoutineCts;
/// <summary>
/// Class constructor
/// <paramref name="screen">Parent screen this InputHandler is attached to</paramref>
/// </summary>
public InputHandler (nint screen) {
rootScreen = screen;
}
/// <summary> /// <summary>
/// Start listening to key presses and call registered callbacks /// Start listening to key presses and call registered callbacks
/// </summary> /// </summary>
@ -96,59 +84,59 @@ public class InputHandler {
); );
} }
/// <summary>
/// Prepares the window for proper key press reading
/// </summary>
private void WindowSetup () {
if (ActiveWindow is not null) {
NCurses.Keypad (ActiveWindow.WindowId, true);
NCurses.NoDelay (ActiveWindow.WindowId, false);
} else {
NCurses.Keypad (rootScreen, true);
NCurses.NoDelay (rootScreen, false);
}
}
/// <summary>
/// Undoes any changes the setup routine configured for the active window
/// </summary>
private void WindowTeardown () {
if (ActiveWindow is not null) {
NCurses.Keypad (ActiveWindow.WindowId, false);
NCurses.NoDelay (ActiveWindow.WindowId, true);
} else {
NCurses.Keypad (rootScreen, false);
NCurses.NoDelay (rootScreen, true);
}
}
/// <summary> /// <summary>
/// Actual keypress handler being called as Task by StartListening () /// Actual keypress handler being called as Task by StartListening ()
/// </summary> /// </summary>
private void _ListeningRoutine () { private void _ListeningRoutine () {
if (listeningRoutineCts is null) return; if (listeningRoutineCts is null) return;
WindowSetup ();
int keyCode = -1;
while (!listeningRoutineCts.Token.IsCancellationRequested) { while (!listeningRoutineCts.Token.IsCancellationRequested) {
if (OnKeyPress is null) { int keyCode = -1;
Console.Title = "No handlers"; int keyCode2 = -1;
} else {
Console.Title = $"Got {OnKeyPress.GetInvocationList ().Length} handlers";
}
if (ActiveWindow is not null) { if (ActiveWindow is not null) {
keyCode = NCurses.WindowGetChar (ActiveWindow.WindowId); keyCode = NCurses.WindowGetChar (ActiveWindow.WindowId);
// If this block looks yanky; it is.
// This code checks if ESC was pressed and waits for another
// key press for another 10 milliseconds. This is to catch
// key combinations with the Alt key, as it is sent as
// [Alt] + [<key>]
if (keyCode == CursesKey.ESC) {
NCurses.WindowTimeOut (ActiveWindow.WindowId, 10);
try {
keyCode2 = NCurses.WindowGetChar (ActiveWindow.WindowId);
} catch (Exception) {
keyCode2 = -1;
}
NCurses.WindowTimeOut (ActiveWindow.WindowId, -1);
}
} else { } else {
keyCode = NCurses.GetChar (); keyCode = NCurses.GetChar ();
// Janky solution, see above
if (keyCode == CursesKey.ESC) {
NCurses.TimeOut (10);
try {
keyCode2 = NCurses.GetChar ();
} catch (Exception) {
keyCode2 = -1;
}
NCurses.TimeOut (-1);
}
} }
// Do nothing until the key code is larger than -1 // Do nothing until either key code is larger than -1
if (keyCode < 0) continue; if (keyCode < 0 && keyCode2 < 0) continue;
// Test if [Alt]+[key] combination was pressed.
// This is due to [Alt]+[key] being sent as [ESC], [key]
bool isAltCombo = keyCode == CursesKey.ESC && keyCode2 != -1;
if (isAltCombo) {
keyCode = keyCode2;
}
var eventArgs = new NCursesKeyPressEventArgs (keyCode, ActiveWindow); var eventArgs = new NCursesKeyPressEventArgs (keyCode, ActiveWindow, isAltCombo);
// Handle any registered privileged key handlers // Handle any registered privileged key handlers
if (OnKeyPressPrivileged is not null) { if (OnKeyPressPrivileged is not null) {
@ -183,8 +171,12 @@ public class NCursesKeyPressEventArgs : EventArgs {
public bool CancelNextEvent { get; set; } = false; public bool CancelNextEvent { get; set; } = false;
public NCursesKeyPressEventArgs (int keyCode, Window? sourceWindow) { private bool _isAltCombination = false;
public bool IsAltCombination { get { return _isAltCombination; } }
public NCursesKeyPressEventArgs (int keyCode, Window? sourceWindow, bool isAltCombination) {
_keyCode = keyCode; _keyCode = keyCode;
_sourceWindow = sourceWindow; _sourceWindow = sourceWindow;
_isAltCombination = isAltCombination;
} }
} }

@ -5,261 +5,351 @@ using Mindmagma.Curses;
namespace SCI.CursesWrapper; namespace SCI.CursesWrapper;
public class Window { public class Window {
/// <summary> public const int InnerPadding = 1;
/// Gets or sets the window position on the screen
/// </summary> /// <summary>
private Point _position; /// Gets or sets the window position on the screen
public Point Position { /// </summary>
get { private Point _position;
return _position; public Point Position {
} get {
set { return _position;
if (ParentWindow is not null) { }
NCurses.WindowMove ( set {
WindowId, if (ParentWindow is not null) {
ParentWindow.Position.Y + value.Y, int addedPadding = ParentWindow.BorderEnabled?
ParentWindow.Position.X + value.X InnerPadding + 1 : 0;
);
} else { NCurses.WindowMove (
NCurses.WindowMove ( WindowId,
WindowId, ParentWindow.Position.Y + value.Y + addedPadding,
value.Y, ParentWindow.Position.X + value.X + addedPadding
value.X );
); } else {
} NCurses.WindowMove (
_position = value; WindowId,
} value.Y,
} value.X
);
/// <summary> }
/// Gets or sets the width and height of the window _position = value;
/// </summary> }
private Size _windowSize; }
public Size WindowSize {
get { /// <summary>
return _windowSize; /// Gets or sets the width and height of the window
} /// </summary>
set { private Size _windowSize;
_windowSize = value; public Size WindowSize {
} get {
} return _windowSize;
}
/// <summary> set {
/// Gets or sets the window background color pair _windowSize = value;
/// </summary> }
private uint _backgroundColorId; }
public uint BackgroundColorId {
get { /// <summary>
return _backgroundColorId; /// Gets or sets the window background color pair
} /// </summary>
set { private uint _backgroundColorId;
NCurses.WindowBackground (WindowId, value); public uint BackgroundColorId {
Redraw (); get {
_backgroundColorId = value; return _backgroundColorId;
} }
} set {
NCurses.WindowBackground (WindowId, value);
/// <summary> Draw ();
/// Sets the parent window _backgroundColorId = value;
/// </summary> }
private Window? _parentWindow; }
public Window? ParentWindow { get { return _parentWindow; }}
private bool _borderEnabled;
/// <summary> public bool BorderEnabled {
/// Holds a list of children windows this window posesses get {
/// </summary> return _borderEnabled;
private List<Window> _childWindows = new List<Window> (); }
public ReadOnlyCollection<Window> ChildWindows { get { return _childWindows.AsReadOnly (); }} set {
SetBorder (value);
/// <summary> Draw ();
/// Holds the pointer for this window _borderEnabled = value;
/// </summary> }
private nint _windowId; }
public nint WindowId { get { return _windowId; }}
/// <summary>
/// <summary> /// Sets the parent window
/// Input handler assigned to this window /// </summary>
/// </summary> private Window? _parentWindow;
private InputHandler? inputHandler; public Window? ParentWindow { get { return _parentWindow; }}
/// <summary> /// <summary>
/// Event handler called when this window is active and a key is pressed /// Holds a list of children windows this window posesses
/// </summary> /// </summary>
public event InputHandler.KeypressEventHandler? OnKeyPress; private List<Window> _childWindows = new List<Window> ();
public ReadOnlyCollection<Window> ChildWindows { get { return _childWindows.AsReadOnly (); }}
/// <summary>
/// Create new window by specifying X/Y and Width/Height geometry /// <summary>
/// </summary> /// Holds the pointer for this window
/// <param name="x"></param> /// </summary>
/// <param name="y"></param> private nint _windowId;
/// <param name="width"></param> public nint WindowId { get { return _windowId; }}
/// <param name="height"></param>
/// <param name="parentWindow"></param> /// <summary>
public Window (int x, int y, int width, int height, Window? parentWindow = null) { /// Input handler assigned to this window
if (parentWindow is not null) { /// </summary>
_windowId = NCurses.DeriveWindow ( protected InputHandler? _targetInputHandler;
parentWindow.WindowId, public InputHandler? TargetInputHandler { get { return _targetInputHandler; }}
height, width,
y, x /// <summary>
); /// Event handler called when this window is active and a key is pressed
} else { /// </summary>
_windowId = NCurses.NewWindow ( public event InputHandler.KeypressEventHandler? OnKeyPress;
height, width,
y, x /// <summary>
); /// Create new window by specifying X/Y and Width/Height geometry
} /// </summary>
/// <param name="x"></param>
Redraw (); /// <param name="y"></param>
} /// <param name="width"></param>
/// <param name="height"></param>
/// <summary> /// <param name="parentWindow"></param>
/// Create new window by specifying geometry through Point and Size objects public Window (int x, int y, int width, int height, Window? parentWindow = null) {
/// </summary> _Initialize (new Point (x, y), new Size (width, height), parentWindow);
/// <param name="position"></param> }
/// <param name="windowSize"></param>
/// <param name="parentWindow"></param> /// <summary>
public Window (Point position, Size windowSize, Window? parentWindow = null) { /// Create new window by specifying geometry through Point and Size objects
if (parentWindow is not null) { /// </summary>
_windowId = NCurses.SubWindow ( /// <param name="position"></param>
parentWindow.WindowId, /// <param name="windowSize"></param>
windowSize.Height, windowSize.Width, /// <param name="parentWindow"></param>
position.Y, position.X public Window (Point position, Size windowSize, Window? parentWindow = null) {
); _Initialize (position, windowSize, parentWindow);
} else { }
_windowId = NCurses.NewWindow (
windowSize.Height, windowSize.Width, /// <summary>
position.Y, position.X /// Create new window by specifying X/Y and Width/Height geometry
); /// </summary>
} /// <param name="x"></param>
/// <param name="y"></param>
Redraw (); /// <param name="width"></param>
} /// <param name="height"></param>
/// <param name="targetInputHandler"></param>
/// <summary> /// <param name="parentWindow"></param>
/// Adds a child window to this window public Window (int x, int y, int width, int height, InputHandler targetInputHandler, Window? parentWindow = null) {
/// </summary> _Initialize (new Point (x, y), new Size (width, height), parentWindow);
/// <param name="child"></param> RegisterInputHandler (targetInputHandler);
public void AddChildWindow (Window child) { SetWindowActive ();
if (_childWindows.Contains (child)) return; }
_childWindows.Add (child);
} /// <summary>
/// Create new window by specifying geometry through Point and Size objects
/// <summary> /// </summary>
/// Removes a child window from this window /// <param name="position"></param>
/// </summary> /// <param name="windowSize"></param>
/// <param name="child"></param> /// <param name="targetInputHandler"></param>
public void RemoveChildWindow (Window child) { /// <param name="parentWindow"></param>
if (!_childWindows.Contains (child)) return; public Window (Point position, Size windowSize, InputHandler targetInputHandler, Window? parentWindow = null) {
_childWindows.Remove (child); _Initialize (position, windowSize, parentWindow);
} RegisterInputHandler (targetInputHandler);
SetWindowActive ();
/// <summary> }
/// Discards all optimization options about drawn parts of this window.
/// Call before drawing a sub window /// <summary>
/// </summary> /// Actual initialization function for this class
public void TouchWin () { /// </summary>
NCurses.TouchWindow (WindowId); /// <param name="position"></param>
} /// <param name="windowSize"></param>
/// <param name="parentWindow"></param>
/// <summary> private void _Initialize (Point position, Size windowSize, Window? parentWindow = null) {
/// Redraws this window if (parentWindow is not null) {
/// </summary> int addedPadding = parentWindow.BorderEnabled?
public void Redraw () { InnerPadding + 1 : 0;
NCurses.Refresh (); // TODO: Necessary?
_windowId = NCurses.DeriveWindow (
if (ChildWindows.Count > 0) { parentWindow.WindowId,
foreach (var window in ChildWindows) { windowSize.Height, windowSize.Width,
window.Redraw (); position.Y + addedPadding, position.X + addedPadding
} );
}
parentWindow.AddChildWindow (this);
if (ParentWindow is not null) ParentWindow.TouchWin (); } else {
NCurses.WindowRefresh (WindowId); _windowId = NCurses.NewWindow (
} windowSize.Height, windowSize.Width,
position.Y, position.X
/// <summary> );
/// Destroys this window and all children windows }
/// </summary>
public void Destroy () { _position = position;
if (ChildWindows.Count > 0) { _windowSize = windowSize;
foreach (var window in _childWindows) { _parentWindow = parentWindow;
window.Destroy ();
} NCurses.Keypad (WindowId, true);
}
Draw ();
if (inputHandler is not null) inputHandler.ActiveWindow = null; }
if (ParentWindow is not null) ParentWindow.RemoveChildWindow (this);
/// <summary>
UnregisterInputHandler (); /// Destroys the window and its sub-windows when the Window object is destroyed
/// </summary>
SetBorder (false); ~Window () {
Destroy ();
NCurses.ClearWindow (WindowId); }
Console.Title = "About to destroy";
NCurses.DeleteWindow (WindowId); /// <summary>
Console.Title = "Destroyed"; /// Adds a child window to this window
/// </summary>
//TODO: Program hangs on DeleteWindow /// <param name="child"></param>
} public void AddChildWindow (Window child) {
if (_childWindows.Contains (child)) return;
/// <summary> _childWindows.Add (child);
/// Register an input handler for this window to attach to OnKeyPress events }
/// </summary>
/// <param name="inputHandler">InputHandler to register</param> /// <summary>
public void RegisterInputHandler (InputHandler targetInputHandler) { /// Removes a child window from this window
if (inputHandler is not null) throw new Exception ( /// </summary>
"Another input handler is already registered" /// <param name="child"></param>
); public void RemoveChildWindow (Window child) {
_childWindows.Remove (child);
inputHandler = targetInputHandler; }
inputHandler.OnKeyPress += KeyPressHandler;
} /// <summary>
/// Discards all optimization options about drawn parts of this window.
/// <summary> /// Call before drawing a sub window
/// Detach from all OnKeyPress events and unset input handler /// </summary>
/// </summary> public void TouchWin () {
public void UnregisterInputHandler () { NCurses.TouchWindow (WindowId);
if (inputHandler is null) return; }
inputHandler.OnKeyPress -= KeyPressHandler; /// <summary>
} /// Draws this window and all sub windows
/// </summary>
/// <summary> public void Draw () {
/// Handle key press events from the input handler NCurses.Refresh (); // TODO: Necessary?
/// </summary>
/// <param name="sender"></param> foreach (var window in ChildWindows) {
/// <param name="e"></param> window.Draw ();
private void KeyPressHandler (object sender, NCursesKeyPressEventArgs e) { }
if (e.SourceWindow != this) return;
if (ParentWindow is not null) ParentWindow.TouchWin ();
if (OnKeyPress is not null) { NCurses.WindowRefresh (WindowId);
OnKeyPress (sender, e); }
}
} /// <summary>
/// Destroys this window and all children windows
/// <summary> /// </summary>
/// Tells the input handler this window is active public void Destroy () {
/// </summary> // Catch double destroy calls
public void SetWindowActive () { if (WindowId == -1) throw new Exception ("Destroy called twice on object");
if (inputHandler is null) return;
foreach (var window in _childWindows.ToList ()) {
inputHandler.ActiveWindow = this; window.Destroy ();
} }
/// <summary> UnregisterInputHandler ();
/// Enables or disables a border around this window
/// </summary> // Prepare for screen clear
/// <param name="enabled">Sets the status of the border</param> NCurses.WindowBackground (WindowId, NCurses.ColorPair (-1));
/// <param name="horizontalChar">If specified, uses this character as the top and bottom border</param> SetBorder (false);
/// <param name="verticalChar">If specified, uses this character as the left and right border</param>
public void SetBorder (bool enabled, char? horizontalChar = null, char? verticalChar = null) { // Clear window and redraw
if (horizontalChar is null) horizontalChar = (char) 0; NCurses.ClearWindow (WindowId);
if (verticalChar is null) verticalChar = (char) 0; Draw ();
if (enabled) { NCurses.DeleteWindow (WindowId);
NCurses.Box (WindowId, (char) horizontalChar, (char) verticalChar); _windowId = -1;
} else {
NCurses.Box (WindowId, ' ', ' '); // Ensures sure the parent is updated too
} if (ParentWindow is not null) {
} ParentWindow.RemoveChildWindow (this);
ParentWindow.Draw ();
}
}
/// <summary>
/// Register an input handler for this window to attach to OnKeyPress events
/// </summary>
/// <param name="targetInputHandler">InputHandler to register</param>
public void RegisterInputHandler (InputHandler inputHandler) {
if (TargetInputHandler is not null) throw new Exception (
"Another input handler is already registered"
);
_targetInputHandler = inputHandler;
TargetInputHandler!.OnKeyPress += KeyPressHandler;
}
/// <summary>
/// Detach from all OnKeyPress events and unset input handler
/// </summary>
public void UnregisterInputHandler () {
if (TargetInputHandler is null) return;
if (TargetInputHandler.ActiveWindow == this) TargetInputHandler.ActiveWindow = null;
TargetInputHandler.OnKeyPress -= KeyPressHandler;
}
/// <summary>
/// Handle key press events from the input handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void KeyPressHandler (object sender, NCursesKeyPressEventArgs e) {
if (e.SourceWindow != this) return;
if (OnKeyPress is not null) {
OnKeyPress (this, e);
}
}
/// <summary>
/// Tells the input handler this window is active
/// </summary>
public void SetWindowActive () {
if (TargetInputHandler is null) return;
TargetInputHandler.ActiveWindow = this;
Draw ();
}
/// <summary>
/// Enables or disables a border around this window
/// </summary>
/// <param name="enabled">Sets the status of the border</param>
/// <param name="horizontalChar">If specified, uses this character as the top and bottom border</param>
/// <param name="verticalChar">If specified, uses this character as the left and right border</param>
public void SetBorder (bool enabled, char? horizontalChar = null, char? verticalChar = null) {
if (horizontalChar is null) horizontalChar = (char) 0;
if (verticalChar is null) verticalChar = (char) 0;
if (enabled) {
NCurses.Box (WindowId, (char) horizontalChar, (char) verticalChar);
} else {
NCurses.Box (WindowId, ' ', ' ');
}
}
/// <summary>
/// Calcaulates the usable inner width of this window
/// </summary>
/// <returns>Usable inner width of window in columns</returns>
public int GetUsableWidth () {
if (BorderEnabled) {
return WindowSize.Width - 1 - InnerPadding;
} else {
return WindowSize.Width;
}
}
/// <summary>
/// Calcaulates the usable inner height of this window
/// </summary>
/// <returns>Usable inner height of window in rows</returns>
public int GetUsableHeight () {
if (BorderEnabled) {
return WindowSize.Height - 1 - InnerPadding;
} else {
return WindowSize.Height;
}
}
} }

@ -4,94 +4,94 @@ using Newtonsoft.Json;
namespace Fahrplan; namespace Fahrplan;
public class Root { public class Root {
[JsonProperty("$schema")] [JsonProperty("$schema")]
public string schema { get; set; } public string schema { get; set; }
public Generator generator { get; set; } public Generator generator { get; set; }
public Schedule schedule { get; set; } public Schedule schedule { get; set; }
} }
public class Colors { public class Colors {
public string primary { get; set; } public string primary { get; set; }
} }
public class Conference { public class Conference {
public string acronym { get; set; } public string acronym { get; set; }
public string title { get; set; } public string title { get; set; }
public string start { get; set; } public string start { get; set; }
public string end { get; set; } public string end { get; set; }
public int daysCount { get; set; } public int daysCount { get; set; }
public string timeslot_duration { get; set; } public string timeslot_duration { get; set; }
public string time_zone_name { get; set; } public string time_zone_name { get; set; }
public Colors colors { get; set; } public Colors colors { get; set; }
public List<Room> rooms { get; set; } public List<Room> rooms { get; set; }
public List<Track> tracks { get; set; } public List<Track> tracks { get; set; }
public List<Day> days { get; set; } public List<Day> days { get; set; }
} }
public class Day { public class Day {
public int index { get; set; } public int index { get; set; }
public string date { get; set; } public string date { get; set; }
public DateTime day_start { get; set; } public DateTime day_start { get; set; }
public DateTime day_end { get; set; } public DateTime day_end { get; set; }
public Dictionary<string, List<RoomEvent>> rooms { get; set; } public Dictionary<string, List<RoomEvent>> rooms { get; set; }
} }
public class Generator { public class Generator {
public string name { get; set; } public string name { get; set; }
public string version { get; set; } public string version { get; set; }
} }
public class Person { public class Person {
public string guid { get; set; } public string guid { get; set; }
public int id { get; set; } public int id { get; set; }
public string code { get; set; } public string code { get; set; }
public string public_name { get; set; } public string public_name { get; set; }
public string avatar { get; set; } public string avatar { get; set; }
public string biography { get; set; } public string biography { get; set; }
public List<object> answers { get; set; } public List<object> answers { get; set; }
} }
public class Room { public class Room {
public string name { get; set; } public string name { get; set; }
public string guid { get; set; } public string guid { get; set; }
public object description { get; set; } public object description { get; set; }
public int capacity { get; set; } public int capacity { get; set; }
} }
public class Track { public class Track {
public string name { get; set; } public string name { get; set; }
public string color { get; set; } public string color { get; set; }
} }
public class RoomEvent { public class RoomEvent {
public string url { get; set; } public string url { get; set; }
public int id { get; set; } public int id { get; set; }
public string guid { get; set; } public string guid { get; set; }
public DateTime date { get; set; } public DateTime date { get; set; }
public string start { get; set; } public string start { get; set; }
public string logo { get; set; } public string logo { get; set; }
public string duration { get; set; } public string duration { get; set; }
public string room { get; set; } public string room { get; set; }
public string slug { get; set; } public string slug { get; set; }
public string title { get; set; } public string title { get; set; }
public string subtitle { get; set; } public string subtitle { get; set; }
public string track { get; set; } public string track { get; set; }
public string type { get; set; } public string type { get; set; }
public string language { get; set; } public string language { get; set; }
public string @abstract { get; set; } public string @abstract { get; set; }
public string description { get; set; } public string description { get; set; }
public string recording_license { get; set; } public string recording_license { get; set; }
public bool do_not_record { get; set; } public bool do_not_record { get; set; }
public List<Person> persons { get; set; } public List<Person> persons { get; set; }
public List<object> links { get; set; } public List<object> links { get; set; }
public List<object> attachments { get; set; } public List<object> attachments { get; set; }
public List<object> answers { get; set; } public List<object> answers { get; set; }
} }
public class Schedule { public class Schedule {
public string url { get; set; } public string url { get; set; }
public string version { get; set; } public string version { get; set; }
public string base_url { get; set; } public string base_url { get; set; }
public Conference conference { get; set; } public Conference conference { get; set; }
} }

@ -7,7 +7,7 @@ namespace ANSI_Fahrplan;
public class Playground { public class Playground {
public static void Run (string [] args) { public static void Run (string [] args) {
Console.WriteLine ("Program initializing Curses..."); Console.WriteLine ("Program initializing Curses...");
// var jsonString = File.ReadAllText ("schedule.json"); // var jsonString = File.ReadAllText ("schedule.json");
// var schedule = JsonConvert.DeserializeObject<Fahrplan.Root> (jsonString); // var schedule = JsonConvert.DeserializeObject<Fahrplan.Root> (jsonString);
@ -18,22 +18,22 @@ public class Playground {
// Console.WriteLine ($" \\__ Room: {room.Key}"); // Console.WriteLine ($" \\__ Room: {room.Key}");
// foreach (var ev in room.Value) { // foreach (var ev in room.Value) {
// if (ev is null) continue; // if (ev is null) continue;
// Console.WriteLine ($" \\__ Event: {ev.title}"); // Console.WriteLine ($" \\__ Event: {ev.title}");
// } // }
// } // }
// } // }
var screen = NCurses.InitScreen (); var screen = NCurses.InitScreen ();
NCurses.NoEcho (); NCurses.NoEcho ();
NCurses.CBreak (); NCurses.CBreak ();
NCurses.SetCursor (0); NCurses.SetCursor (0);
var hasColors = NCurses.HasColors (); var hasColors = NCurses.HasColors ();
if (hasColors) { if (hasColors) {
NCurses.StartColor (); NCurses.StartColor ();
NCurses.InitPair (1, CursesColor.WHITE, CursesColor.BLUE); NCurses.InitPair (1, CursesColor.WHITE, CursesColor.BLUE);
//ColorPairs.InitColors (); //ColorPairs.InitColors ();
} }
var win1 = NCurses.NewWindow (10, 20, 10, 10); var win1 = NCurses.NewWindow (10, 20, 10, 10);
NCurses.Box (win1, (char) 0, (char) 0); NCurses.Box (win1, (char) 0, (char) 0);
@ -48,67 +48,67 @@ public class Playground {
NCurses.GetChar (); NCurses.GetChar ();
NCurses.EndWin (); NCurses.EndWin ();
/*var window = CreateWindowCentered (screen, 40, 12); /*var window = CreateWindowCentered (screen, 40, 12);
NCurses.WindowBackground (window, NCurses.ColorPair (1)); NCurses.WindowBackground (window, NCurses.ColorPair (1));
NCurses.WindowAttributeOn (window, NCurses.ColorPair (1));*/ NCurses.WindowAttributeOn (window, NCurses.ColorPair (1));*/
//NCurses.GetMaxYX (screen, out int screenHeight, out int screenWidth); //NCurses.GetMaxYX (screen, out int screenHeight, out int screenWidth);
//NCurses.MoveAddString (screenHeight - 1, 0, "NCurses Example"); //NCurses.MoveAddString (screenHeight - 1, 0, "NCurses Example");
/*var menuItems = new List<TopMenu.MenuItem> () { /*var menuItems = new List<TopMenu.MenuItem> () {
new TopMenu.MenuItem () { Label = "Test 1" }, new TopMenu.MenuItem () { Label = "Test 1" },
new TopMenu.MenuItem () { Label = "Test 2" }, new TopMenu.MenuItem () { Label = "Test 2" },
new TopMenu.MenuItem () { Label = "Test 3" }, new TopMenu.MenuItem () { Label = "Test 3" },
}; };
var menu = new TopMenu (screen) { MenuItems = menuItems }; var menu = new TopMenu (screen) { MenuItems = menuItems };
menu.Render ();*/ menu.Render ();*/
var msgBoxResponse = MessageBox.Show (screen, "Hello World, this is a message box text, yippee!! Let's make this text even longer, wooowiieeeee!", MessageBox.MessageBoxButtons.YesNo); var msgBoxResponse = MessageBox.Show (screen, "Hello World, this is a message box text, yippee!! Let's make this text even longer, wooowiieeeee!", MessageBox.MessageBoxButtons.YesNo);
NCurses.AddString ($"Input was {msgBoxResponse}"); NCurses.AddString ($"Input was {msgBoxResponse}");
NCurses.Refresh (); NCurses.Refresh ();
var inputTask = Task.Run (() => InputRoutine ()); var inputTask = Task.Run (() => InputRoutine ());
while (inputTask.Status == TaskStatus.WaitingToRun); while (inputTask.Status == TaskStatus.WaitingToRun);
while (inputTask.Status == TaskStatus.Running) { while (inputTask.Status == TaskStatus.Running) {
NCurses.Refresh (); NCurses.Refresh ();
//NCurses.WindowRefresh (window); //NCurses.WindowRefresh (window);
NCurses.Nap (1000 / 30); NCurses.Nap (1000 / 30);
} }
//NCurses.AttributeOff (NCurses.ColorPair (1)); //NCurses.AttributeOff (NCurses.ColorPair (1));
NCurses.EndWin (); NCurses.EndWin ();
} }
static void InputRoutine () { static void InputRoutine () {
int chr = 0; int chr = 0;
while (chr != CursesKey.ESC) { while (chr != CursesKey.ESC) {
chr = NCurses.GetChar (); chr = NCurses.GetChar ();
/*if (chr == 265) { /*if (chr == 265) {
menu.Render (0); menu.Render (0);
} else if (chr == 266) { } else if (chr == 266) {
menu.Render (1); menu.Render (1);
} else if (chr == 267) { } else if (chr == 267) {
menu.Render (2); menu.Render (2);
} else if (chr > 0) { } else if (chr > 0) {
menu.Render (); menu.Render ();
NCurses.MoveAddString (2, 10, $"You pressed the key {chr} "); NCurses.MoveAddString (2, 10, $"You pressed the key {chr} ");
}*/ }*/
if (chr > 0) NCurses.MoveAddString (2, 10, $"You pressed the key {chr} "); if (chr > 0) NCurses.MoveAddString (2, 10, $"You pressed the key {chr} ");
} }
/*NCurses.WindowBorder (window, ' ', ' ', ' ',' ',' ',' ',' ',' '); /*NCurses.WindowBorder (window, ' ', ' ', ' ',' ',' ',' ',' ',' ');
NCurses.WindowRefresh (window); NCurses.WindowRefresh (window);
NCurses.DeleteWindow (window);*/ NCurses.DeleteWindow (window);*/
NCurses.MoveAddString (10, 10, "Input routine exited"); NCurses.MoveAddString (10, 10, "Input routine exited");
NCurses.Refresh (); NCurses.Refresh ();
return; return;
} }
} }

@ -1,13 +1,12 @@
using SCI.CursesWrapper; using SCI.CursesWrapper;
using SCI.CursesWrapper.UiElements;
using ANSI_Fahrplan.Screens; using ANSI_Fahrplan.Screens;
using Mindmagma.Curses; using Mindmagma.Curses;
namespace ANSI_Fahrplan; namespace ANSI_Fahrplan;
class Program { class Program {
private static void Main (string [] args) { private static void Main (string [] args) {
//Playground.Run (args);
/** /**
* General procedure: * General procedure:
* - Draw welcome screen * - Draw welcome screen
@ -20,96 +19,76 @@ class Program {
**/ **/
var screen = CursesWrapper.InitNCurses (); var screen = CursesWrapper.InitNCurses ();
//var topLevelWindows = new List<Window> ();
// -- Screen-wide input handler -- // // -- Screen-wide input handler -- //
var inputHandler = new InputHandler (screen); var inputHandler = new InputHandler ();
inputHandler.StartListening (); inputHandler.StartListening ();
// Register quit key on main screen // Register global quit key
inputHandler.OnKeyPress += (object sender, NCursesKeyPressEventArgs e) => { inputHandler.OnKeyPressPrivileged += (object sender, NCursesKeyPressEventArgs e) => {
if (e.SourceWindow is not null) return; if (!CursesWrapper.KeyPressIsCtrl (e.KeyCode, 'q')) return;
if (e.KeyCode != 'q') return;
NCurses.EndWin (); NCurses.EndWin ();
Console.WriteLine ("Bye-bye!"); Console.WriteLine ("Bye-bye!");
Environment.Exit (0); Environment.Exit (0);
}; };
// -- Create menu bar -- // // -- Inner Window -- //
//var topMenu = new TopMenu (screen, CreateMenuItems (screen, screenInputHandler)); //
// This window contains all the dynamic content between
// -- Show introduction screen -- // // the menu and status bars
// NCurses.GetMaxYX (screen, out int height, out int width); var innerWindow = new InnerWindow (screen, inputHandler) {
BorderEnabled = true
// var innerWindow = NCurses.NewWindow (height - 2, width - 4, 1, 2); };
// NCurses.Box (innerWindow, (char) 0, (char) 0);
var introScreen = new IntroScreen (screen); // -- Intro screen -- //
introScreen.SetBorder (true); var introScreen = new IntroScreen (innerWindow);
introScreen.Redraw ();
var parentWindow = new Window (3, 3, 10 * 2, 10) { // Close intro screen on any keypress
BackgroundColorId = ColorSchemes.TextInputField () introScreen.OnKeyPress += (object sender, NCursesKeyPressEventArgs e) => {
((Window) sender).Destroy ();
}; };
parentWindow.SetBorder (true);
parentWindow.Redraw ();
var childWindow = new Window (1, 1, 3, 3, parentWindow); introScreen.RegisterInputHandler (inputHandler);
childWindow.SetBorder (true); introScreen.SetWindowActive ();
childWindow.Redraw ();
inputHandler.OnKeyPress += (object sender, NCursesKeyPressEventArgs e) => { // Wait until intro screen is closed
if (e.SourceWindow is not null) { while (introScreen.WindowId > -1) Thread.Sleep (50); // TODO; Unjank this
NCurses.WindowAddChar (e.SourceWindow.WindowId, e.KeyCode);
return;
} else {
NCurses.AddChar (e.KeyCode);
}
if (e.KeyCode != 'd') return;
parentWindow.Destroy ();
};
NCurses.AddString ("Done drawing"); // -- Create menu bar -- //
NCurses.Refresh (); var topMenu = new TopMenu (screen, inputHandler, CreateMenuItems (innerWindow));
// Wait until the input handler routine stops // Wait until the input handler routine stops
while (true) Thread.Sleep (500); while (inputHandler.IsListening ()) Thread.Sleep (50);
NCurses.EndWin (); NCurses.EndWin ();
Console.WriteLine ("Oh wow, the input handler crashed, that's not supposed to happen. Sorry!"); Console.WriteLine ("Oh wow, the input handler crashed, that's not supposed to happen. Sorry!");
Environment.Exit (1); Environment.Exit (1);
} }
// private static List<MenuItem> CreateMenuItems (nint screen, InputHandler inputHandler) { private static List<MenuItem> CreateMenuItems (InnerWindow innerWindow) {
// var helpItem = new MenuItem ("Help", "F1", inputHandler); var helpItem = new MenuItem ("Help", "F1");
// var upcomingItem = new MenuItem ("Upcoming", "F2", inputHandler); var upcomingItem = new MenuItem ("Upcoming", "b"); //F2
// var byDayItem = new MenuItem ("By Day", "F3", inputHandler); var byDayItem = new MenuItem ("By Day", "n"); // F3
// var byRoomItem = new MenuItem ("By Room", "F4", inputHandler); var byRoomItem = new MenuItem ("By Room", "F4");
// var bySpeakerItem = new MenuItem ("By Speaker", "F5", inputHandler); var bySpeakerItem = new MenuItem ("By Speaker", "F5");
// var quitItem = new MenuItem ("Quit", "q", inputHandler); var byTestItem = new MenuItem ("By Test", "F7");
var quitItem = new MenuItem ("Quit (C-q)");
// quitItem.OnItemActivated += (object? sender, EventArgs e) => {
// NCurses.EndWin (); helpItem.OnItemActivated += (object sender, MenuItemActivatedEventArgs e) => {
// Console.WriteLine ("Bye-bye!");
// Environment.Exit (0); };
// };
return new List<MenuItem> {
// helpItem.OnItemActivated += (object? sender, EventArgs e) => { helpItem,
// InputBox.InputCompleted callback = (string ee) => { upcomingItem,
// NCurses.MoveAddString (3, 3, $"<{ee}>"); byDayItem,
// }; byRoomItem,
// new InputBox().RequestInput (screen, inputHandler, callback, "Please enter some text now:"); bySpeakerItem,
// }; quitItem
};
// return new List<MenuItem> { }
// helpItem,
// upcomingItem,
// byDayItem,
// byRoomItem,
// bySpeakerItem,
// quitItem
// };
// }
} }

@ -1,12 +0,0 @@
namespace ANSI_Fahrplan.Screens;
/// <summary>
/// NCurses Header Window
/// </summary>
public class Header {
public Header () {
}
}

@ -1,28 +1,26 @@
using Mindmagma.Curses;
using SCI.CursesWrapper; using SCI.CursesWrapper;
namespace ANSI_Fahrplan.Screens; namespace ANSI_Fahrplan.Screens;
public class IntroScreen : Window { public class IntroScreen : Window {
private nint infoTextBox;
/// <summary> /// <summary>
/// Pass class constructor through to inherited constructor /// Pass class constructor through to inherited constructor
/// </summary> /// </summary>
/// <param name="parentWindow">Parent window of this window object</param> /// <param name="parentWindow">Parent window of this window object</param>
public IntroScreen (nint screen) : public IntroScreen (Window parentWindow) :
base ( base (
1, 1, 0, 0,
CursesWrapper.GetWidth (screen) - 2, parentWindow.GetUsableWidth (),
CursesWrapper.GetHeight (screen) - 2 parentWindow.GetUsableHeight (),
parentWindow
) )
{ {
string asciiArtPlusText = string asciiArtPlusText =
"Press F1 for a quick start guide or simply press Enter\n" + "Press F1 for a quick start guide or simply press Enter\n" +
"to see upcoming events\n" + "to see upcoming events\n" +
AsciiArt.logo38c3_80x24 + AsciiArt.logo38c3_80x24 +
"\n38C3 Fahrplan in your terminal!"; "\n38C3 Fahrplan in your terminal!";
AsciiArt.ShowCentered (WindowId, asciiArtPlusText); AsciiArt.ShowCentered (WindowId, asciiArtPlusText);
} }
} }

@ -14,8 +14,8 @@ public abstract class Screen {
private void PrepareWindow () { private void PrepareWindow () {
NCurses.NoEcho (); NCurses.NoEcho ();
NCurses.CBreak (); NCurses.CBreak ();
NCurses.SetCursor (0); NCurses.SetCursor (0);
//NCurses.ClearWindow (RootWindow); //NCurses.ClearWindow (RootWindow);
NCurses.WindowMove (RootWindow, 0, 0); NCurses.WindowMove (RootWindow, 0, 0);

@ -2,7 +2,7 @@ namespace ANSI_Fahrplan.Screens;
public abstract class ScrollableScreen : Screen { public abstract class ScrollableScreen : Screen {
public ScrollableScreen (nint rootWindow) : base (rootWindow) {} public ScrollableScreen (nint rootWindow) : base (rootWindow) {}
} }

@ -1,18 +1,17 @@
using SCI.CursesWrapper;
using Mindmagma.Curses; using Mindmagma.Curses;
namespace ANSI_Fahrplan.UiElements; namespace SCI.CursesWrapper.UiElements;
public class InputBox { public class InputBox {
public delegate void InputCompleted (string input); public delegate void InputCompleted (string input);
public void RequestInput (nint screen, InputHandler inputHandler, InputCompleted callback, string question) { public void RequestInput (nint screen, InputHandler inputHandler, InputCompleted callback, string question) {
const int boxPaddingTop = 1; const int boxPaddingTop = 1;
const int boxPaddingSides = 2; const int boxPaddingSides = 2;
const int boxPaddingBottom = 2; const int boxPaddingBottom = 2;
const int boxMarginTopBottom = 1; const int boxMarginTopBottom = 1;
const int boxMarginSides = 2; const int boxMarginSides = 2;
NCurses.GetMaxYX (screen, out int height, out int width); NCurses.GetMaxYX (screen, out int height, out int width);

@ -1,7 +1,6 @@
using SCI.CursesWrapper;
using Mindmagma.Curses; using Mindmagma.Curses;
namespace ANSI_Fahrplan.UiElements; namespace SCI.CursesWrapper.UiElements;
public class MenuItem { public class MenuItem {
private List<MenuItem> _childItems; private List<MenuItem> _childItems;
@ -10,43 +9,75 @@ public class MenuItem {
private string _label; private string _label;
public string Label { get { return _label; }} public string Label { get { return _label; }}
private string _key; private string? _key;
public string Key { get { return _key; }} public string? Key { get { return _key; }}
public delegate void ItemActivated (object sender, EventArgs e); public delegate void ItemActivated (object sender, MenuItemActivatedEventArgs e);
// Event fired when menu item is activated // Event fired when menu item is activated
public event EventHandler? OnItemActivated; public event ItemActivated? OnItemActivated;
private InputHandler _inputHandler; private TopMenu? _parentTopMenuWindow;
public InputHandler InputHandler { get { return _inputHandler; }} public TopMenu? ParentTopMenuWindow { get { return _parentTopMenuWindow; }}
public MenuItem (string label, string key, InputHandler inputHandler, List<MenuItem>? childItems = null) { /// <summary>
/// Class constructor
/// </summary>
/// <param name="label"></param>
/// <param name="key"></param>
/// <param name="childItems"></param>
public MenuItem (string label, string? key = null, List<MenuItem>? childItems = null) {
_childItems = childItems ?? new List<MenuItem> (); _childItems = childItems ?? new List<MenuItem> ();
_key = key; _key = key;
_inputHandler = inputHandler;
_label = label; _label = label;
} }
public void KeyHandler (object sender, NCursesKeyPressEventArgs e) { /// <summary>
/// Sets this menu items parent TopMenu window
/// </summary>
/// <param name="parent">TopMenu window this menu item belongs to</param>
public void AssignParentTopMenuWindow (TopMenu parent) {
if (_parentTopMenuWindow is not null) return;
_parentTopMenuWindow = parent;
}
/// <summary>
/// Called when key on parent TopMenu window is pressed
/// </summary>
/// <param name="sender">Event sender, should be a Window object</param>
/// <param name="e">Key Press Event arguments</param>
public void KeypressHandler (object sender, NCursesKeyPressEventArgs e) {
// Do not listen to key presses if not hotkey is assigned
if (Key is null) return;
// Do not react to key presses unless this menu items belongs to a window
// and has event listeners upon menu item activation
if (OnItemActivated is null) return; if (OnItemActivated is null) return;
if (ParentTopMenuWindow is null) return;
var eventArgs = new MenuItemActivatedEventArgs (ParentTopMenuWindow, this);
if (Key.Length > 1 && Key.StartsWith ("F")) { // Handle F keys if (Key.Length > 1 && Key.StartsWith ("F")) { // Handle F keys
if (e.KeyCode == CursesKey.KEY_F (int.Parse (Key.Substring (1)))) { if (e.KeyCode == CursesKey.KEY_F (int.Parse (Key.Substring (1)))) {
OnItemActivated (this, EventArgs.Empty); OnItemActivated (this, eventArgs);
} }
} else if (Key.Length == 1) { // Handle letters and numbers } else if (Key.Length == 1) { // Handle letters and numbers
if (e.KeyCode == Key [0]) { if (e.KeyCode == Key [0]) {
OnItemActivated (this, EventArgs.Empty); OnItemActivated (this, eventArgs);
} }
} else throw new NotImplementedException ("Currently only F-keys and letters work for Top Menu actions"); } else throw new NotImplementedException ("Currently only F-keys and letters work for Top Menu actions");
} }
}
public void RegisterKeyHandler () { public class MenuItemActivatedEventArgs : EventArgs {
InputHandler.OnKeyPress += KeyHandler; private TopMenu _topMenuWindow;
} public TopMenu TopMenuWindow { get { return _topMenuWindow; }}
private MenuItem _relatedMenuItem;
public MenuItem RelatedMenuItem { get { return _relatedMenuItem; }}
public void UnregisterKeyHandler () { public MenuItemActivatedEventArgs (TopMenu sourceTopMenuWindow, MenuItem relatedMenuItem) {
InputHandler.OnKeyPress -= KeyHandler; _topMenuWindow = sourceTopMenuWindow;
_relatedMenuItem = relatedMenuItem;
} }
} }

@ -1,42 +1,49 @@
using SCI.CursesWrapper;
using Mindmagma.Curses; using Mindmagma.Curses;
using System.Collections.ObjectModel;
namespace ANSI_Fahrplan.UiElements; namespace SCI.CursesWrapper.UiElements;
public class TopMenu : UiElement { public class TopMenu : Window {
private List<MenuItem> _menuItems = new List<MenuItem> (); private List<MenuItem> _menuItems = new List<MenuItem> ();
public List<MenuItem> MenuItems { public ReadOnlyCollection<MenuItem> MenuItems {
get { get {
return _menuItems; return _menuItems.AsReadOnly ();
}
set {
UnregisterItemEventHandlers ();
_menuItems = value;
RegisterItemEventHandlers ();
DrawMenu ();
} }
} }
private MenuItem? activeMenuItem; private MenuItem? activeMenuItem;
public TopMenu (nint screen, List<MenuItem> menuItems) : base (screen) { /// <summary>
NCurses.GetMaxYX (screen, out _, out int width); /// Class constructor
innerWindow = NCurses.NewWindow (1, width, 0, 0); /// </summary>
/// <param name="screen"></param>
/// <param name="targetInputHandler"></param>
/// <param name="menuItems"></param>
public TopMenu (nint screen, InputHandler targetInputHandler, List<MenuItem> menuItems) :
base (0, 0, CursesWrapper.GetWidth (screen), 1, targetInputHandler)
{
var colorSchemeNormal = ColorSchemes.TopMenuNormal (); var colorSchemeNormal = ColorSchemes.TopMenuNormal ();
NCurses.WindowBackground (innerWindow, colorSchemeNormal); BackgroundColorId = colorSchemeNormal;
NCurses.WindowAttributeOn (innerWindow, colorSchemeNormal); NCurses.WindowAttributeOn (WindowId, colorSchemeNormal);
_menuItems = menuItems;
foreach (var item in menuItems) {
item.AssignParentTopMenuWindow (this);
}
MenuItems = menuItems; RegisterItemEventHandlers ();
DrawMenu ();
} }
/// <summary> /// <summary>
/// Register key event handlers for each menu item /// Register key event handlers for each menu item
/// </summary> /// </summary>
private void RegisterItemEventHandlers () { private void RegisterItemEventHandlers () {
if (TargetInputHandler is null) return;
foreach (var item in MenuItems) { foreach (var item in MenuItems) {
item.OnItemActivated += ItemActivatedHandler; item.OnItemActivated += ItemActivatedHandler;
item.RegisterKeyHandler (); TargetInputHandler.OnKeyPressPrivileged += item.KeypressHandler;
} }
} }
@ -44,9 +51,11 @@ public class TopMenu : UiElement {
/// Unregister key event handlers for each menu item /// Unregister key event handlers for each menu item
/// </summary> /// </summary>
private void UnregisterItemEventHandlers () { private void UnregisterItemEventHandlers () {
if (TargetInputHandler is null) return;
foreach (var item in MenuItems) { foreach (var item in MenuItems) {
item.OnItemActivated -= ItemActivatedHandler; item.OnItemActivated -= ItemActivatedHandler;
item.UnregisterKeyHandler (); OnKeyPress -= item.KeypressHandler;
} }
} }
@ -66,7 +75,7 @@ public class TopMenu : UiElement {
/// Draw the top menu with currently configured menu items /// Draw the top menu with currently configured menu items
/// </summary> /// </summary>
private void DrawMenu () { private void DrawMenu () {
NCurses.ClearWindow (innerWindow); NCurses.ClearWindow (WindowId);
var normalColor = ColorSchemes.TopMenuNormal (); var normalColor = ColorSchemes.TopMenuNormal ();
var activeColor = ColorSchemes.TopMenuActive (); var activeColor = ColorSchemes.TopMenuActive ();
@ -77,8 +86,8 @@ public class TopMenu : UiElement {
if (itemIsActive) { if (itemIsActive) {
itemString = "["; itemString = "[";
NCurses.WindowAttributeOff (innerWindow, normalColor); NCurses.WindowAttributeOff (WindowId, normalColor);
NCurses.WindowAttributeOn (innerWindow, activeColor); NCurses.WindowAttributeOn (WindowId, activeColor);
} }
itemString += $"{item.Label}"; itemString += $"{item.Label}";
@ -88,21 +97,16 @@ public class TopMenu : UiElement {
} }
if (itemIsActive) { if (itemIsActive) {
NCurses.WindowAttributeOff (innerWindow, activeColor); NCurses.WindowAttributeOff (WindowId, activeColor);
NCurses.WindowAttributeOn (innerWindow, normalColor); NCurses.WindowAttributeOn (WindowId, normalColor);
itemString += "] "; itemString += "] ";
} else { } else {
itemString += " "; itemString += " ";
} }
NCurses.WindowAddString (innerWindow, itemString); NCurses.WindowAddString (WindowId, itemString);
} }
NCurses.Refresh (); Draw ();
NCurses.WindowRefresh (innerWindow);
}
public void Refresh () {
DrawMenu ();
} }
} }

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="dotnet-curses" Version="1.0.3" /> <PackageReference Include="dotnet-curses" Version="1.0.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

Loading…
Cancel
Save