From d0853e23bcc3940440dd1e490f752cc0fd1c3500 Mon Sep 17 00:00:00 2001 From: resneptacle Date: Tue, 24 Dec 2024 19:06:31 +0100 Subject: [PATCH] yet more changes --- AsciiArt/Generator.cs | 39 ++++++++++++ CursesWrapper/ColorSchemes.cs | 4 ++ CursesWrapper/ContentWindow.cs | 37 +++++++++++ CursesWrapper/InnerWindow.cs | 12 ---- CursesWrapper/ScrollWindow.cs | 109 +++++++++++++++++++++++++++++++++ CursesWrapper/Window.cs | 68 +++++++++++++++++--- Program.cs | 34 ++++++---- Screens/IntroScreen.cs | 10 ++- Screens/ScrollableScreen.cs | 8 --- UiElements/UiElement.cs | 2 +- ansifahrplan.csproj | 3 + res/38c3.jpg | Bin 0 -> 16437 bytes 12 files changed, 284 insertions(+), 42 deletions(-) create mode 100644 AsciiArt/Generator.cs create mode 100644 CursesWrapper/ContentWindow.cs delete mode 100644 CursesWrapper/InnerWindow.cs create mode 100644 CursesWrapper/ScrollWindow.cs delete mode 100644 Screens/ScrollableScreen.cs create mode 100644 res/38c3.jpg 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 0000000000000000000000000000000000000000..94fcecbbb382f8bbaaeea8b33d562e87b81c9941 GIT binary patch literal 16437 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