using System.Collections.ObjectModel; using System.Drawing; using Mindmagma.Curses; namespace SCI.CursesWrapper; public class ScrollWindow : Window { private Point _scrollPosition; public Point ScrollPosition { get { return _scrollPosition; }} private Size _scrollAreaSize; public Size ScrollAreaSize { get { return _scrollAreaSize; }} private List _innerContent = new List (); public ReadOnlyCollection InnerContent { get { return _innerContent.AsReadOnly (); }} public ScrollWindowOverflowAction OverflowAction = ScrollWindowOverflowAction.ResizeScrollArea; /// /// 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 (); } /// /// Run initialization code upon object creation /// private void _InitializeScrollingWindow () { _scrollAreaSize = new Size ( WindowSize.Width, WindowSize.Height ); 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); } 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); } } /// /// Scrolls the pad to the specified coordinates /// /// /// public void ScrollTo (int scrollX, int scrollY) { _scrollPosition = new Point (scrollX, scrollY); RenderCurrentViewport (); } /// /// Adds text content to this window /// /// Block of text to add to this window public void AddContent (string contentStr) { var lines = contentStr.Split ('\n', StringSplitOptions.RemoveEmptyEntries); AddContent (lines); } /// /// 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); } _scrollAreaSize.Height = _innerContent.Count; } /// /// 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 (visibleArea.Y < 0) { visibleArea.Y = 0; _scrollPosition.Y = 0; } // 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 (); } } /// /// 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 }