diff --git a/AsciiArt/AsciiArt.cs b/AsciiArt/AsciiArt.cs index 84f8315..12460b6 100644 --- a/AsciiArt/AsciiArt.cs +++ b/AsciiArt/AsciiArt.cs @@ -72,7 +72,7 @@ public class AsciiArt { public static string lorem_ipsum { get { return @" - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer euismod in sapien id rhoncus. Suspendisse potenti. Nam bibendum risus lobortis mollis commodo. Donec ac ante nec augue vestibulum consequat. In condimentum id nisl sed placerat. Suspendisse rhoncus lectus a risus sollicitudin accumsan. Quisque semper dui a erat aliquet, ac venenatis felis blandit. In id eros quis tellus condimentum luctus et sit amet enim. Aliquam malesuada erat augue, in euismod libero rhoncus quis. Nam augue sapien, pellentesque quis congue ac, semper nec nisl. Donec accumsan vitae justo ac volutpat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus in dictum ipsum. Nunc venenatis egestas metus id maximus. Aliquam erat volutpat. +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer euismod in sapien id rhoncus. Suspendisse potenti. Nam bibendum risus lobortis mollis commodo. Donec ac ante nec augue vestibulum consequat. In condimentum id nisl sed placerat. Suspendisse rhoncus lectus a risus sollicitudin accumsan. Quisque semper dui a erat aliquet, ac venenatis felis blandit. In id eros quis tellus condimentum luctus et sit amet enim. Aliquam malesuada erat augue, in euismod libero rhoncus quis. Nam augue sapien, pellentesque quis congue ac, semper nec nisl. Donec accumsan vitae justo ac volutpat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus in dictum ipsum. Nunc venenatis egestas metus id maximus. Aliquam erat volutpat. Fusce vitae tortor a tortor hendrerit condimentum quis non libero. Proin eget dignissim neque. Aliquam ac justo tempus, semper diam eget, finibus eros. Nullam elementum odio ante, eu volutpat sapien aliquet quis. Nulla eget volutpat lorem. Sed feugiat est vel tellus sodales, ac mollis libero faucibus. Vestibulum auctor dapibus quam, eu efficitur dolor feugiat id. Vestibulum tincidunt maximus arcu. Integer a urna posuere sapien efficitur tincidunt ut nec nibh. Etiam eu ultrices metus, in pretium leo. Duis egestas arcu quis tristique pretium. Maecenas dapibus enim vel quam malesuada elementum. Fusce vulputate mollis fringilla. Nullam fermentum magna velit, ac pharetra ante viverra ac. diff --git a/CursesWrapper/ColorSchemes.cs b/CursesWrapper/ColorSchemes.cs index acb2269..8689d4a 100644 --- a/CursesWrapper/ColorSchemes.cs +++ b/CursesWrapper/ColorSchemes.cs @@ -15,6 +15,12 @@ public class ColorSchemes { // TextInputField NCurses.InitPair (4, CursesColor.BLACK, CursesColor.WHITE); + + // CustomColorTest + NCurses.InitPair (5, CursesColor.WHITE, CursesColor.RED); + + // CustomColorTest2 + NCurses.InitPair (6, CursesColor.BLUE, CursesColor.YELLOW); } public static uint Default () { @@ -34,6 +40,10 @@ public class ColorSchemes { } public static uint CustomColorTest () { - return NCurses.ColorPair (21); + return NCurses.ColorPair (5); + } + + public static uint CustomColorTest2 () { + return NCurses.ColorPair (6); } } \ No newline at end of file diff --git a/CursesWrapper/ContentWindow.cs b/CursesWrapper/ContentWindow.cs index 72e5e8a..264f2fe 100644 --- a/CursesWrapper/ContentWindow.cs +++ b/CursesWrapper/ContentWindow.cs @@ -13,10 +13,36 @@ public class ContentWindow : Window { { } /// - /// Creates a new nested child window without inner padding + /// Removes current children and creates a new + /// nested child window without inner padding /// + /// New child window public Window CreateInnerWindow () { - return new Window (-1, -1, -2, -2, this) { + Clean (); + + if (TargetInputHandler is not null) { + return new Window (-1, -1, -2, -2, TargetInputHandler, this) { + BorderEnabled = false + }; + } else { + return new Window (-1, -1, -2, -2, this) { + BorderEnabled = false + }; + } + } + + /// + /// Removes current children and creates a new + /// nested scrollable child window without inner padding + /// + /// New scrollable child window + /// Throws when TargetInputHandler is null + public ScrollWindow CreateInnerScrollWindow () { + if (TargetInputHandler is null) throw new NullReferenceException ( + "Input handler of Content Window cannot be null when creating child ScrollWindow" + ); + + return new ScrollWindow (-1, -1, -2, -2, TargetInputHandler, this) { BorderEnabled = false }; } diff --git a/CursesWrapper/ScrollWindow.cs b/CursesWrapper/ScrollWindow.cs index da33940..f66613b 100644 --- a/CursesWrapper/ScrollWindow.cs +++ b/CursesWrapper/ScrollWindow.cs @@ -1,35 +1,58 @@ +using System.Collections.ObjectModel; 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; }} + private Size _scrollAreaSize; + public Size ScrollAreaSize { get { return _scrollAreaSize; }} - 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 (); - } + private List _innerContent = new List (); + public ReadOnlyCollection InnerContent { get { return _innerContent.AsReadOnly (); }} - _innerPadId = NCurses.NewPad (padHeight, padWidth); + public ScrollWindowOverflowAction OverflowAction = ScrollWindowOverflowAction.ResizeScrollArea; - _padSize = new Size (padWidth, padHeight); + /// + /// Create new window by specifying X/Y and Width/Height geometry + /// + /// + /// + /// + /// + /// + /// + public ScrollWindow (int x, int y, int width, int height, InputHandler targetInputHandler, Window? parentWindow = null) : + base (x, y, width, height, targetInputHandler, parentWindow) + { + _InitializeScrollingWindow (); + } + + /// + /// Create new scrolling window by specifying geometry through Point and Size objects + /// + /// + /// + /// + /// + public ScrollWindow (Point position, Size windowSize, InputHandler targetInputHandler, Window? parentWindow = null) : + base (position, windowSize, targetInputHandler, parentWindow) + { + _InitializeScrollingWindow (); + } - NCurses.WindowBackground (InnerPadId, ColorSchemes.TextInputField ()); - ShowPad (); + /// + /// Run initialization code upon object creation + /// + private void _InitializeScrollingWindow () { + _scrollAreaSize = new Size ( + WindowSize.Width, + WindowSize.Height + ); OnKeyPress += ScrollKeyHandler; } @@ -46,6 +69,18 @@ public class ScrollWindow : Window { ScrollTo (ScrollPosition.X, ScrollPosition.Y - 1); } else if (e.KeyCode == CursesKey.DOWN) { ScrollTo (ScrollPosition.X, ScrollPosition.Y + 1); + } else if (e.KeyCode == CursesKey.LEFT) { + ScrollTo (ScrollPosition.X - 1, ScrollPosition.Y); + } else if (e.KeyCode == CursesKey.RIGHT) { + ScrollTo (ScrollPosition.X + 1, ScrollPosition.Y); + } else if (e.KeyCode == 339) { // Page Up + ScrollTo (ScrollPosition.X, ScrollPosition.Y - WindowSize.Height); + } else if (e.KeyCode == 338) { // Page Down + ScrollTo (ScrollPosition.X, ScrollPosition.Y + WindowSize.Height); + } else if (e.KeyCode == CursesKey.HOME) { + ScrollTo (0, ScrollPosition.Y); + } else if (e.KeyCode == CursesKey.END) { + ScrollTo (ScrollAreaSize.Width, ScrollPosition.Y); } } @@ -55,55 +90,160 @@ public class ScrollWindow : Window { /// /// public void ScrollTo (int scrollX, int scrollY) { - if (ParentWindow is null) return; - _scrollPosition = new Point (scrollX, scrollY); - ShowPad (); + RenderCurrentViewport (); } /// - /// Shows the pad the current scroll location + /// Adds text content to this window /// - public void ShowPad () { - if (ParentWindow is null) return; + /// Block of text to add to this window + public void AddContent (string contentStr) { + var lines = contentStr.Split ('\n', StringSplitOptions.RemoveEmptyEntries); + AddContent (lines); + } - var scrollToX = ScrollPosition.X; - var scrollToY = ScrollPosition.Y; + /// + /// Adds text content to this window + /// + /// Lines of text to add to this window + public void AddContent (string[] lines) { + // TODO: Iterate over every line and turn \n into an actual new line + // TODO: When iterating over every line, do overflow checks + + foreach (var _line in lines) { + // Necessary to make the current line editable + var line = _line; + + // Remove trailing new line character + if (line.EndsWith ('\n')) + line = line.Substring (0, line.Length - 1); + + // Turn every new line character into an actual new content line + var sublines = line.Split ('\n'); + + // If there are new line splits in the current line, add those + // by calling this function recusively on those lines again + if (sublines.Length > 1) { + AddContent (sublines); + continue; + } + + // Do overflow action if text is overflowing + if (line.Length > ScrollAreaSize.Width) { + switch (OverflowAction) { + case ScrollWindowOverflowAction.CutOff: + line = line.Substring (0, WindowSize.Width); + break; + case ScrollWindowOverflowAction.ResizeScrollArea: + _scrollAreaSize.Width = line.Length; + break; + case ScrollWindowOverflowAction.ThrowException: + throw new OverflowException ("Line too long for scrollable window area"); + } + } + + // Finally, add string to content string list + _innerContent.Add (line); + } - var viewableWidth = GetUsableWidth (); - var viewableHeight = GetUsableHeight (); + _scrollAreaSize.Height = _innerContent.Count; + } - // Check for unnecessary scrolling due to small Pad size - if (PadSize.Width <= viewableWidth) { - scrollToX = 0; + /// + /// Updates the currently visible part of the inner content + /// + public void RenderCurrentViewport () { + var visibleArea = new Rectangle ( + ScrollPosition.X, + ScrollPosition.Y, + WindowSize.Width, + WindowSize.Height + ); + + // Check top/left bounds of scroll area + if (visibleArea.X < 0) { + visibleArea.X = 0; + _scrollPosition.X = 0; } - if (PadSize.Height <= viewableHeight) { - scrollToY = 0; + if (visibleArea.Y < 0) { + visibleArea.Y = 0; + _scrollPosition.Y = 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 - ); + // Check bottom/right bounds of scroll area + if (visibleArea.Right > ScrollAreaSize.Width) { + visibleArea.X = ScrollAreaSize.Width - visibleArea.Width; + _scrollPosition.X = visibleArea.X; + } + + if (visibleArea.Bottom > ScrollAreaSize.Height) { + visibleArea.Y = ScrollAreaSize.Height - visibleArea.Height; + _scrollPosition.Y = visibleArea.Y; + } + + // Do not scroll if the virtual scroll area is smaller or + // the same size of the actual window + if (ScrollAreaSize.Width <= visibleArea.Width) { + visibleArea.X = 0; + _scrollPosition.X = 0; + } + if (ScrollAreaSize.Height <= visibleArea.Height) { + visibleArea.Y = 0; + _scrollPosition.Y = 0; + } + + // Clear inner window + Clear (); + + var textToAdd = ""; + + for (int linePos = visibleArea.Y; linePos < visibleArea.Bottom; linePos++) { + if (linePos > _innerContent.Count - 1) break; + var line = _innerContent [linePos]; + + // Cut out only the visible part of a line + if (line.Length < visibleArea.X) { + line = ""; + } else if (line.Length >= visibleArea.Right) { + line = line.Substring (visibleArea.X, visibleArea.Width); + } else { + line = line.Substring (visibleArea.X); + } + + // Last line on the viewport + if (linePos >= visibleArea.Bottom - 1) { + // On the last line, remove the last character if the line is as long as the window. + // NCurses throws an error on the last character due to historical reasons, + // See: https://stackoverflow.com/q/10877469 + // I accept the data loss at this point because the only "proper" solution + // by pushing characters around does not work as the C# NCurses wrapper + // used in this project lacks the insch function and just catching the Exception + // makes the whole screen flicker with each scroll + if (line.Length >= visibleArea.Width) { + line = line.Substring (0, line.Length - 1); + } + } else if (line.Length < visibleArea.Width) { + // Add new line to the end of a cut-off line that is smaller than the window + line = $"{line}\n"; + } + + textToAdd += line; + } + + try { + NCurses.WindowAddString (this, textToAdd); + } catch (Exception) {} + Draw (); } -} \ No newline at end of file +} + +/// +/// Defines how to deal with lines to long for a scrolling window +/// +public enum ScrollWindowOverflowAction { + OverflowNewLine, // Let the content overflow onto the next line + CutOff, // Cut the overflowing text off at the border + ResizeScrollArea, // Resizes the virtual scroll area (default) + ThrowException // Throw a warning +} diff --git a/CursesWrapper/Window.cs b/CursesWrapper/Window.cs index 655bbae..e59a3ed 100644 --- a/CursesWrapper/Window.cs +++ b/CursesWrapper/Window.cs @@ -260,6 +260,7 @@ public class Window { /// public void Clear () { NCurses.ClearWindow (this); + NCurses.MoveWindowAddString (this, GetInnerOriginY (), GetInnerOriginY (), ""); Draw (); } diff --git a/Program.cs b/Program.cs index 157dad4..610231b 100755 --- a/Program.cs +++ b/Program.cs @@ -2,8 +2,6 @@ using SCI.CursesWrapper.UiElements; using ANSI_Fahrplan.Screens; using Mindmagma.Curses; -using System.Reflection; -using Microsoft.Win32.SafeHandles; namespace ANSI_Fahrplan; @@ -77,21 +75,23 @@ class Program { 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 upcomingItem = new MenuItem ("Upcoming", "F2"); + var byDayItem = new MenuItem ("By Day", "F3"); var byRoomItem = new MenuItem ("By Room", "F4"); var bySpeakerItem = new MenuItem ("By Speaker", "F5"); 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 (); + var scrollWindow = contentWindow.CreateInnerScrollWindow (); + for (int i = 0; i <= 100; i++) + scrollWindow.AddContent ($"Line {i} {AsciiArt.lorem_ipsum}\n"); + scrollWindow.RenderCurrentViewport (); + }; + + upcomingItem.OnItemActivated += (object sender, MenuItemActivatedEventArgs e) => { + var scrollWindow = contentWindow.CreateInnerScrollWindow (); + scrollWindow.AddContent ("-- Upcoming --"); + scrollWindow.RenderCurrentViewport (); }; return new List {