Gui.cs: TextView / ScrollToEnd

Created on 2 Sep 2020  路  13Comments  路  Source: migueldeicaza/gui.cs

Build: 0.89.4

New to using this project so there could be a better way to do what I'm intending, I was unable to figure it out without using some of the internals. Basically, I'm using a TextView to display the contents of a running log. As new text is added from an async Task I want the text view to scroll to the end.

Flow would be:

  1. Scroll to the last line minus the frame height so that you see the last line _but also_ the last screen of information (scrolling to just the last line with ScrollTo shows only the last line in my testing).
  2. Position the cursor on the last line.

I had attempted to add this in my code, wasn't sure how to get the current number of lines from the TextView so I was counting line breaks which I'd prefer to avoid since it already exists. In the TextView control the model.Count has that value and it additionally has the current frame height which I couldn't figure out how to get at from my own code. I created this function I put in TextView, I'm curious if it would be useful in some form (or if there's a way to accomplish this in my code without adding a function to TextView).

/// <summary>
/// Will scroll the <see cref="TextView"/> to the last line and position the cursor there.
/// </summary>
public void ScrollToEnd ()
{
    // Subtract off the frame's height from the current number of lines so that
    // when we scroll to the bottom we show the last screen worth of lines and not
    // just the last line.
    topRow = Math.Max (1, model.Count - Frame.Height);
    SetNeedsDisplay ();

    // Set the cursor to the last row and then position it.
    currentRow = (model.Count > 0) ? currentRow = model.Count - 1 : 0;
    PositionCursor ();
}

I tested this in cmd and Powershell on Windows and via Putty on Ubuntu 18.04.

All 13 comments

Want to do the Same thing!

There is a bug in TextViewat line 1007. It must be currentRow = model.Count - 1;
So you can replace this:

            case Key.CtrlMask | Key.End:
                currentRow = model.Count - 1;
                TrackColumn ();
                PositionCursor ();
                break;

            case Key.CtrlMask | Key.Home:
                currentRow = 0;
                TrackColumn ();
                PositionCursor ();
                break;

to this:

            case Key.CtrlMask | Key.End:
                ScrollToEnd ();
                break;

            case Key.CtrlMask | Key.Home:
                ScrollToHome ();
                break;

public void ScrollToEnd ()
{
    currentRow = model.Count - 1;
    TrackColumn ();
    PositionCursor ();
}

public void ScrollToHome ()
{
    currentRow = 0;
    TrackColumn ();
    PositionCursor ();
}

model.Count is never equal to zero because the default Viewconstructor insert a empty string which is added do the model.

public View () : this (text: string.Empty) { }

You could send a PR because it would be useful.

Don't forget to add documentation to the public methods.

Another suggestion is since there are already the navigation methods MoveUpand MoveDownis call the new methods as MoveHomeand MoveEnd. But they are private and I don't know they must be also public or not because perhaps they won't be very used like the new ones.

MoveHome and MoveEnd sound good to me as well. My personal preference would be to see MoveUp and MoveDown be public as well in regards to giving the caller more options to get the TextView to do what they need it to (I won't put that in the PR unless that's approved). But I also understand that sometimes the original designers are trying to save us from ourselves and have intentions that are bigger picture than my use case. :D

I tested your code and it works well. I'll work on a PR. :)

One thing of note that is probably unrelated, on cmd and also Powershell the mouse events stop firing at times for me after some usage. Further, some keys don't fire. "Control-Home" will work at first.. but then it will stop and no key combination with control works (ProcessKey stops firing, at least for some key combinations, Home works and is passed to ProcessKey, but then Control-Home doesn't fire for it). I haven't been able to track down that down yet.

That's strange. It's working on Windows.

Here's a contrived example as a Scenario, I can make a reproduction fork. Once the process ends (I used ping as an example) the mouse events stop working as well as control+home (although I can navigate to the button with the keyboard and it still works). But, you can still type in the TextView.

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Terminal.Gui;

namespace UICatalog
{

    [ScenarioMetadata(Name: "ping test", Description: "Runs a ping to test.")]
    [ScenarioCategory("Debug")]
    public class Ping : Scenario
    {
        private TextView _textView;

        public override void Setup()
        {
            var pingButton = new Button("Run Ping")
            {
                X = Pos.Center(),
                Y = 1,
                IsDefault = true,
                Clicked = () =>
                {
                    var processInfo = new ProcessStartInfo
                    {
                        FileName = "cmd.exe",
                        Arguments = "/c \"ping 127.0.0.1 -n 50\"",
                        UseShellExecute = false,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true
                    };

                    // A ping setup to run in either Linux or Windows.
                    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                    {
                        processInfo.FileName = "cmd.exe";
                        processInfo.Arguments = "/c \"ping 127.0.0.1 -n 10\"";
                    }
                    else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                    {
                        processInfo.FileName = "/bin/bash";
                        processInfo.Arguments = $"-c \"ping 127.0.0.1 -c 10\"";
                    }

                    using (var process = Process.Start(processInfo))
                    {
                        _ = ConsumeReader(process.StandardOutput);
                        _ = ConsumeReader(process.StandardError);
                    }

                }
            };

            Win.Add(pingButton);

            _textView = new TextView()
            {
                X = 1,
                Y = Pos.Bottom(pingButton) + 2,
                Width = Dim.Fill(),
                Height = Dim.Fill()                
            };

            Win.Add(_textView);
        }

        /// <summary>
        /// Consumes the TextReader and writes it's contents to the main text view.
        /// </summary>
        /// <param name="reader"></param>
        private async Task ConsumeReader(TextReader reader)
        {
            string text;

            while ((text = await reader.ReadLineAsync()) != null)
            {
                _textView.Text += $"{text}\n";
                _textView.MoveEnd();  // This can be commented out, still occurs.
            }
        }

    }
}

@blakepell thanks for the example Scenario. You're right. It's only happens on Windowsand not in Linux. On Linuxthe issue is different and only happen after quit the application and the Linuxconsole became irresponsive. It seems the process is passing to the cmd.exe because if you right click the mouse it'll show the console context menu. It'll be a challenge to discover this behaviour issue.

Thanks for your feedback @BDisp. I've put the pull request in for the original MoveHome and MoveEnd features.

If you like I can create a new issue with for the console mouse bug.

Very well thanks. We wait until @tig saying anything.
I checked that this issue only occurs on using (var process = Process.Start (processInfo)). It seems it doesn't return to the original handle when is closed.

I think you're right. I was just looking at it as well. I found a workaround for Windows that doesn't work on Linux unfortunately.

On Windows if ProcessStartInfo.CreateNoWindow is set to true this behavior goes away and all the key/mouse events continue to work. No dice on Linux.

Yes that do the trick in Windows, thanks. Unfortunately in Linuxthe unresponsive issue still happens.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tig picture tig  路  17Comments

daltskin picture daltskin  路  12Comments

BDisp picture BDisp  路  12Comments

BDisp picture BDisp  路  12Comments

MihaMarkic picture MihaMarkic  路  17Comments