diff --git a/NCursesColorSchemes.cs b/CursesWrapper/ColorSchemes.cs similarity index 95% rename from NCursesColorSchemes.cs rename to CursesWrapper/ColorSchemes.cs index 64c5be9..1d7f8ff 100644 --- a/NCursesColorSchemes.cs +++ b/CursesWrapper/ColorSchemes.cs @@ -1,6 +1,6 @@ using Mindmagma.Curses; -namespace ANSI_Fahrplan; +namespace SCI.CursesWrapper; public class ColorSchemes { public static void InitAll () { diff --git a/CursesWrapper/CursesWrapper.cs b/CursesWrapper/CursesWrapper.cs new file mode 100644 index 0000000..b539e5d --- /dev/null +++ b/CursesWrapper/CursesWrapper.cs @@ -0,0 +1,41 @@ +using Mindmagma.Curses; + +namespace SCI.CursesWrapper; + +public class CursesWrapper { + /// + /// Initializes NCurses and creates a screen + /// + public static nint InitNCurses () { + var screen = NCurses.InitScreen (); + NCurses.SetCursor (0); + NCurses.NoEcho (); + + if (NCurses.HasColors ()) { + NCurses.StartColor (); + ColorSchemes.InitAll (); + } + + return screen; + } + + /// + /// Get the total width of a window + /// + /// Window to query + /// Width of window in columns + public static int GetWidth (nint window) { + NCurses.GetMaxYX (window, out int _, out int width); + return width; + } + + /// + /// Get the total height of a window + /// + /// Window to query + /// Height of window in rows + public static int GetHeight (nint window) { + NCurses.GetMaxYX (window, out int height, out int _); + return height; + } +} \ No newline at end of file diff --git a/InputHandler.cs b/CursesWrapper/InputHandler.cs similarity index 55% rename from InputHandler.cs rename to CursesWrapper/InputHandler.cs index 6b17fd3..80e1b04 100644 --- a/InputHandler.cs +++ b/CursesWrapper/InputHandler.cs @@ -1,7 +1,6 @@ -using System.Security.Principal; using Mindmagma.Curses; -namespace ANSI_Fahrplan; +namespace SCI.CursesWrapper; public class InputHandler { /// @@ -11,35 +10,54 @@ public class InputHandler { /// Event arguments with details like the pressed key code public delegate void KeypressEventHandler (object sender, NCursesKeyPressEventArgs e); - // Event fired when key is pressed + /// + /// Event handlers for NCurses key presses + /// public event KeypressEventHandler? OnKeyPress; - // Similar to OnKeyPress, but called before any regular OnKeyPress event handlers are called + /// + /// Event Handlers called before the regular OnKeyPress handlers are called + /// public event KeypressEventHandler? OnKeyPressPrivileged; - // When this variable is set, no event handlers will be called except this one - // Once e.CancelNextEvent is true from this event, it will be set to null again - private KeypressEventHandler? _rawKeyPressHandler; - public KeypressEventHandler? RawKeyPressHandler { get { return _rawKeyPressHandler; }} - - private nint _targetWindow; - public nint TargetWindow { get { return _targetWindow; }} + /// + /// Gets or sets the window this InputHandler is listening to + /// + private Window? _activeWindow; + public Window? ActiveWindow { + get { + return _activeWindow; + } + set { + _activeWindow = value; + } + } - private CancellationTokenSource? listeningRoutineCts; + /// + /// Root screen to fall back to when no window is active + /// + private nint rootScreen; + /// + /// Task running the endless input handler routine + /// private Task? listeningTask; + /// + /// Cancellation Token Source to stop the listening task + /// + private CancellationTokenSource? listeningRoutineCts; + /// /// Class constructor + /// Parent screen this InputHandler is attached to /// - /// Window for which to listen to key presses - public InputHandler (nint targetWindow) { - _targetWindow = targetWindow; + public InputHandler (nint screen) { + rootScreen = screen; } /// /// Start listening to key presses and call registered callbacks - /// Window to listen to key presses in /// public void StartListening () { if (IsListening ()) return; @@ -62,7 +80,9 @@ public class InputHandler { listeningTask.Wait (); listeningTask = null; - NCurses.Keypad (TargetWindow, false); + if (ActiveWindow is not null) { + NCurses.Keypad (ActiveWindow.WindowId, false); + } } /// @@ -77,43 +97,29 @@ public class InputHandler { } /// - /// Enable temporary raw keypress event handler to forward - /// all keypressed directly to a specific function + /// Prepares the window for proper key press reading /// - /// Event handler to pass key presses to - /// Screen to pass key char duty to - public void EnableRawEventHandler (KeypressEventHandler eventHandler, nint? window = null) { - if (window is not null) { - StopListening (); - _targetWindow = (nint) window; - StartListening (); + 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); } - - _rawKeyPressHandler = eventHandler; } /// - /// Disables the temporary raw keypress event handler - /// Screen to pass key char duty back to + /// Undoes any changes the setup routine configured for the active window /// - public void DisableRawEventHandler (nint? screen = null) { - if (RawKeyPressHandler is null) return; - - if (screen is not null) { - StopListening (); - _targetWindow = (nint) screen; - StartListening (); + 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); } - - _rawKeyPressHandler = null; - } - - /// - /// Prepares the window for proper key press reading - /// - private void WindowSetup () { - NCurses.Keypad (TargetWindow, true); - NCurses.NoDelay (TargetWindow, false); } /// @@ -124,35 +130,38 @@ public class InputHandler { WindowSetup (); + int keyCode = -1; + while (!listeningRoutineCts.Token.IsCancellationRequested) { - int keyCode = NCurses.WindowGetChar (TargetWindow); + if (OnKeyPress is null) { + Console.Title = "No handlers"; + } else { + Console.Title = $"Got {OnKeyPress.GetInvocationList ().Length} handlers"; + } + + if (ActiveWindow is not null) { + keyCode = NCurses.WindowGetChar (ActiveWindow.WindowId); + } else { + keyCode = NCurses.GetChar (); + } // Do nothing until the key code is larger than -1 if (keyCode < 0) continue; - var eventArgs = new NCursesKeyPressEventArgs (keyCode, TargetWindow); - - // If enabled, forward all key pressed directly to raw keypress event handler - if (RawKeyPressHandler is not null) { - RawKeyPressHandler (this, eventArgs); - continue; - } - - var skipRegularEventHandlers = false; - + var eventArgs = new NCursesKeyPressEventArgs (keyCode, ActiveWindow); + // Handle any registered privileged key handlers if (OnKeyPressPrivileged is not null) { foreach (KeypressEventHandler h in OnKeyPressPrivileged.GetInvocationList ()) { h (this, eventArgs); if (eventArgs.CancelNextEvent == true) { - skipRegularEventHandlers = true; break; } } } // Handle any regular registered key handlers - if (OnKeyPress is not null && !skipRegularEventHandlers) { + if (OnKeyPress is not null) { foreach (KeypressEventHandler h in OnKeyPress.GetInvocationList ()) { h (this, eventArgs); if (eventArgs.CancelNextEvent == true) continue ; @@ -169,13 +178,13 @@ public class NCursesKeyPressEventArgs : EventArgs { private int _keyCode; public int KeyCode { get { return _keyCode; }} - private nint _targetWindow; - public nint TargetWindow { get { return _targetWindow; }} + private Window? _sourceWindow; + public Window? SourceWindow { get { return _sourceWindow; }} public bool CancelNextEvent { get; set; } = false; - public NCursesKeyPressEventArgs (int keyCode, nint targetWindow) { + public NCursesKeyPressEventArgs (int keyCode, Window? sourceWindow) { _keyCode = keyCode; - _targetWindow = targetWindow; + _sourceWindow = sourceWindow; } } \ No newline at end of file diff --git a/CursesWrapper/Window.cs b/CursesWrapper/Window.cs new file mode 100644 index 0000000..ecc13d4 --- /dev/null +++ b/CursesWrapper/Window.cs @@ -0,0 +1,265 @@ +using System.Collections.ObjectModel; +using System.Drawing; +using Mindmagma.Curses; + +namespace SCI.CursesWrapper; + +public class Window { + /// + /// Gets or sets the window position on the screen + /// + private Point _position; + public Point Position { + get { + return _position; + } + set { + if (ParentWindow is not null) { + NCurses.WindowMove ( + WindowId, + ParentWindow.Position.Y + value.Y, + ParentWindow.Position.X + value.X + ); + } else { + NCurses.WindowMove ( + WindowId, + value.Y, + value.X + ); + } + _position = value; + } + } + + /// + /// Gets or sets the width and height of the window + /// + private Size _windowSize; + public Size WindowSize { + get { + return _windowSize; + } + set { + _windowSize = value; + } + } + + /// + /// Gets or sets the window background color pair + /// + private uint _backgroundColorId; + public uint BackgroundColorId { + get { + return _backgroundColorId; + } + set { + NCurses.WindowBackground (WindowId, value); + Redraw (); + _backgroundColorId = value; + } + } + + /// + /// Sets the parent window + /// + private Window? _parentWindow; + public Window? ParentWindow { get { return _parentWindow; }} + + /// + /// Holds a list of children windows this window posesses + /// + private List _childWindows = new List (); + public ReadOnlyCollection ChildWindows { get { return _childWindows.AsReadOnly (); }} + + /// + /// Holds the pointer for this window + /// + private nint _windowId; + public nint WindowId { get { return _windowId; }} + + /// + /// Input handler assigned to this window + /// + private InputHandler? inputHandler; + + /// + /// Event handler called when this window is active and a key is pressed + /// + public event InputHandler.KeypressEventHandler? OnKeyPress; + + /// + /// Create new window by specifying X/Y and Width/Height geometry + /// + /// + /// + /// + /// + /// + public Window (int x, int y, int width, int height, Window? parentWindow = null) { + if (parentWindow is not null) { + _windowId = NCurses.DeriveWindow ( + parentWindow.WindowId, + height, width, + y, x + ); + } else { + _windowId = NCurses.NewWindow ( + height, width, + y, x + ); + } + + Redraw (); + } + + /// + /// Create new window by specifying geometry through Point and Size objects + /// + /// + /// + /// + public Window (Point position, Size windowSize, Window? parentWindow = null) { + if (parentWindow is not null) { + _windowId = NCurses.SubWindow ( + parentWindow.WindowId, + windowSize.Height, windowSize.Width, + position.Y, position.X + ); + } else { + _windowId = NCurses.NewWindow ( + windowSize.Height, windowSize.Width, + position.Y, position.X + ); + } + + Redraw (); + } + + /// + /// Adds a child window to this window + /// + /// + public void AddChildWindow (Window child) { + if (_childWindows.Contains (child)) return; + _childWindows.Add (child); + } + + /// + /// Removes a child window from this window + /// + /// + public void RemoveChildWindow (Window child) { + if (!_childWindows.Contains (child)) return; + _childWindows.Remove (child); + } + + /// + /// Discards all optimization options about drawn parts of this window. + /// Call before drawing a sub window + /// + public void TouchWin () { + NCurses.TouchWindow (WindowId); + } + + /// + /// Redraws this window + /// + public void Redraw () { + NCurses.Refresh (); // TODO: Necessary? + + if (ChildWindows.Count > 0) { + foreach (var window in ChildWindows) { + window.Redraw (); + } + } + + if (ParentWindow is not null) ParentWindow.TouchWin (); + NCurses.WindowRefresh (WindowId); + } + + /// + /// Destroys this window and all children windows + /// + public void Destroy () { + if (ChildWindows.Count > 0) { + foreach (var window in _childWindows) { + window.Destroy (); + } + } + + if (inputHandler is not null) inputHandler.ActiveWindow = null; + if (ParentWindow is not null) ParentWindow.RemoveChildWindow (this); + + UnregisterInputHandler (); + + SetBorder (false); + + NCurses.ClearWindow (WindowId); + Console.Title = "About to destroy"; + NCurses.DeleteWindow (WindowId); + Console.Title = "Destroyed"; + + //TODO: Program hangs on DeleteWindow + } + + /// + /// Register an input handler for this window to attach to OnKeyPress events + /// + /// InputHandler to register + public void RegisterInputHandler (InputHandler targetInputHandler) { + if (inputHandler is not null) throw new Exception ( + "Another input handler is already registered" + ); + + inputHandler = targetInputHandler; + inputHandler.OnKeyPress += KeyPressHandler; + } + + /// + /// Detach from all OnKeyPress events and unset input handler + /// + public void UnregisterInputHandler () { + if (inputHandler is null) return; + + inputHandler.OnKeyPress -= KeyPressHandler; + } + + /// + /// Handle key press events from the input handler + /// + /// + /// + private void KeyPressHandler (object sender, NCursesKeyPressEventArgs e) { + if (e.SourceWindow != this) return; + + if (OnKeyPress is not null) { + OnKeyPress (sender, e); + } + } + + /// + /// Tells the input handler this window is active + /// + public void SetWindowActive () { + if (inputHandler is null) return; + + inputHandler.ActiveWindow = this; + } + + /// + /// Enables or disables a border around this window + /// + /// Sets the status of the border + /// If specified, uses this character as the top and bottom border + /// If specified, uses this character as the left and right border + 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, ' ', ' '); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index d8eb55e..90fb10b 100755 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,5 @@ -using ANSI_Fahrplan.Screens; -using ANSI_Fahrplan.UiElements; +using SCI.CursesWrapper; +using ANSI_Fahrplan.Screens; using Mindmagma.Curses; namespace ANSI_Fahrplan; @@ -19,31 +19,60 @@ class Program { * - User can change the list type with function keys **/ - var screen = NCurses.InitScreen (); - NCurses.SetCursor (0); - NCurses.NoEcho (); - - var hasColors = NCurses.HasColors (); - if (hasColors) { - NCurses.StartColor (); - ColorSchemes.InitAll (); - } + var screen = CursesWrapper.InitNCurses (); // -- Screen-wide input handler -- // - var screenInputHandler = new InputHandler (screen); - screenInputHandler.StartListening (); + var inputHandler = new InputHandler (screen); + inputHandler.StartListening (); + + // Register quit key on main screen + inputHandler.OnKeyPress += (object sender, NCursesKeyPressEventArgs e) => { + if (e.SourceWindow is not null) return; + if (e.KeyCode != 'q') return; + + NCurses.EndWin (); + Console.WriteLine ("Bye-bye!"); + Environment.Exit (0); + }; // -- Create menu bar -- // - var topMenu = new TopMenu (screen, CreateMenuItems (screen, screenInputHandler)); + //var topMenu = new TopMenu (screen, CreateMenuItems (screen, screenInputHandler)); // -- Show introduction screen -- // - NCurses.GetMaxYX (screen, out int height, out int width); + // NCurses.GetMaxYX (screen, out int height, out int width); + + // var innerWindow = NCurses.NewWindow (height - 2, width - 4, 1, 2); + // NCurses.Box (innerWindow, (char) 0, (char) 0); + + var introScreen = new IntroScreen (screen); + introScreen.SetBorder (true); + introScreen.Redraw (); + + var parentWindow = new Window (3, 3, 10 * 2, 10) { + BackgroundColorId = ColorSchemes.TextInputField () + }; + parentWindow.SetBorder (true); + parentWindow.Redraw (); - var innerWindow = NCurses.NewWindow (height - 2, width - 4, 1, 2); - NCurses.Box (innerWindow, (char) 0, (char) 0); + var childWindow = new Window (1, 1, 3, 3, parentWindow); + childWindow.SetBorder (true); + childWindow.Redraw (); - //var introScreen = new IntroScreen (innerWindow); - //introScreen.Show (); + inputHandler.OnKeyPress += (object sender, NCursesKeyPressEventArgs e) => { + if (e.SourceWindow is not null) { + NCurses.WindowAddChar (e.SourceWindow.WindowId, e.KeyCode); + return; + } else { + NCurses.AddChar (e.KeyCode); + } + + if (e.KeyCode != 'd') return; + + parentWindow.Destroy (); + }; + + NCurses.AddString ("Done drawing"); + NCurses.Refresh (); // Wait until the input handler routine stops while (true) Thread.Sleep (500); @@ -53,34 +82,34 @@ class Program { Environment.Exit (1); } - private static List CreateMenuItems (nint screen, InputHandler inputHandler) { - var helpItem = new MenuItem ("Help", "F1", inputHandler); - var upcomingItem = new MenuItem ("Upcoming", "F2", inputHandler); - var byDayItem = new MenuItem ("By Day", "F3", inputHandler); - var byRoomItem = new MenuItem ("By Room", "F4", inputHandler); - var bySpeakerItem = new MenuItem ("By Speaker", "F5", inputHandler); - var quitItem = new MenuItem ("Quit", "q", inputHandler); + // private static List CreateMenuItems (nint screen, InputHandler inputHandler) { + // var helpItem = new MenuItem ("Help", "F1", inputHandler); + // var upcomingItem = new MenuItem ("Upcoming", "F2", inputHandler); + // var byDayItem = new MenuItem ("By Day", "F3", inputHandler); + // var byRoomItem = new MenuItem ("By Room", "F4", inputHandler); + // var bySpeakerItem = new MenuItem ("By Speaker", "F5", inputHandler); + // var quitItem = new MenuItem ("Quit", "q", inputHandler); - quitItem.OnItemActivated += (object? sender, EventArgs e) => { - NCurses.EndWin (); - Console.WriteLine ("Bye-bye!"); - Environment.Exit (0); - }; + // quitItem.OnItemActivated += (object? sender, EventArgs e) => { + // NCurses.EndWin (); + // Console.WriteLine ("Bye-bye!"); + // Environment.Exit (0); + // }; - helpItem.OnItemActivated += (object? sender, EventArgs e) => { - InputBox.InputCompleted callback = (string ee) => { - NCurses.MoveAddString (3, 3, $"<{ee}>"); - }; - new InputBox().RequestInput (screen, inputHandler, callback, "Please enter some text now:"); - }; + // helpItem.OnItemActivated += (object? sender, EventArgs e) => { + // InputBox.InputCompleted callback = (string ee) => { + // NCurses.MoveAddString (3, 3, $"<{ee}>"); + // }; + // new InputBox().RequestInput (screen, inputHandler, callback, "Please enter some text now:"); + // }; - return new List { - helpItem, - upcomingItem, - byDayItem, - byRoomItem, - bySpeakerItem, - quitItem - }; - } + // return new List { + // helpItem, + // upcomingItem, + // byDayItem, + // byRoomItem, + // bySpeakerItem, + // quitItem + // }; + // } } diff --git a/Screens/IntroScreen.cs b/Screens/IntroScreen.cs index 7011626..834f672 100644 --- a/Screens/IntroScreen.cs +++ b/Screens/IntroScreen.cs @@ -1,32 +1,28 @@ using Mindmagma.Curses; +using SCI.CursesWrapper; namespace ANSI_Fahrplan.Screens; -public class IntroScreen : Screen { +public class IntroScreen : Window { private nint infoTextBox; /// /// Pass class constructor through to inherited constructor /// - /// Root window to use for this screen - public IntroScreen (nint rootWindow) : base (rootWindow) { } - - public override void Show () { - string asciiArtPlusText = - "Press F1 for a quick start guide or simply press Enter\n" + - "to see upcoming events\n" + - AsciiArt.logo38c3_80x24 + - "\n38C3 Fahrplan in your terminal!"; - - AsciiArt.ShowCentered (RootWindow, asciiArtPlusText); - - //infoTextBox = NCurses.SubWindow (infoTextBox, ); - - NCurses.Refresh (); - NCurses.WindowRefresh (RootWindow); - } - - public override void Hide () { - + /// Parent window of this window object + public IntroScreen (nint screen) : + base ( + 1, 1, + CursesWrapper.GetWidth (screen) - 2, + CursesWrapper.GetHeight (screen) - 2 + ) + { + string asciiArtPlusText = + "Press F1 for a quick start guide or simply press Enter\n" + + "to see upcoming events\n" + + AsciiArt.logo38c3_80x24 + + "\n38C3 Fahrplan in your terminal!"; + + AsciiArt.ShowCentered (WindowId, asciiArtPlusText); } } \ No newline at end of file diff --git a/Screens/ScrollableScreen.cs b/Screens/ScrollableScreen.cs new file mode 100644 index 0000000..23821ef --- /dev/null +++ b/Screens/ScrollableScreen.cs @@ -0,0 +1,8 @@ +namespace ANSI_Fahrplan.Screens; + +public abstract class ScrollableScreen : Screen { + + public ScrollableScreen (nint rootWindow) : base (rootWindow) {} + + +} \ No newline at end of file diff --git a/UiElements/InputBox.cs b/UiElements/InputBox.cs index 56dd7d2..281b811 100644 --- a/UiElements/InputBox.cs +++ b/UiElements/InputBox.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices.Marshalling; +using SCI.CursesWrapper; using Mindmagma.Curses; namespace ANSI_Fahrplan.UiElements; @@ -77,7 +77,7 @@ public class InputBox { NCurses.SetCursor (0); NCurses.NoEcho (); - inputHandler.DisableRawEventHandler (screen); + //inputHandler.DisableRawEventHandler (screen); NCurses.MoveWindow (screen, 0, 0); NCurses.DeleteWindow (inputFieldWin); @@ -93,6 +93,6 @@ public class InputBox { NCurses.MoveWindow (inputFieldWin, inputFieldWinY, inputFieldWinX); }; - inputHandler.EnableRawEventHandler (handlerFunction, inputFieldWin); + //inputHandler.EnableRawEventHandler (handlerFunction, inputFieldWin); } } \ No newline at end of file diff --git a/UiElements/MenuItem.cs b/UiElements/MenuItem.cs index 04ed393..2e7af1b 100644 --- a/UiElements/MenuItem.cs +++ b/UiElements/MenuItem.cs @@ -1,3 +1,4 @@ +using SCI.CursesWrapper; using Mindmagma.Curses; namespace ANSI_Fahrplan.UiElements; diff --git a/UiElements/TopMenu.cs b/UiElements/TopMenu.cs index 9f4aee3..5e72440 100644 --- a/UiElements/TopMenu.cs +++ b/UiElements/TopMenu.cs @@ -1,3 +1,4 @@ +using SCI.CursesWrapper; using Mindmagma.Curses; namespace ANSI_Fahrplan.UiElements;