Added scrolling window

main
resneptacle 3 weeks ago
parent d0853e23bc
commit be2c4fab03

@ -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);
}
}

@ -13,13 +13,39 @@ public class ContentWindow : Window {
{ }
/// <summary>
/// Creates a new nested child window without inner padding
/// Removes current children and creates a new
/// nested child window without inner padding
/// </summary>
/// <returns>New child window</returns>
public Window CreateInnerWindow () {
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
};
}
}
/// <summary>
/// Removes current children and creates a new
/// nested scrollable child window without inner padding
/// </summary>
/// <returns>New scrollable child window</returns>
/// <exception cref="NullReferenceException">Throws when TargetInputHandler is null</exception>
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
};
}
/// <summary>
/// Removes all children elements and redraws the window

@ -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>
/// Shows the pad the current scroll location
/// Adds text content to this window
/// </summary>
public void ShowPad () {
if (ParentWindow is null) return;
/// <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);
}
var scrollToX = ScrollPosition.X;
var scrollToY = ScrollPosition.Y;
/// <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;
}
var viewableWidth = GetUsableWidth ();
var viewableHeight = GetUsableHeight ();
// 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");
}
}
// Check for unnecessary scrolling due to small Pad size
if (PadSize.Width <= viewableWidth) {
scrollToX = 0;
// Finally, add string to content string list
_innerContent.Add (line);
}
if (PadSize.Height <= viewableHeight) {
scrollToY = 0;
_scrollAreaSize.Height = _innerContent.Count;
}
// Check values for upper bounds
if (scrollToX > PadSize.Width - viewableWidth) scrollToX = PadSize.Width - viewableWidth;
if (scrollToY > PadSize.Height - viewableHeight) scrollToY = PadSize.Height - viewableHeight;
/// <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 values for lower bounds
if (scrollToX < 0) scrollToX = 0;
if (scrollToY < 0) scrollToY = 0;
// 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;
}
// Store potentially updated scroll coordinates
_scrollPosition = new Point (scrollToX, scrollToY);
// Check bottom/right bounds of scroll area
if (visibleArea.Right > ScrollAreaSize.Width) {
visibleArea.X = ScrollAreaSize.Width - visibleArea.Width;
_scrollPosition.X = visibleArea.X;
}
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?!
if (visibleArea.Bottom > ScrollAreaSize.Height) {
visibleArea.Y = ScrollAreaSize.Height - visibleArea.Height;
_scrollPosition.Y = visibleArea.Y;
}
NCurses.PadRefresh (
_innerPadId,
scrollToY, scrollToX,
fromY,
fromX,
toY,
toX
);
// 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
}

@ -260,6 +260,7 @@ public class Window {
/// </summary>
public void Clear () {
NCurses.ClearWindow (this);
NCurses.MoveWindowAddString (this, GetInnerOriginY (), GetInnerOriginY (), "");
Draw ();
}

@ -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<MenuItem> 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<MenuItem> {

Loading…
Cancel
Save