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

Question: How to scroll two CodeAreas simultaneously? #1136

Closed
jplwill opened this issue Sep 6, 2022 · 13 comments · Fixed by FXMisc/Flowless#113
Closed

Question: How to scroll two CodeAreas simultaneously? #1136

jplwill opened this issue Sep 6, 2022 · 13 comments · Fixed by FXMisc/Flowless#113

Comments

@jplwill
Copy link

jplwill commented Sep 6, 2022

I'm wanting to implement a two-column diff viewer based on RichTextFX's CodeArea; and to do that I need two CodeAreas side-by-side that are scrolled simultaneously. More specifically:

  • Scrolling either CodeArea vertically, by any mechanism, should automatically scroll the other, so they display the same range of lines.
  • Horizontally, I don't much care whether they scroll independently or not.

Is this possible with the current version? What's the cleanest way to do it?

@jplwill
Copy link
Author

jplwill commented Sep 6, 2022

Here's my current attempt, using RichTextFX 0.10.9. First, I've defined a simple class, CodeViewer, that combines a CodeArea with its virtualized scroll pane:

public class CodeViewer extends StackPane {
    private final CodeArea codeArea;
    private final VirtualizedScrollPane<CodeArea> scrollPane;

    public CodeViewer() {
        super();
        this.codeArea = new CodeArea();
        this.scrollPane = new VirtualizedScrollPane<>(codeArea);
        getChildren().add(scrollPane);
        codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
        codeArea.setEditable(false);
    }

    public VirtualizedScrollPane<?> scrollPane() {
        return scrollPane;
    }

    public void setContent(String code) {
        codeArea.replaceText(0, 0, code);
    }
}

Then, I bind them together like this:

CodeViewer left = ...;
CodeViewer right = ...;

left.scrollPane().estimatedScrollXProperty()
    .bindBidirectional(right.scrollPane().estimatedScrollXProperty());
left.scrollPane().estimatedScrollYProperty()
    .bindBidirectional(right.scrollPane().estimatedScrollYProperty());
left.scrollPane().setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);

This almost does what's wanted. I can drag the vertical scrollbar and the two panes scroll together; but sometimes when I let go of the thumb the bidirectional binding causes a logic loop and it starts jittering between two positions uncontrollable, or slowly scrolling to the bottom uncontrollably.

@Jugen
Copy link
Collaborator

Jugen commented Sep 7, 2022

Yeah, the problem is that the scroll x/y property is an estimate and is continuously being updated/recalculated as new cells/lines come into and leave the view port. When bound bidirectionally this creates an unstable value changing environment that easily gets messed up. So we need to resort to some trickery to try and stabilize the situation.

Here's my initial attempt using your CodeViewer above:

Var<Double> leftScrollY = left.scrollPane().estimatedScrollYProperty();
Var<Double> rightScrollY = right.scrollPane().estimatedScrollYProperty();
boolean[] isBusy = new boolean[2];
final int LEFT = 0, RIGHT = 1;

leftScrollY.addListener( (ob,ov,nv) ->
{
    isBusy[LEFT] = true;
    if ( ! isBusy[RIGHT] ) rightScrollY.setValue( nv );
    isBusy[LEFT] = false;
});

rightScrollY.addListener( (ob,ov,nv) ->
{
    isBusy[RIGHT] = true;
    if ( ! isBusy[LEFT] ) leftScrollY.setValue( nv );
    isBusy[RIGHT] = false;
});

Hopefully this works for you ....

@jplwill
Copy link
Author

jplwill commented Sep 7, 2022

That appears to do it! Thanks very much! I'll get back to you if I run into any surprises.

@jplwill
Copy link
Author

jplwill commented Sep 15, 2022

So, the above still seems to be working, but while debugging something else I ran into some behavior I didn't expect. I've instrumented the above listeners as follows:

      leftScrollY.addListener((ob,ov,nv) -> {
            isBusy[LEFT] = true;
            if (!isBusy[RIGHT] && rightScrollY.getValue() != nv.doubleValue()) {
                System.out.println("leftScrollY: scrolling right=" +
                    rightScrollY.getValue() + " to " + nv);
                rightScrollY.setValue( nv );
                System.out.println("leftScrollY: done");

            }
            isBusy[LEFT] = false;
        });

        rightScrollY.addListener((ob,ov,nv) -> {
            isBusy[RIGHT] = true;
            if (!isBusy[LEFT] && leftScrollY.getValue() != nv.doubleValue()) {
                System.out.println("rightScrollY: scrolling left=" +
                        leftScrollY.getValue() + " to " + nv);
                leftScrollY.setValue( nv );
                System.out.println("rightScrollY: done:");
            }
            isBusy[RIGHT] = false;
        });

When the user navigates from one change in the diff view to the next, I scroll the paired CodeAreas by calling showParagraphAtTop on the lefthand CodeArea, and letting the listeners propagate it. When I do that, I get a trace like this:

Scrolling to selected edit
showParagraphAtTop: 17
leftScrollY: scrolling right=0.0 to 146.0
leftScrollY: done
rightScrollY: scrolling left=146.0 to 203.0
rightScrollY: done:
leftScrollY: scrolling right=203.0 to 162.0
leftScrollY: done
leftScrollY: scrolling right=162.0 to 178.0
leftScrollY: done
rightScrollY: scrolling left=178.0 to 121.0
rightScrollY: done:
leftScrollY: scrolling right=121.0 to 162.0
leftScrollY: done
leftScrollY: scrolling right=121.0 to 105.0
leftScrollY: done
leftScrollY: scrolling right=105.0 to 15.0
leftScrollY: done

The number of calls back and forth varies; I've counted as many as 9. Also, the order of the messages indicates that isBusy array is doing its job, and each listener call after the first is being triggered via Platform.runLater.

Or maybe showParagraphAtTop does a number of scrolls in sequence?

Dunno. This looks like a problem waiting to happen, so I wanted to pass it along.

@jplwill
Copy link
Author

jplwill commented Sep 15, 2022

And in fact, calling showParagraphAtTop isn't reliable in this setting. (It works fine for me on a single CodeArea.) The resulting position can be:

  • Exactly where I wanted it
  • A couple of lines (paragraphs) below where I wanted it
  • Nowhere near where I wanted it.

@jplwill
Copy link
Author

jplwill commented Sep 19, 2022

Maybe if I disabled the leftScrollY/rightScrollY listeners before calling showParagraphAtTop for both CodeAreas, and then re-enabled them via Platform.runLater() (and so, after everything had settled)?

@jplwill
Copy link
Author

jplwill commented Sep 22, 2022

Aha! The showParagraphAtTop issue was my fault. My code has been setting a special style on the paragraphGraphic for the selected lines to indicate the selection; and the prototype has been doing this by calling recreateParagraphGraphic for every paragraph in the CodeArea. I disabled that, and no more scrolling problem.

@jplwill
Copy link
Author

jplwill commented Sep 23, 2022

Whoops! That helped; but it didn't solve the problem. Programmatically scrolling still causes occasional problems.

I've tried disabling the leftScrollY/rightScrollY listeners while calling showParagraphTop (in several different ways), but it doesn't get the job done.

The problem seems to be that setting the property, e.g., rightScrollY.setValue( nv ); doesn't take effect immediately. I've seen traces like this:

println(rightScrollY.getValue()); // Prints 1224.0
rightScrollY.setValue(1332.0);
println(rightScrollY.getValue()); // Still prints 1224.0
// And then later, the listener gets called.

It appears that the call to setValue schedules a future event to actually set the value. My attempts to work around this using Platform.runLater() have been unavailing; this future event happens after my Platform.runLater().

Is there a way to just set the scroll value and make it stick immediately? Failing that, is there a way to use CodeArea with a standard JavaFX ScrollPane?

@Jugen
Copy link
Collaborator

Jugen commented Oct 16, 2022

Sorry for the delayed response, I now have time again to take another look at this .....

In order to see if it's the ScrollPane that's at fault could you please try and test what happens if you put both the CodeArea's directly into the Scene without being wrapped in a VirtualizedScrollPane. (You'll still be able to scroll with the mouse wheel, just the scrollbar will be missing.)

Also make the following change:

Var<Double> leftScrollY = left.estimatedScrollYProperty();    // remove: ~scrollPane()~ 
Var<Double> rightScrollY = right.estimatedScrollYProperty();  // remove: ~scrollPane()~

Thanks

@jplwill
Copy link
Author

jplwill commented Oct 20, 2022

So I made this change, and yes, I'm still seeing bizarre behavior. Sometimes it does the right thing, sometimes it does something slightly wrong, and sometimes it is just way off.

@Jugen
Copy link
Collaborator

Jugen commented Oct 20, 2022

Thanks, I'll work on it again on Friday and hopefully figure something out.

@jplwill
Copy link
Author

jplwill commented Oct 20, 2022

Thanks! I persist in thinking that there are some timing-related things going on: that when I click (and how often) might also be having an effect. But for the experiment this morning I made sure to trigger the programmatic scroll slowly and deliberately, so as to make sure that the timing of my clicks wasn't part of the problem.

@jplwill
Copy link
Author

jplwill commented Nov 7, 2022

Thank you!

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

Successfully merging a pull request may close this issue.

2 participants