Added scrolling window

main
resneptacle 4 weeks ago
parent d0853e23bc
commit be2c4fab03

@ -15,6 +15,12 @@ public class ColorSchemes {
// TextInputField // TextInputField
NCurses.InitPair (4, CursesColor.BLACK, CursesColor.WHITE); 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 () { public static uint Default () {
@ -34,6 +40,10 @@ public class ColorSchemes {
} }
public static uint CustomColorTest () { 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> /// <summary>
/// Creates a new nested child window without inner padding /// Removes current children and creates a new
/// nested child window without inner padding
/// </summary> /// </summary>
/// <returns>New child window</returns>
public Window CreateInnerWindow () { 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) { return new Window (-1, -1, -2, -2, this) {
BorderEnabled = false 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> /// <summary>
/// Removes all children elements and redraws the window /// Removes all children elements and redraws the window

@ -1,35 +1,58 @@
using System.Collections.ObjectModel;
using System.Drawing; using System.Drawing;
using Mindmagma.Curses; using Mindmagma.Curses;
namespace SCI.CursesWrapper; namespace SCI.CursesWrapper;
public class ScrollWindow : Window { public class ScrollWindow : Window {
private nint _innerPadId;
public nint InnerPadId { get { return _innerPadId; }}
private Point _scrollPosition; private Point _scrollPosition;
public Point ScrollPosition { get { return _scrollPosition; }} public Point ScrollPosition { get { return _scrollPosition; }}
private Size _padSize; private Size _scrollAreaSize;
public Size PadSize { get { return _padSize; }} public Size ScrollAreaSize { get { return _scrollAreaSize; }}
public ScrollWindow (Window parentWindow, InputHandler inputHandler, int innerHeight = -1) : private List<string> _innerContent = new List<string> ();
base (0, 0, 0, 0, inputHandler, parentWindow) public ReadOnlyCollection<string> InnerContent { get { return _innerContent.AsReadOnly (); }}
{
var padWidth = GetUsableWidth ();
var padHeight = innerHeight;
if (padHeight == -1) { public ScrollWindowOverflowAction OverflowAction = ScrollWindowOverflowAction.ResizeScrollArea;
padHeight = parentWindow.GetUsableHeight ();
}
_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 ()); /// <summary>
ShowPad (); /// Run initialization code upon object creation
/// </summary>
private void _InitializeScrollingWindow () {
_scrollAreaSize = new Size (
WindowSize.Width,
WindowSize.Height
);
OnKeyPress += ScrollKeyHandler; OnKeyPress += ScrollKeyHandler;
} }
@ -46,6 +69,18 @@ public class ScrollWindow : Window {
ScrollTo (ScrollPosition.X, ScrollPosition.Y - 1); ScrollTo (ScrollPosition.X, ScrollPosition.Y - 1);
} else if (e.KeyCode == CursesKey.DOWN) { } else if (e.KeyCode == CursesKey.DOWN) {
ScrollTo (ScrollPosition.X, ScrollPosition.Y + 1); 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="scrollX"></param>
/// <param name="scrollY"></param> /// <param name="scrollY"></param>
public void ScrollTo (int scrollX, int scrollY) { public void ScrollTo (int scrollX, int scrollY) {
if (ParentWindow is null) return;
_scrollPosition = new Point (scrollX, scrollY); _scrollPosition = new Point (scrollX, scrollY);
ShowPad (); RenderCurrentViewport ();
} }
/// <summary> /// <summary>
/// Shows the pad the current scroll location /// Adds text content to this window
/// </summary> /// </summary>
public void ShowPad () { /// <param name="contentStr">Block of text to add to this window</param>
if (ParentWindow is null) return; public void AddContent (string contentStr) {
var lines = contentStr.Split ('\n', StringSplitOptions.RemoveEmptyEntries);
AddContent (lines);
}
var scrollToX = ScrollPosition.X; /// <summary>
var scrollToY = ScrollPosition.Y; /// 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 (); // Do overflow action if text is overflowing
var viewableHeight = GetUsableHeight (); 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 // Finally, add string to content string list
if (PadSize.Width <= viewableWidth) { _innerContent.Add (line);
scrollToX = 0;
} }
if (PadSize.Height <= viewableHeight) {
scrollToY = 0; _scrollAreaSize.Height = _innerContent.Count;
} }
// Check values for upper bounds /// <summary>
if (scrollToX > PadSize.Width - viewableWidth) scrollToX = PadSize.Width - viewableWidth; /// Updates the currently visible part of the inner content
if (scrollToY > PadSize.Height - viewableHeight) scrollToY = PadSize.Height - viewableHeight; /// </summary>
public void RenderCurrentViewport () {
var visibleArea = new Rectangle (
ScrollPosition.X,
ScrollPosition.Y,
WindowSize.Width,
WindowSize.Height
);
// Check values for lower bounds // Check top/left bounds of scroll area
if (scrollToX < 0) scrollToX = 0; if (visibleArea.X < 0) {
if (scrollToY < 0) scrollToY = 0; visibleArea.X = 0;
_scrollPosition.X = 0;
}
if (visibleArea.Y < 0) {
visibleArea.Y = 0;
_scrollPosition.Y = 0;
}
// Store potentially updated scroll coordinates // Check bottom/right bounds of scroll area
_scrollPosition = new Point (scrollToX, scrollToY); if (visibleArea.Right > ScrollAreaSize.Width) {
visibleArea.X = ScrollAreaSize.Width - visibleArea.Width;
_scrollPosition.X = visibleArea.X;
}
var fromY = Position.Y; if (visibleArea.Bottom > ScrollAreaSize.Height) {
var fromX = Position.X; visibleArea.Y = ScrollAreaSize.Height - visibleArea.Height;
var toY = viewableHeight; _scrollPosition.Y = visibleArea.Y;
var toX = viewableWidth - 2; // TODO: Why the two? How is the geometry this wong?! }
NCurses.PadRefresh ( // Do not scroll if the virtual scroll area is smaller or
_innerPadId, // the same size of the actual window
scrollToY, scrollToX, if (ScrollAreaSize.Width <= visibleArea.Width) {
fromY, visibleArea.X = 0;
fromX, _scrollPosition.X = 0;
toY, }
toX 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> /// </summary>
public void Clear () { public void Clear () {
NCurses.ClearWindow (this); NCurses.ClearWindow (this);
NCurses.MoveWindowAddString (this, GetInnerOriginY (), GetInnerOriginY (), "");
Draw (); Draw ();
} }

@ -2,8 +2,6 @@
using SCI.CursesWrapper.UiElements; using SCI.CursesWrapper.UiElements;
using ANSI_Fahrplan.Screens; using ANSI_Fahrplan.Screens;
using Mindmagma.Curses; using Mindmagma.Curses;
using System.Reflection;
using Microsoft.Win32.SafeHandles;
namespace ANSI_Fahrplan; namespace ANSI_Fahrplan;
@ -77,21 +75,23 @@ class Program {
private static List<MenuItem> CreateMenuItems (ContentWindow contentWindow) { private static List<MenuItem> CreateMenuItems (ContentWindow contentWindow) {
var helpItem = new MenuItem ("Help", "F1"); var helpItem = new MenuItem ("Help", "F1");
var upcomingItem = new MenuItem ("Upcoming", "b"); //F2 var upcomingItem = new MenuItem ("Upcoming", "F2");
var byDayItem = new MenuItem ("By Day", "n"); // F3 var byDayItem = new MenuItem ("By Day", "F3");
var byRoomItem = new MenuItem ("By Room", "F4"); var byRoomItem = new MenuItem ("By Room", "F4");
var bySpeakerItem = new MenuItem ("By Speaker", "F5"); var bySpeakerItem = new MenuItem ("By Speaker", "F5");
var quitItem = new MenuItem ("Quit (C-q)"); var quitItem = new MenuItem ("Quit (C-q)");
helpItem.OnItemActivated += (object sender, MenuItemActivatedEventArgs e) => { helpItem.OnItemActivated += (object sender, MenuItemActivatedEventArgs e) => {
contentWindow.Clean (); var scrollWindow = contentWindow.CreateInnerScrollWindow ();
if (contentWindow.TargetInputHandler is null) return; for (int i = 0; i <= 100; i++)
scrollWindow.AddContent ($"Line {i} {AsciiArt.lorem_ipsum}\n");
var scrollWindow = new ScrollWindow (contentWindow, contentWindow.TargetInputHandler, 30); scrollWindow.RenderCurrentViewport ();
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}"); upcomingItem.OnItemActivated += (object sender, MenuItemActivatedEventArgs e) => {
scrollWindow.ShowPad (); var scrollWindow = contentWindow.CreateInnerScrollWindow ();
scrollWindow.AddContent ("-- Upcoming --");
scrollWindow.RenderCurrentViewport ();
}; };
return new List<MenuItem> { return new List<MenuItem> {

Loading…
Cancel
Save