Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resizable dialog with scrollview #99

Open
michael-lehn opened this issue Jan 11, 2022 · 19 comments
Open

resizable dialog with scrollview #99

michael-lehn opened this issue Jan 11, 2022 · 19 comments
Labels

Comments

@michael-lehn
Copy link
Contributor

michael-lehn commented Jan 11, 2022

Dear Markus,

after discovering just recently your amazing library I started to play around with your examples.

I am trying to write a dialog (similar to examples/scrollview.cpp) that can display some text file. Of course this can be done with FTextView. But I thought implementing some simplified, self-made variant might be a good way to learn how some of the concepts of finalcut. So my problem is mainly academic and out of curiosity :D

In a first step my plan was to write just a dialog that shows a fixed number of lines, e.g line = 1, ..., line = 42. This worked fine until I tried to make the dialog resizeable. My idea was that once a file is completely read into some buffer I will know the number of lines and number of columns that are needed to display it. Hence, the size of the scrollable area should at least have these "text dimensions".

If I understand FScrollView::setGeometry correctly then it also changes the size of the scroll area. So after making the dialog smaller the scrollbars were gone ... My idea was that the overloaded setGeometry of my scrollview adjusts the scroll dimensions to the maximum of the current widget size and the "text dimensions". But this seems to be the wrong idea ...

Maybe you can point me to some example or give me a hint how to manage this.

Best regards,
Michael

Here the (working) variant using an instance of FTextView

#include <final/final.h>

using namespace finalcut;

class PrgView final : public FDialog
{
  public:
    PrgView(FWidget *parent)
      : FDialog{ parent }
      , lineView{ this } // show 42 lines
    {
        FString line;
        for (auto i{ 0 }; i < 42; ++i) {
            line.sprintf("line = %d\n", i);
            lineView.append(line.c_str());
        }
    }

    void
    initLayout() override
    {
        FDialog::setGeometry(FPoint{ 16, 3 }, FSize{ 50, 19 });

        setText("Program");
        setBorder(false);
        setResizeable();
        lineView.setGeometry(FPoint{ 1, 1 },
                             FSize{ getWidth() - 1, getHeight() - 1 });
        FDialog::initLayout();
    }

    void
    adjustSize() override
    {
        FDialog::adjustSize();
        lineView.setGeometry(FPoint{ 1, 1 },
                             FSize{ getWidth() - 1, getHeight() - 1 });
    }

    FTextView lineView;
};

int
main(int argc, char *argv[])
{
    FApplication app{ argc, argv };

    PrgView prgView{ &app };
    FWidget::setMainWidget(&prgView);
    app.show();
    return app.exec();
}

Here my attempt to achieve the same with a class derived from FScrollView:

#include <final/final.h>

using namespace finalcut;

class LineView : public FScrollView
{
  public:
    LineView(FWidget *parent)
      : FScrollView{ parent }
    {
    }

    void
    draw() override
    {
        clearArea();
        for (auto y{ 1 }; y <= int(numLines); ++y) {
            setPrintPos(FPoint{ 1, y });
            print() << "Line = " << y;
        }
        FScrollView::draw();
    }

    void
    setGeometry(const FPoint &pos, const FSize &size,
                bool adjust = true) override
    {
        FScrollView::setGeometry(pos, size, adjust);
        auto w = std::max(numCols, getWidth());
        auto h = std::max(numLines, getHeight());

        setScrollSize(FSize{ w, h });
    }

  private:
    std::size_t numCols{ 10 };
    std::size_t numLines{ 42 };
};

class PrgView final : public FDialog
{
  public:
    PrgView(FWidget *parent)
      : FDialog{ parent }
      , lineView{ this } // show 42 lines
    {
    }

    void
    initLayout() override
    {
        FDialog::setGeometry(FPoint{ 16, 3 }, FSize{ 50, 19 });

        setText("Program");
        setBorder(false);
        setResizeable();
        lineView.setGeometry(FPoint{ 1, 1 },
                             FSize{ getWidth() - 1, getHeight() - 1 });
        FDialog::initLayout();
    }

    void
    setGeometry(const FPoint &p, const FSize &s, bool adj = true) override
    {
        FDialog::setGeometry(p, s, adj);
    }

    void
    adjustSize() override
    {
        FDialog::adjustSize();
        lineView.setGeometry(FPoint{ 1, 1 },
                             FSize{ getWidth() - 1, getHeight() - 1 });
    }

    LineView lineView;
};

int
main(int argc, char *argv[])
{
    FApplication app{ argc, argv };

    PrgView prgView{ &app };

    FWidget::setMainWidget(&prgView);
    app.show();
    return app.exec();
}
@gansm
Copy link
Owner

gansm commented Jan 12, 2022

Hello Michael, I'm glad you like FINAL CUT 😊.

First, a few remarks about your program:

  • FScrollView is a meta widget that has no predefined color. You have to assign a foreground and background color to the widget. For example, you can use the method useParentWidgetColor() here.

  • If you want a child widget that can draw on the padding area of the parent widget, you must explicitly enable this in the child widget by calling ignorePadding(). The FPoint{1, 1} is then identical to its parent widget. So you can set the geometry values for a full-size child widget from FDialog to setGeometry(FPoint{1, 2}, FSize{getWidth(), getHeight() - 1}).

  • If you use my last commit (34c3be3), you can also pass the FScrollView mouse events of the resize-corner to the Dialog widget.

Unfortunately, I have to inform you that the widget you are trying to generate already exists under the name FTextView 😉. I use it in examples/ui.cpp, for example.

 
Example:

#include <final/final.h>

using namespace finalcut;

class TextDialogWidget final : public FDialog
{
  public:
    explicit TextDialogWidget (FWidget* parent = nullptr)
      : FDialog(parent)
    { 
      // Fill with a few lines
      for (std::size_t n{0}; n < numLines; n++)
      {
        scrollText << FString(FString("Line = ") << n);
      }
    }

  private:
    void initLayout() override
    {
      setText ("Dialog");
      scrollText.ignorePadding();
      scrollText.setFocus();
      scrollText.setGeometry (FPoint{1, 2}, FSize{getWidth(), getHeight() - 1});
      setMinimizable();
      setResizeable();
      setMinimumSize (FSize{17, 6});
      FDialog::initLayout();
    }

    void adjustSize()
    {
      FDialog::adjustSize();
      scrollText.setGeometry (FPoint{1, 2}, FSize(getWidth(), getHeight() - 1));
    }

    // Data members
    FTextView scrollText{this};
    std::size_t numLines{42};
};

int main (int argc, char* argv[])
{
  FApplication app(argc, argv);
  TextDialogWidget dialog(&app);
  dialog.setGeometry (FPoint{28, 2}, FSize{18, 15});
  app.setMainWidget(&dialog);
  dialog.show();
  return app.exec();
}

image

@gansm gansm added the question label Jan 12, 2022
@gansm
Copy link
Owner

gansm commented Jan 12, 2022

I just noticed that you edited your post again. Nice that you found FTextView yourself, without a complete widget documentation.

@michael-lehn
Copy link
Contributor Author

Thanks for your fast response! And sorry for my late feedback 😬

The problem with my self-made FDialog + FScrollView variant was that it "occasionally" crashed, and these crashes were more or less reproducible. But before it crashes resizing, scrolling and colouring works fine. I will try to apply your notes and also investigate further what causes the problem. It certainly requires a better understanding of things from my side. And I anyway want to understand your implementation in more detail.

Besides for learning, there actually was another reason for considering to implement some self-made text viewer. What I need is a scrollable text viewer where I can highlight a certain line, and I also want to place markers in the first column. As a dirty hack I therefore

  • changed in FTextView the private into protected
  • derived a class TextView which most of all overrides drawText

However, this is certainly not the way it should be done. Of course I could copy the FTextView code, remove/add a few things ... but that is not much better. Do you have any suggestions for achieving something like this?

Screenshot 2022-01-13 at 20 44 03

For giving some background: As the screenshot suggest I am writing a debugger for a virtual machine called ULM (Ulm Lecture Machine). Years ago I have implemented a TUI for it with ncurses. And I can confirm:

Users of ncurses are cursed, literally.

Although I personally believe that programming is best learned by using the terminal only I therefore implemented a GUI with Qt. This is certainly a nice library but Corona actually proved the importance of the terminal and TUIs:

  • It is already hard to help students to install compilers, libraries and stuff on their own computers.
  • It is nearly impossible to provide sufficient help to install non-standard software.

Giving them access to a server via ssh is therefore important. But a GUI then requires that they have a working X-server, and even if that works it's painfully slow. So your library is a lifesaver for me. It is well-designed, compiles without problems on Linux/macOS/Solaris. Windows users can install Cygwin or can use ssh.

Btw, the screenshot below shows the current status. The debugger can execute instruction-by-instruction and a CPU viewer shows the instruction pointer, instruction register and read/writes to general purpose registers. Thanks to FINAL CUT this was really easy to implement. And I am pretty confident that other things e.g. viewers for the memory or I/O will go similarly smooth.

Screenshot 2022-01-13 at 20 43 10

@gansm
Copy link
Owner

gansm commented Jan 14, 2022

Wow, I am very impressed with your screenshots. It looks very fancy.

The problem with crashes could be caused by the fact that you did not restrict the shrinking of the dialog by using the setMinimumSize() method. Therefore, some lengths or areas may get negative values due to the difference calculation.

Another reason for a crash could be an infinite loop caused by setGeometry(). This happens e.g. when calling setGeometry() from adjustSize(), because setGeometry() has set adjust = true by default, which results in a call to adjustSize().

(See the last section in chapter Areas)

Unfortunately, I have not yet come up with a better solution to prevent unwanted recursion loops.

 
Why avoid FScrollView for large scrolling areas:

FScrollView uses a lot of memory because it stores the displayed data in a virtual terminal area (similar to a bitmap/pixmap) composed of FChar elements (defined in final/ftypes.h). FTextView, on the other hand, keeps only the plain text information in memory.

To create a derived class of FTextView seems to be the right way. The virtual method draw() should be overridden in this class. This class could have additional information that allows displaying lines or text areas in different colors. For example, you could implement syntax highlighting or something similar.


I am glad that you have found a working solution to your implementation problems with FINAL CUT. It always makes me happy when my concept and interface design is well received. My intention in implementing the widget API was that it should be user-friendly and easy to learn. FINAL CUT should support developers in their work. A framework should not disturb a user too much in what he wants to do with it.

PS: ULM is a nice recursive acronym! I like it :-)

@gansm
Copy link
Owner

gansm commented Jan 15, 2022

Hi Michael, I liked your idea with the colors in the text so much that I just implemented it in FTextView (dc1afbb + c5b75e5). Thank you for this great idea.

I will also generate a sample program to demonstrate functionality in one following commits.

Here is my short explanation of how to use text highlighting in FTextView:

finalcut::FTextView scrolltext{parent_widget};
scrolltext.append("Linux is like wigwam: no Gates, no Windows and Apache inside.");

finalcut::FChar fchar{};
fchar.ch[0]        = L' ';
fchar.fg_color     = FColor::Red;
fchar.bg_color     = FColor::Yellow;
fchar.attr.byte[0] = 0;
fchar.attr.byte[1] = 0;
fchar.attr.byte[2] = 0;
fchar.attr.byte[3] = 0;
fchar.attr.bit.italic = true;
scrolltext.addHighlight(0, finalcut::FTextView::FTextHighlight{14, 6, std::move(fchar)});

finalcut::FStyle style1;
style1.setStyle (finalcut::Style::Italic);
scrolltext.addHighlight(0, finalcut::FTextView::FTextHighlight{32, 10, FColor::Green, style1});

finalcut::FStyle style2;
style2.setStyle (finalcut::Style::Underline);
scrolltext.addHighlight(0, finalcut::FTextView::FTextHighlight{47, 6, finalcut::FColorPair(FColor::White, FColor::Blue), style2});

The result looks like this:
image

New methods for highlighting:

void FTextView::addHighlight (std::size_t line, FTextHighlight&& hgl);
void FTextView::resetHighlight (std::size_t line);

The data class FTextHighlight:

struct FTextHighlight
{
  FTextHighlight (std::size_t i, std::size_t l, FChar&& fchar) noexcept
    : index{i}
    , length{l}
    , attributes{std::move(fchar)}
  { }

  FTextHighlight (std::size_t i, std::size_t l, FColor c, const FStyle& s = FStyle()) noexcept
    : index{i}
    , length{l}
  {
    attributes.fg_color = c;
    attributes.bg_color = getColorTheme()->dialog_bg;
    attributes.attr = s.toFAttribute();
  }

  FTextHighlight (std::size_t i, std::size_t l, const FColorPair& cpair, const FStyle& s = FStyle()) noexcept
    : index{i}
    , length{l}
  {
    attributes.fg_color = cpair.getForegroundColor();
    attributes.bg_color = cpair.getBackgroundColor();
    attributes.attr = s.toFAttribute();
  }

  std::size_t index{};
  std::size_t length{};
  FChar       attributes{};
};

 
UPDATE:

I have now created an example program that shows how to use text highlighting with the GNU Lesser General Public License (LGPL) text.

examples/highlight-text.cpp

@michael-lehn
Copy link
Contributor Author

This is great! Thanks for the feature! Today I finally found some time for coding and used your example. This is the result (and a follow question):

Screenshot 2022-01-21 at 18 46 55

The almost invisibly grey highlighted line indicates what instruction was executed and the red line indicates what instruction gets executed next. Ideally the complete line would be highlighted.

My current hack is to pad each line with spaces such that each line has (for example) 200 characters. Then I get the effect that I want. But I wonder whether there is already a feature to achieve this more elegant?

I also hope to find some time to implement a pygmentize like syntax highlighter so that I can post some cooler screenshots ;-) Your new feature really provides lots of opportunities!

@gansm
Copy link
Owner

gansm commented Jan 21, 2022

You are right. It doesn't work because the trailing whitespaces were not part of the FVTermBuffer object. I have changed this now (16b56a3) so that highlighting is also possible up to the end of the line.

constexpr auto EOL = std::numeric_limits<std::size_t>::max();
scrolltext.addHighlight(1, finalcut::FTextView::FTextHighlight{0, EOL, finalcut::FColorPair(FColor::White, FColor::Red)});

I have an idea for your project:
Using jump arrows like in radare2 would visualize the program flow better.

@michael-lehn
Copy link
Contributor Author

Great! Works like a charm 👍

Also thanks for your suggestion to use jump arrows like in radare2. That is certainly the right thing to do

@gansm
Copy link
Owner

gansm commented Jan 29, 2022

Hi Michael, I just had the idea for FTextView::FTextHighlight to provide constructors without length specification in the interface. These constructors always highlight the whole text from the given position to the end of the line. I have implemented this now (4ffd4be). A developer or you no longer need to implement this yourself.

Constructors of FTextHighlight:

FTextHighlight (std::size_t index, std::size_t length, const FChar& fchar);
FTextHighlight (std::size_t index, const FChar& fchar);
FTextHighlight (std::size_t index, std::size_t length, const FStyle& style);
FTextHighlight (std::size_t index, const FStyle& style) ;
FTextHighlight (std::size_t index, std::size_t length, FColor fg_color, const FStyle& style = FStyle());
FTextHighlight (std::size_t index, FColor fg_color, const FStyle& style = FStyle());
FTextHighlight (std::size_t index, std::size_t length, const FColorPair& cpair, const FStyle& style = FStyle());
FTextHighlight (std::size_t index, const FColorPair& cpair, const FStyle& style = FStyle());

@michael-lehn
Copy link
Contributor Author

Again, sorry for the late response. I took some time to find time to continue with my project. It is still not finished but now I have reached a stage where it is providing some core functionality:

  • Execute a program step-by-step or advance to the next break point
  • showing updates of CPU registers
  • showing allocated pages of the virtual memory
  • highlighting the current stack frame
  • some input/output view

Here some brief demo showing the execution of a "hello, word!" program (compiled with the ULM C compiler so the code is lengthy and breakpoints are really needed), and a simple "echo" program showing how characters are read from the input buffer ...

Untitled.mp4

However, I had to apply some hacks to finalcut. Mostly I needed some more access to internal things so I added some methods or changed in some place 'private' to 'protected'. If you are interested I can write down in more detail where and why I made some modifications. However, most of it was just done with the intension "make it work now and think about how to do it right later". I also add the result of 'git diff' this might already give some overview.

diff.txt

@gansm
Copy link
Owner

gansm commented Apr 11, 2022

I am back from my vacation and I hope my answer is not too late for you. Thank you for the screencast video. Your implementation looks very consistent and neat.

I am very interested in where the FINAL CUT API is too restrictive or a widget cannot be used as expected. Therefore, I have analyzed your diff file with great interest.

If I see it correctly, we are talking about the widgets FLineEdit and FTextView. Unfortunately, all private methods in FLineEdit have been made protected methods. So I cannot identify the intuition for this.

FTextView lacks direct access to individual lines.

Proposal:

// Direct access to any text line
FTextViewLine& getLine (std::size_t line);

// Usage in programming
auto& text_ref = getLine(20).text;
auto& highlight_ref = getLine(20).highlight;

FTextView does not return information about text position and size.

Proposal:

// Get the scroll position (analogous to FScrollView)
FPoint getScrollPos() const;

// Get the size of the visible text
FSize getTextVisibleSize() const;

Note: The explicit call of the method FDialog::drawTitleBar() is not required because the call of redraw() calls this method automatically.

dgl.setText("New title");
dgl.redraw();

This call is very economical through FINAL CUT only writes the changes to the terminal.

@michael-lehn
Copy link
Contributor Author

Your vacation is well deserved! I think most of your proposals will do the trick for me.

About FLineEdit however, for filling the input buffer I was deriving a class from it. If a user hits some special keys (e.g. return, tab, Control-D, etc.) I want some special behaviour like adding the escaped representation (e.g. hitting return adds two characters backslash and n). So I came up with something like that:

class IOView final : public finalcut::FDialog
{
  public:
    explicit IOView(finalcut::FWidget * = nullptr);
    void notify();

    class InputBuffer final : public finalcut::FLineEdit
    {
      public:
        explicit InputBuffer(IOView *);
        ~InputBuffer();
        void onKeyPress(finalcut::FKeyEvent *) override;
        bool keyInput(finalcut::FKey key);
        void deletePreviousCharacter();
        void notify();

      private:
        IOView *ioView;
    };

  private:
    using MatchList = std::vector<std::size_t>;
    using StringPos = std::wstring::size_type;

    void initLayout() override;
    void adjustSize() override;

    void onAccel(finalcut::FAccelEvent *) override;

    finalcut::FTextView scrolltext{ this };
    finalcut::FLabel inBufLabel{ this };
    finalcut::FLabel inputLabel{ this };
    InputBuffer input{ this };
    finalcut::FLineEdit inBuf{ this };
};

Btw: Last week I made a YouTube video for giving some overview of my lecture. And Final Cut gets mentioned at 13:50.

@gansm
Copy link
Owner

gansm commented Apr 17, 2022

Hi Michael, I have now implemented your great hints in code (083ea79).

FLineEdit now has the following new methods:

void inputText (const FString& input);
void deletesCharacter();
void moveCursorToBegin();
void moveCursorToEnd();
void stepCursorForward (std::size_t steps = 1);
void stepCursorBackward (std::size_t steps = 1);
void setOverwriteMode (bool overwrite = true);

You can easily recreate the backspace functionality (deletePreviousCharacter()) with:

stepCursorBackward();
deletesCharacter();

I hope that any modification of the library code is now no longer necessary to implement your application.

Many thanks for mentioning FINAL CUT in your education video.

@michael-lehn
Copy link
Contributor Author

I finally found some time to adapt my code. Thanks a lot!!

With your modifications almost everything works (and I can perfectly live with that):

  • stepCursorBackward(); is moving the cursor to the right. So for deletePreviousCharacter() is used stepCursorForward()
  • If the \t gets typed in I would like to add two characters, \\ and t. So I tried this in my onKeyPress(FKeyEvent *ev) method:
    // ...
    } else if (key == FKey::Tab) {
        inputText("\\t");
        stepCursorBackward(2);
        ev->accept();
    }
    // ...
    if (ev->isAccepted()) {
        drawInputField();
        forceTerminalUpdate();
    }
    
    This works partially. The text gets added but the input filed not redrawn. That means I can not see the added text until some other character gets typed in

@gansm
Copy link
Owner

gansm commented May 4, 2022

Hi Michael, thank you for the feedback. When I wrote the code, I had just worked with scrollAreaForward() and scrollAreaReverse(). So I probably confused the text move with the cursor move. This behavior is now corrected (6571342).

On changes, inputText() now sets the input cursor to the correct position and outputs the changed text directly in the input field.

@michael-lehn
Copy link
Contributor Author

Wow! Thank you Markus for this quick response. Everything works now like a charm! :-)

@michael-lehn
Copy link
Contributor Author

I am sudden facing a crash on the solaris machine (gcc 11.2.0, SunOS 5.11). And I can not reproduce it on any Linux and MacOS boxes.

I located the problem in codeview.cpp and could the crash with the more minimalistic test program below. It crashes in auto &highlight = scrolltext.getLine(line).highlight;. And actually the scrolltext.getLine(line) which is a call to std::vector::at(). So it certainly is an out of range exception. I must be blind, but I did do not understand why ... Could you have a look at it?

#include <cinttypes>
#include <final/final.h>
#include <iostream>

class Test : public finalcut::FDialog
{
  public:
    // Constructor
    explicit Test(finalcut::FWidget *parent = nullptr)
      : finalcut::FDialog{ parent }
    {
        finalcut::FString instrStr;

        uint64_t addr;
        finalcut::FTextView::FTextViewList::size_type line;
        // int line;
        for (addr = 0, line = 0; addr < 42; addr += 4, ++line) {
            instrStr.sprintf("0x%016" PRIX64 ", sizeof(line) = %zu, line = %zu",
                             addr, sizeof(line), line);
            scrolltext.append(instrStr);
            auto &highlight = scrolltext.getLine(line).highlight;   // <- crash
        }
    }

    void
    initLayout() override
    {
        scrolltext.setGeometry(finalcut::FPoint{ 1, 2 },
                               finalcut::FSize{ getWidth(), getHeight() - 1 });
    }

    finalcut::FTextView scrolltext{ this };
};

int
main(int argc, char *argv[])
{
    // Create the application object
    finalcut::FApplication app{ argc, argv };

    Test test{ &app };
    test.setSize(finalcut::FSize{ 57, 20 });
    test.setShadow();

    // Set dialog d as main widget
    finalcut::FWidget::setMainWidget(&test);

    // Show and start the application
    test.show();
    return app.exec();
}

@gansm
Copy link
Owner

gansm commented May 6, 2022

Hmm, that sounds very strange. I have tried to reproduce the error. Unfortunately, I have not had success.

image

image

Your system installation may be incomplete or the Solaris versions are not the same.

@michael-lehn
Copy link
Contributor Author

Thanks! Actually I am glad that you can not reproduce the error. And most of all don't see an obvious error in the code. Because I also started to believe that there is a problem with the Solaris system.

Unfortunately it a machine that is not maintained by myself. So I will abandon that system for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants