|
|
|
@ -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;
|
|
|
|
|
private List<string> _innerContent = new List<string> ();
|
|
|
|
|
public ReadOnlyCollection<string> InnerContent { get { return _innerContent.AsReadOnly (); }}
|
|
|
|
|
|
|
|
|
|
if (padHeight == -1) {
|
|
|
|
|
padHeight = parentWindow.GetUsableHeight ();
|
|
|
|
|
}
|
|
|
|
|
public ScrollWindowOverflowAction OverflowAction = ScrollWindowOverflowAction.ResizeScrollArea;
|
|
|
|
|
|
|
|
|
|
_innerPadId = NCurses.NewPad (padHeight, padWidth);
|
|
|
|
|
/// <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 ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_padSize = new Size (padWidth, padHeight);
|
|
|
|
|
/// <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 ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NCurses.WindowBackground (InnerPadId, ColorSchemes.TextInputField ());
|
|
|
|
|
ShowPad ();
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Run initialization code upon object creation
|
|
|
|
|
/// </summary>
|
|
|
|
|
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 {
|
|
|
|
|
/// <param name="scrollX"></param>
|
|
|
|
|
/// <param name="scrollY"></param>
|
|
|
|
|
public void ScrollTo (int scrollX, int scrollY) {
|
|
|
|
|
if (ParentWindow is null) return;
|
|
|
|
|
|
|
|
|
|
_scrollPosition = new Point (scrollX, scrollY);
|
|
|
|
|
ShowPad ();
|
|
|
|
|
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>
|
|
|
|
|
/// Shows the pad the current scroll location
|
|
|
|
|
/// Adds text content to this window
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void ShowPad () {
|
|
|
|
|
if (ParentWindow is null) return;
|
|
|
|
|
/// <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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var scrollToX = ScrollPosition.X;
|
|
|
|
|
var scrollToY = ScrollPosition.Y;
|
|
|
|
|
_scrollAreaSize.Height = _innerContent.Count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var viewableWidth = GetUsableWidth ();
|
|
|
|
|
var viewableHeight = GetUsableHeight ();
|
|
|
|
|
/// <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 for unnecessary scrolling due to small Pad size
|
|
|
|
|
if (PadSize.Width <= viewableWidth) {
|
|
|
|
|
scrollToX = 0;
|
|
|
|
|
// 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 ();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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
|
|
|
|
|
}
|
|
|
|
|