You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

250 lines
7.8 KiB

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<string> _innerContent = new List<string> ();
public ReadOnlyCollection<string> InnerContent { get { return _innerContent.AsReadOnly (); }}
public ScrollWindowOverflowAction OverflowAction = ScrollWindowOverflowAction.ResizeScrollArea;
/// <summary>
/// Create new window by specifying X/Y and Width/Height geometry
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="targetInputHandler"></param>
/// <param name="parentWindow"></param>
public ScrollWindow (int x, int y, int width, int height, InputHandler targetInputHandler, Window? parentWindow = null) :
base (x, y, width, height, targetInputHandler, parentWindow)
{
_InitializeScrollingWindow ();
}
/// <summary>
/// Create new scrolling window by specifying geometry through Point and Size objects
/// </summary>
/// <param name="position"></param>
/// <param name="windowSize"></param>
/// <param name="targetInputHandler"></param>
/// <param name="parentWindow"></param>
public ScrollWindow (Point position, Size windowSize, InputHandler targetInputHandler, Window? parentWindow = null) :
base (position, windowSize, targetInputHandler, parentWindow)
{
_InitializeScrollingWindow ();
}
/// <summary>
/// Run initialization code upon object creation
/// </summary>
private void _InitializeScrollingWindow () {
_scrollAreaSize = new Size (
WindowSize.Width,
WindowSize.Height
);
OnKeyPress += ScrollKeyHandler;
}
/// <summary>
/// Scrolls the window up or down on up key or down key
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
/// <summary>
/// Scrolls the pad to the specified coordinates
/// </summary>
/// <param name="scrollX"></param>
/// <param name="scrollY"></param>
public void ScrollTo (int scrollX, int scrollY) {
_scrollPosition = new Point (scrollX, scrollY);
RenderCurrentViewport ();
}
/// <summary>
/// Adds text content to this window
/// </summary>
/// <param name="contentStr">Block of text to add to this window</param>
public void AddContent (string contentStr) {
var lines = contentStr.Split ('\n', StringSplitOptions.RemoveEmptyEntries);
AddContent (lines);
}
/// <summary>
/// Adds text content to this window
/// </summary>
/// <param name="lines">Lines of text to add to this window</param>
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;
}
/// <summary>
/// Updates the currently visible part of the inner content
/// </summary>
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 ();
}
}
/// <summary>
/// Defines how to deal with lines to long for a scrolling window
/// </summary>
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
}