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

Option to show a split view #152

Closed
JordanMartinez opened this issue Jun 17, 2015 · 13 comments
Closed

Option to show a split view #152

JordanMartinez opened this issue Jun 17, 2015 · 13 comments

Comments

@JordanMartinez
Copy link
Contributor

Hello Tomas,

How could I split a rich text area (whatever flavor) into two views, something that is typically done in a Word processor?
There are two parts to this:

  1. Two views display a portion of the same document.
  2. A change in the document will update both views to display this update

As of now, I could do the first by adding two rich text areas to a Split Pane, but how would I do the second? If I add two areas to a Split Pane, I could have them listen to each other for any updates. This is probably inefficient if the area contains a lot of text, styles, etc, but it provides a work around until a better solution is provided.

@ghost
Copy link

ghost commented Jun 17, 2015

Hi Jordan,
I did something like that too, by making the text area in focus be the one driving changes in the other. I listen to position of caret and push key entries from the one in focus to the other one. It did not give always the correct sequence of key characters though when I typed quickly .. Probably has something to do with threads (something above my level of programming).
It was OK to show an idea I wanted to explore. But certainly not desirable performance...
Maher
Sent from my Cyanogen phone
On Jun 16, 2015 9:29 PM, JordanMartinez notifications@github.com wrote:Hello Tomas,

How could I split a rich text area (whatever flavor) into two views, something that is typically done in a Word processor?
There are two parts to this:

  1. Two views display a portion of the same document.
  2. A change in the document will update both views to display this update

As of now, I could do the first by adding two rich text areas to a Split Pane, but how would I do the second? If I add two areas to a Split Pane, I could have them listen to each other for any updates. This is probably inefficient if the area contains a lot of text, styles, etc, but it provides a work around until a better solution is provided.

—Reply to this email directly or view it on GitHub.

@TomasMikula
Copy link
Member

Hi Jordan,

I think the best you can do today is as you suggest - have the two text areas listen to each other. It is not necessarily inefficient, if you observe and re-do the individual changes. Here you can see how to apply a change generated by one text area to the other one.
You will have to take care of ignoring changes from a text area while you are applying changes to it, so that you don't get an infinite loop.

A cleaner (but unsupported as of now) solution would be to let the two areas share the same underlying StyledDocument. StyledDocument is a data model for the text area. Each text area constructs it's own private instance of a StyledDocument. It would be easy to add StyledTextArea constructors that accept an editable StyledDocument as an argument, and then use the same document for two different areas. The problem is that a StyledTextArea assumes that it is the exclusive owner of the document, i.e. no one else is making changes to it. External modifications would mess up the selection and caret position, which are kept track by the area (not by the document). For example, the caret position stored in one area may even become out of the document's bounds if the other area deleted some parts of the document. Lifting the assumption of exclusive ownership and adjusting the selection and caret position accordingly upon external changes would be a desirable thing to have.

@JordanMartinez
Copy link
Contributor Author

@melkhaldi Thanks for the suggestion. Threads are above my level of programming as well. Maybe they won't be some day. :-) A Cyanogen phone! O.o

@TomasMikula Thanks for the guidance. So, the quick and dirty solution is to do what I've described, but the better solution (and one that contributes to this project) would be to create a pull request that adds this support, correct?

@TomasMikula
Copy link
Member

Yes, that is exactly correct ;)

@JordanMartinez
Copy link
Contributor Author

If someone else doesn't beat me to this, I'd like to do it. However, I'm still writing other core parts of my program. This feature isn't absolutely necessary to my program, but it would definitely be convenient to have. :-)

@JordanMartinez
Copy link
Contributor Author

So, to complete this, we'd need to:

  • add a constructor that accepts an editable StyledDocument
  • move caret ownership from StyledTextArea to StyleDocument
  • add a convenience method createClone() that returns a new StyledTextArea that is bound to the same document to which this one is bound.

@JordanMartinez
Copy link
Contributor Author

StyledDocument is a data model for the text area. Each text area constructs it's own private instance of a StyledDocument. It would be easy to add StyledTextArea constructors that accept an editable StyledDocument as an argument, and then use the same document for two different areas.

Aye. I did this in a separate branch, and it wasn't difficult at all.

The problem is that a StyledTextArea assumes that it is the exclusive owner of the document, i.e. no one else is making changes to it. External modifications [emphasis mine] would mess up the selection and caret position, which are kept track by the area (not by the document). For example, the caret position stored in one area may even become out of the document's bounds if the other area deleted some parts of the document. Lifting the assumption of exclusive ownership and adjusting the selection and caret position accordingly upon external changes would be a desirable thing to have.

This is me thinking outloud:

The area can be changed in two situations: 1) via user interaction and 2) via modifications made by code. I'm assuming your comment about "external modification" means the 2nd case as only one area could have the focus (and thus user inputs) at a time.
If the EditableStyledDocument doesn't keep track of the caret, then modifications can have unexpected results:

// length == 30
String areaText = "Just some text for an example." 
// both carets are positioned at 0
StyledTextArea area = // creation code; 
StyledTextArea clone = area.createClone(); 

// area's caret repositioned to 30
area.positionCaret(area.getLength());
// deletes all text
clone.replaceText(0, clone.getLength(), "");
// clone's caret == 0
// area's caretPosition should be 0 but is 30

// length == 12
String someText = "someText";
// assumed result is that area displays: areaText + "someText"
area.insertText(area.getCaretPosition(), someText);
// actual result: area and clone both display "someText"
//  and area's caret position == 12. 

It could be problematic for the EditableStyledDocument to keep track of only one caret position to which its StyledTextAreas are bound. If a user was interacting (typing, deleting, etc.) in area when clone does some sort of modification, it would surprise the user. However, when that happens, the developer writing that code would also write the code to deal with that scenario.
Also, EditableStyledDocument should be the only StyledDocument instance that actually keeps track of the caret position and selection range as opposed to ReadOnlyStyledDocument.

@TomasMikula
Copy link
Member

The area can be changed in two situations: 1) via user interaction and 2) via modifications made by code. I'm assuming your comment about "external modification" means the 2nd case as only one area could have the focus (and thus user inputs) at a time.

By "external modification", I meant modification of the underlying EditableStyledDocument by the other area, whether via user interaction or code.

What I thought of as a solution was that the area would listen to changes on the EditableStyledDocument and adjust the caret position/selection appropriately when necessary.

  • move caret ownership from StyledTextArea to StyleDocument

I think each area should have its own caret position and selection, don't you think?

@JordanMartinez
Copy link
Contributor Author

By "external modification", I meant modification of the underlying EditableStyledDocument by the other area, whether via user interaction or code.
What I thought of as a solution was that the area would listen to changes on the EditableStyledDocument and adjust the caret position/selection appropriately when necessary.

Ah........ ok, well I think I was reaching that conclusion with my recent commit in the PR ;-)
Yeah, each area should.

@JordanMartinez
Copy link
Contributor Author

After thinking about this more, I'm not sure why I went in the original direction I did...

Let's GTD this:
Purpose: 2+ StyledTextAreas (Views) share the same EditableStyledDocument (Model)
Principles:

  • each area has its own independent caret and selection values
  • each area's caret and selection values are always within the document's bounds regardless of edits

Outcome: any modifications (via user or code) to the shared EditableStyledDocument will update each area's caret and selection values so that they still refer to the same place as before.

Where

  • Area 1's caret is "|" and its selection "[selection]"
  • Area 2's caret is "^" and its selection "(selection)"
  • Area 3's caret is "#" and its selection is {selection}"

Given a document with the following text:

Some text with [so|me] length
that (spa^ns) over
multiple {lines#}.

When Area 2 deletes it's selection, the text should be updated accordingly:

Some text with [so|me] length
that ^ over  // Area 2's selection is now empty
multiple {lines#}.

Brainstorm:

One way to update the caret and selection values is by calculating the index where a chance occurs and how much to offset an Area's caret and selection values. If an Area's caret or its selection's end is in front of this index, then both don't need to be changed. Otherwise, each needs to be offset by some amount. This offset amount (second) is the difference of the text's length before the change and after the change.

// given these values
int indexOfChange = ...;
int changeInAreaLength = ...;

// and knowing the area's caret and selection values
int areaCaretPosition = ...;
int areaSelectionFrom = ...;
int areaSelectionTo = ...;

// then offset the area's caret and selection values if needed
if (changeInAreaLength != 0) {
    if (indexOfChange < areaCaretPosition) {
        area.setCaretPosition(areaCaretPosition + changeInAreaLength);
    }
    area.selectRange( 
        indexOfChange < areaSelectionFrom 
            ? areaSelectionFrom + changeInAreaLength 
            : areaSelectionFrom,
        indexOfChange < areaSelectionTo 
            ? areaSelectionTo + changeInAreaLength 
            : areaSelectionTo
    );
}

@TomasMikula
Copy link
Member

That will work when the change is outside of selection/caret position. When, for example, the caret is at position 8, and a change occurs that deletes the range 6-10, the caret should be moved to position 6, but the above code would move it to position 4.

@JordanMartinez
Copy link
Contributor Author

Good point. What about changing the code to this then?

// cloneUpdatesToDoc should emit its Event when this area is
// not the view making the change to the shared EditableStyledDocument
EventStream<PlainTextChange> cloneUpdatesToDoc = ...;
cloneUpdatesToDoc.subscribe( plainTextChange -> {
    int changeLength = plainTextChange.getInserted().length() - plainTextChange.getRemoved().length();
    if (changeLength != 0) {
        int indexOfChange = plainTextChange.getPosition();
        // in case of a replacement: "hello there" -> "hi."
        int endOfChange = indexOfChange + Math.abs(changeLength);

        // update caret
        int caretPosition = getCaretPosition();
        if (indexOfChange < caretPosition) {
            // if caret is within the changed content, move it to indexOfChange
            // otherwise offset it by changeLength
            positionCaret(
                caretPosition < endOfChange
                    ? indexOfChange
                    : caretPosition + changeLength
            );
        }
        // update selection
        int selectionStart = getSelection().getStart();
        int selectionEnd = getSelection().getEnd();
        if (selectionStart != selectionEnd) {
            // if start/end is within the changed content, move it to indexOfChange
            // otherwise, offset it by changeLength
            // Note: if both are moved to indexOfChange, selection is empty.
            if (indexOfChange < selectionStart) {
                selectionStart = selectionStart < endOfChange
                        ? indexOfChange
                        : selectionStart + changeLength;
            }
            if (indexOfChange < selectionEnd) {
                selectionEnd = selectionEnd < endOfChange
                        ? indexOfChange
                        : selectionEnd + changeLength;
            }
            selectRange(selectionStart, selectionEnd);
        }
    }
});

@JordanMartinez
Copy link
Contributor Author

Resolved via 87c2b30

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

No branches or pull requests

2 participants