diff --git a/AsciiArt/Generator.cs b/AsciiArt/Generator.cs new file mode 100644 index 0000000..0619eb4 --- /dev/null +++ b/AsciiArt/Generator.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; + +namespace SCI.AsciiArt; + +public class Generator { + public static async Task FromImage (string imagePath, int width) { + // Ensure image exists + if (!File.Exists (imagePath)) { + throw new FileNotFoundException ($"The specified file '{imagePath}' does not exist"); + } + + var process = new Process (); + process.StartInfo = new ProcessStartInfo () { + FileName = "jp2a", + Arguments = $"--width={width} \"{imagePath}\"", + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + UseShellExecute = false + }; + + process.Start (); + + // Wait up to one second for the task to finish + try { + var cancellationTokenSource = new CancellationTokenSource (1000); + await process.WaitForExitAsync (cancellationTokenSource.Token); + } catch (TaskCanceledException) { + throw new TimeoutException ("jp2a did not finish after 1000ms, process was killed"); + } + + if (process.ExitCode != 0) { + throw new Exception ($"jp2a returned with exit code {process.ExitCode}.\n{process.StandardError.ReadToEnd ()}"); + } + + return await process.StandardOutput.ReadToEndAsync (); + } +} \ No newline at end of file diff --git a/CursesWrapper/ColorSchemes.cs b/CursesWrapper/ColorSchemes.cs index 1d7f8ff..acb2269 100644 --- a/CursesWrapper/ColorSchemes.cs +++ b/CursesWrapper/ColorSchemes.cs @@ -32,4 +32,8 @@ public class ColorSchemes { public static uint TextInputField () { return NCurses.ColorPair (4); } + + public static uint CustomColorTest () { + return NCurses.ColorPair (21); + } } \ No newline at end of file diff --git a/CursesWrapper/ContentWindow.cs b/CursesWrapper/ContentWindow.cs new file mode 100644 index 0000000..72e5e8a --- /dev/null +++ b/CursesWrapper/ContentWindow.cs @@ -0,0 +1,37 @@ +using Mindmagma.Curses; + +namespace SCI.CursesWrapper; + +public class ContentWindow : Window { + public ContentWindow (nint rootScreen, InputHandler inputHandler) : + base ( + 0, 1, + CursesWrapper.GetWidth (rootScreen), + CursesWrapper.GetHeight (rootScreen) - 2, + inputHandler + ) + { } + + /// + /// Creates a new nested child window without inner padding + /// + public Window CreateInnerWindow () { + return new Window (-1, -1, -2, -2, this) { + BorderEnabled = false + }; + } + + /// + /// Removes all children elements and redraws the window + /// + public void Clean () { + foreach (var window in ChildWindows.ToList ()) { + window.Destroy (true); + } + + // Clear window and redraw + NCurses.ClearWindow (WindowId); + SetBorder (true); + Draw (); + } +} \ No newline at end of file diff --git a/CursesWrapper/InnerWindow.cs b/CursesWrapper/InnerWindow.cs deleted file mode 100644 index 20a09b6..0000000 --- a/CursesWrapper/InnerWindow.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SCI.CursesWrapper; - -public class InnerWindow : Window { - public InnerWindow (nint rootScreen, InputHandler inputHandler) : - base ( - 0, 1, - CursesWrapper.GetWidth (rootScreen), - CursesWrapper.GetHeight (rootScreen) - 2, - inputHandler - ) - { } -} \ No newline at end of file diff --git a/CursesWrapper/ScrollWindow.cs b/CursesWrapper/ScrollWindow.cs new file mode 100644 index 0000000..da33940 --- /dev/null +++ b/CursesWrapper/ScrollWindow.cs @@ -0,0 +1,109 @@ + +using System.Drawing; +using Mindmagma.Curses; + +namespace SCI.CursesWrapper; + +public class ScrollWindow : Window { + private nint _innerPadId; + public nint InnerPadId { get { return _innerPadId; }} + + private Point _scrollPosition; + public Point ScrollPosition { get { return _scrollPosition; }} + + private Size _padSize; + public Size PadSize { get { return _padSize; }} + + public ScrollWindow (Window parentWindow, InputHandler inputHandler, int innerHeight = -1) : + base (0, 0, 0, 0, inputHandler, parentWindow) + { + var padWidth = GetUsableWidth (); + var padHeight = innerHeight; + + if (padHeight == -1) { + padHeight = parentWindow.GetUsableHeight (); + } + + _innerPadId = NCurses.NewPad (padHeight, padWidth); + + _padSize = new Size (padWidth, padHeight); + + NCurses.WindowBackground (InnerPadId, ColorSchemes.TextInputField ()); + ShowPad (); + + OnKeyPress += ScrollKeyHandler; + } + + /// + /// Scrolls the window up or down on up key or down key + /// + /// + /// + private void ScrollKeyHandler (object sender, NCursesKeyPressEventArgs e) { + if (e.SourceWindow is null || e.SourceWindow != this) return; + + if (e.KeyCode == CursesKey.UP) { + ScrollTo (ScrollPosition.X, ScrollPosition.Y - 1); + } else if (e.KeyCode == CursesKey.DOWN) { + ScrollTo (ScrollPosition.X, ScrollPosition.Y + 1); + } + } + + /// + /// Scrolls the pad to the specified coordinates + /// + /// + /// + public void ScrollTo (int scrollX, int scrollY) { + if (ParentWindow is null) return; + + _scrollPosition = new Point (scrollX, scrollY); + ShowPad (); + } + + /// + /// Shows the pad the current scroll location + /// + public void ShowPad () { + if (ParentWindow is null) return; + + var scrollToX = ScrollPosition.X; + var scrollToY = ScrollPosition.Y; + + var viewableWidth = GetUsableWidth (); + var viewableHeight = GetUsableHeight (); + + // Check for unnecessary scrolling due to small Pad size + if (PadSize.Width <= viewableWidth) { + scrollToX = 0; + } + if (PadSize.Height <= viewableHeight) { + scrollToY = 0; + } + + // Check values for upper bounds + if (scrollToX > PadSize.Width - viewableWidth) scrollToX = PadSize.Width - viewableWidth; + if (scrollToY > PadSize.Height - viewableHeight) scrollToY = PadSize.Height - viewableHeight; + + // Check values for lower bounds + if (scrollToX < 0) scrollToX = 0; + if (scrollToY < 0) scrollToY = 0; + + // Store potentially updated scroll coordinates + _scrollPosition = new Point (scrollToX, scrollToY); + + var fromY = Position.Y; + var fromX = Position.X; + var toY = viewableHeight; + var toX = viewableWidth - 2; // TODO: Why the two? How is the geometry this wong?! + + NCurses.PadRefresh ( + _innerPadId, + scrollToY, scrollToX, + fromY, + fromX, + toY, + toX + ); + } +} \ No newline at end of file diff --git a/CursesWrapper/Window.cs b/CursesWrapper/Window.cs index e1699fa..655bbae 100644 --- a/CursesWrapper/Window.cs +++ b/CursesWrapper/Window.cs @@ -105,6 +105,13 @@ public class Window { /// public event InputHandler.KeypressEventHandler? OnKeyPress; + /// + /// Implicit conversion of a Window object to nint representing + /// the window id for use directly with NCurses methods + /// + /// Window object + public static implicit operator nint (Window w) => w.WindowId; + /// /// Create new window by specifying X/Y and Width/Height geometry /// @@ -163,13 +170,25 @@ public class Window { /// private void _Initialize (Point position, Size windowSize, Window? parentWindow = null) { if (parentWindow is not null) { - int addedPadding = parentWindow.BorderEnabled? - InnerPadding + 1 : 0; + position.X = parentWindow.GetInnerOriginX () + position.X; + position.Y = parentWindow.GetInnerOriginY () + position.Y; + + // If windowSize.Width is 0 or smaller, make the width relative + // to the total width of the window, 0 making it as large as possible + windowSize.Width = (windowSize.Width <= 0)? + parentWindow.GetUsableWidth () - windowSize.Width: + windowSize.Width; + + // If windowSize.Height is 0 or smaller, make the height relative + // to the total height of the window, 0 making it as large as possible + windowSize.Height = (windowSize.Height <= 0)? + parentWindow.GetUsableHeight () - windowSize.Height: + windowSize.Height; _windowId = NCurses.DeriveWindow ( parentWindow.WindowId, windowSize.Height, windowSize.Width, - position.Y + addedPadding, position.X + addedPadding + position.Y, position.X ); parentWindow.AddChildWindow (this); @@ -186,6 +205,7 @@ public class Window { NCurses.Keypad (WindowId, true); + Clear (); Draw (); } @@ -235,15 +255,24 @@ public class Window { NCurses.WindowRefresh (WindowId); } + /// + /// Clears the window with the currently set background color + /// + public void Clear () { + NCurses.ClearWindow (this); + Draw (); + } + /// /// Destroys this window and all children windows + /// Skips redrawing the windows parent if true /// - public void Destroy () { + public void Destroy (bool skipParentRedrawing = false) { // Catch double destroy calls if (WindowId == -1) throw new Exception ("Destroy called twice on object"); foreach (var window in _childWindows.ToList ()) { - window.Destroy (); + window.Destroy (true); } UnregisterInputHandler (); @@ -262,7 +291,7 @@ public class Window { // Ensures sure the parent is updated too if (ParentWindow is not null) { ParentWindow.RemoveChildWindow (this); - ParentWindow.Draw (); + if (!skipParentRedrawing) ParentWindow.Draw (); } } @@ -335,7 +364,8 @@ public class Window { /// Usable inner width of window in columns public int GetUsableWidth () { if (BorderEnabled) { - return WindowSize.Width - 1 - InnerPadding; + // Total window with - (1col Border Left and Right) - (InnerPadding Left and Right) + return WindowSize.Width - 2 - (InnerPadding * 2); } else { return WindowSize.Width; } @@ -347,9 +377,31 @@ public class Window { /// Usable inner height of window in rows public int GetUsableHeight () { if (BorderEnabled) { - return WindowSize.Height - 1 - InnerPadding; + // Total window with - (1col Border Top and Bottom) - (InnerPadding Top and Bottom) + return WindowSize.Height - 2 - (InnerPadding * 2); } else { return WindowSize.Height; } } + + /// + /// Calculates the first usable X coordinate inside the window + /// + /// Inner Origin X coordinate of window + public int GetInnerOriginX () { + if (BorderEnabled) { + return 1 + InnerPadding; + } else { + return 0; + } + } + + /// + /// Calculates the first usable Y coordinate inside the window + /// + /// Inner Origin Y coordinate of window + public int GetInnerOriginY () { + // Window borders and padding are symmetrical for now, reuse X function + return GetInnerOriginX (); + } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index b97350c..157dad4 100755 --- a/Program.cs +++ b/Program.cs @@ -2,6 +2,8 @@ using SCI.CursesWrapper.UiElements; using ANSI_Fahrplan.Screens; using Mindmagma.Curses; +using System.Reflection; +using Microsoft.Win32.SafeHandles; namespace ANSI_Fahrplan; @@ -19,8 +21,7 @@ class Program { **/ var screen = CursesWrapper.InitNCurses (); - //var topLevelWindows = new List (); - + // -- Screen-wide input handler -- // var inputHandler = new InputHandler (); inputHandler.StartListening (); @@ -34,16 +35,17 @@ class Program { Environment.Exit (0); }; - // -- Inner Window -- // + // -- Content Window -- // // // This window contains all the dynamic content between // the menu and status bars - var innerWindow = new InnerWindow (screen, inputHandler) { + var contentWindow = new ContentWindow (screen, inputHandler) { BorderEnabled = true }; // -- Intro screen -- // - var introScreen = new IntroScreen (innerWindow); + var introScreen = new IntroScreen (contentWindow); + introScreen.Draw (); // Close intro screen on any keypress introScreen.OnKeyPress += (object sender, NCursesKeyPressEventArgs e) => { @@ -56,10 +58,14 @@ class Program { // Wait until intro screen is closed while (introScreen.WindowId > -1) Thread.Sleep (50); // TODO; Unjank this - - // -- Create menu bar -- // - var topMenu = new TopMenu (screen, inputHandler, CreateMenuItems (innerWindow)); + var topMenu = new TopMenu (screen, inputHandler, CreateMenuItems (contentWindow)); + + // Testing + var testwin = contentWindow.CreateInnerWindow (); + testwin.BackgroundColorId = ColorSchemes.TextInputField (); + NCurses.WindowAddString (testwin, "Hello World."); + contentWindow.Draw (); // Wait until the input handler routine stops while (inputHandler.IsListening ()) Thread.Sleep (50); @@ -69,17 +75,23 @@ class Program { Environment.Exit (1); } - private static List CreateMenuItems (InnerWindow innerWindow) { + private static List CreateMenuItems (ContentWindow contentWindow) { var helpItem = new MenuItem ("Help", "F1"); var upcomingItem = new MenuItem ("Upcoming", "b"); //F2 var byDayItem = new MenuItem ("By Day", "n"); // F3 var byRoomItem = new MenuItem ("By Room", "F4"); var bySpeakerItem = new MenuItem ("By Speaker", "F5"); - var byTestItem = new MenuItem ("By Test", "F7"); var quitItem = new MenuItem ("Quit (C-q)"); helpItem.OnItemActivated += (object sender, MenuItemActivatedEventArgs e) => { - + contentWindow.Clean (); + if (contentWindow.TargetInputHandler is null) return; + + var scrollWindow = new ScrollWindow (contentWindow, contentWindow.TargetInputHandler, 30); + for (int i = 0; i < scrollWindow.PadSize.Height - 1; i++) + NCurses.WindowAddString (scrollWindow.InnerPadId, $"Line {i}\n"); + NCurses.WindowAddString (scrollWindow.InnerPadId, $"Line {scrollWindow.PadSize.Height}"); + scrollWindow.ShowPad (); }; return new List { diff --git a/Screens/IntroScreen.cs b/Screens/IntroScreen.cs index fdb00be..c157085 100644 --- a/Screens/IntroScreen.cs +++ b/Screens/IntroScreen.cs @@ -1,3 +1,4 @@ +using Mindmagma.Curses; using SCI.CursesWrapper; namespace ANSI_Fahrplan.Screens; @@ -15,11 +16,16 @@ public class IntroScreen : Window { parentWindow ) { + string asciiArt = SCI.AsciiArt.Generator.FromImage ( + "res" + Path.DirectorySeparatorChar + "38c3.jpg", + GetUsableWidth () + ).Result; + 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 + + "38C3 Fahrplan in your terminal!"; AsciiArt.ShowCentered (WindowId, asciiArtPlusText); } diff --git a/Screens/ScrollableScreen.cs b/Screens/ScrollableScreen.cs deleted file mode 100644 index 0350210..0000000 --- a/Screens/ScrollableScreen.cs +++ /dev/null @@ -1,8 +0,0 @@ -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/UiElement.cs b/UiElements/UiElement.cs index 98aeb5d..bb22ece 100644 --- a/UiElements/UiElement.cs +++ b/UiElements/UiElement.cs @@ -4,7 +4,7 @@ public abstract class UiElement { private nint _screen; public nint Screen { get { return _screen; }} - protected nint innerWindow { get; set; } + protected nint contentWindow { get; set; } public UiElement (nint screen) { _screen = screen; diff --git a/ansifahrplan.csproj b/ansifahrplan.csproj index 0271387..6a36de7 100755 --- a/ansifahrplan.csproj +++ b/ansifahrplan.csproj @@ -11,6 +11,9 @@ + + PreserveNewest + diff --git a/res/38c3.jpg b/res/38c3.jpg new file mode 100644 index 0000000..94fcecb Binary files /dev/null and b/res/38c3.jpg differ