Open vahidooo opened 1 year ago
For selection, I debugged the code and found out that the problem is in ParagraphText#getRangeShapeSafely
when it called from ParagraphText#updateSingleSelection
. The shape
returned by TextFlowExt#getRangeShape
is based on left.
I checked the ParagraphText. Its nodeOrientation
was null and effectiveNodeOrientation
was RIGHT_TO_LEFT
.
As an initial idea, It seems that the problem could be solved by rotating the shape
around the middle of the ParagraphText (or ParagraphBox) to make it right anchored.
However, it requires that we know the direction of paragraph inside the ParagraphText
. So, we have to add direction to Paragraph
or somewhere else.
Of course, I think it is very common in the word processing application like Microsoft Word to apply direction in the paragraph level. In open xml standard, it defines in paragraph property (see this part of documentation) which is equivalent of paragraph style in our model. By defining direction in paragraphs, we also can go one step forth and handle complex documents those are containing both RTL paragraphs and LTR paragraphs.
I will be glad if you guide me to contribute and resolve this issue by myself and make a push request.
The caret position and selection are tied together, so I think that the caret behavior should be fixed first and hopefully that'll lead to the selection behaving as well.
So the caret shape and position is determined here in TextFlowExt: https://github.com/FXMisc/RichTextFX/blob/040c5e704e60d8b6e00e3803c7fc72dbf896539a/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java#L55-L57
The method caretShape
here is from JavaFX TextFlow. So the first thing to check is if TextFlow is returning the correct shape?
JavaFX TextFlow returns shape of caret in its local coordinates, as mentioned in the documentation. So the shape is independent of the node orientation. It is the same for selection, as well.
Last night I tried another approach. I rolled back the constructor of FoldableStyledArea. So its orientation changed back to the default value, namely LEFT_TO_RIGHT. But I changed the alignment of ParagraphText to TextAlignment.RIGHT. In this way, the problems of caret and selection got solved but the problem related to typing space in the leading position of paragraph(case 5) happened again. In addition, a new problem emerged. The bullets were not aligned to right.
I'm not sure that we should start from node orientation, text alignment, or another point. @Jugen Do you agree with the node orientation approach?
From the JavaDocs it seems that the node orientation approach is the proper way to do it.
It seems to me that caretShape
is returning the incorrect shape for charIdx, specifically it's returning the shape for the charIdx counting from the left and not from the right. If I change the method updateSingleCaret
in ParagraphText to reverse the charIdx then the caret behaves as expected:
private void updateSingleCaret(CaretNode caretNode) {
int charIdx = getClampedCaretPosition(caretNode);
if ( getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT ) {
charIdx = paragraph.length() - charIdx;
}
PathElement[] shape = getCaretShape(charIdx, true);
caretNode.getElements().setAll(shape);
}
Could you verify this observation please. Then same to seems to apply for selection, but that appears more complicated.
@Jugen I truly appreciate all your time spent on my question.
I checked it. There have been a few hiccups along the way.
First of all, ParagraphText#getEffectiveNodeOrientation()
is returning NodeOrientation.LEFT_TO_RIGHT
independently of the node orientation of the area. However I temporary set the node orientation of ParagraphText in its constructor. We'll resolve it later.
Your idea solve the problem related to the direction of caret movement(Problem 2 in the first comment of the discussion) just for LTR texts. For RTL texts, caret is always stuck to the left side of screen.
I should also note that for both RTL and LTR text, the caret is in the left side at the beggining. I mean Problem 1 still happens.
Thanks.
Sorry I've been testing with JavaKeywordsDemo and not RichTextDemo, my bad :-)
In RichTextDemo it seems that wrap text is messing us around as well. If you click the wrap check box to switch wrapping off then it'll behave as expected with my changes. However positioning the cursor by clicking is then broken because TextFlow/HitInfo doesn't seem to return the correct charIdx for hitTest( Point2D ) here: https://github.com/FXMisc/RichTextFX/blob/040c5e704e60d8b6e00e3803c7fc72dbf896539a/richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java#L142-L147 That's the same problem as with caretShape mentioned previously as TextFlow/HitInfo is counting from the left and not from the right, even though it reports the correct NodeOrientation.
Are you sure about this:
First of all, ParagraphText#getEffectiveNodeOrientation() is returning NodeOrientation.LEFT_TO_RIGHT independently of the node orientation of the area.
Don't you mean "getNodeOrientation() is returning LEFT_TO_RIGHT" ? When I check getEffectiveNodeOrientation() it returns RIGHT_TO_LEFT as expected.
(Also just a heads up that unfortunately I don't have a lot of spare time currently so progress on this is going to be slow on my end)
I tested with disabled line wrapping but the result is identical. I had to note that I am using javafx version '18.0.1' on java 17. Does it cause the problem?
I think that the root of the problem is that the CatetNode is child of ParagraphText. If we define a Group and make it the parent of both CaretNode and ParagraphText then the coordination of CaretNode will automatically become revered by changing the node orientation of the group. Because the origin of TextFlow is always on its top-left corner independent of its node orientation.
To demonstrate this difference between Group and TextFlow, I wrote this simple Kotlin snippet:
class JavaFXBidiText : Application() {
override fun start(stage: Stage) {
val font = Font("Tahoma", 48.0)
val text1 = Text("سلام علیکم")
.apply { this.font = font }
val caret1 = Rectangle(0.0, 0.0, 15.0, 60.0)
.apply { fill = Color.RED }
val textFlow1 = TextFlow(text1, caret1)
val text2 = Text("سلام علیکم")
.apply { this.font = font }
val caret2 = Rectangle(0.0, 0.0, 15.0, 60.0)
.apply { fill = Color.BLUE }
val textFlow2 = TextFlow(text2)
val group = Group(textFlow2, caret2)
val vbox = VBox(textFlow1, group)
.apply { nodeOrientation = NodeOrientation.LEFT_TO_RIGHT }
val scene = Scene(vbox, 650.0, 150.0, Color.WHITE)
scene.setOnMouseClicked {
vbox.nodeOrientation =
if (vbox.nodeOrientation == NodeOrientation.LEFT_TO_RIGHT) NodeOrientation.RIGHT_TO_LEFT
else NodeOrientation.LEFT_TO_RIGHT
}
stage.scene = scene
stage.show()
}
}
fun main(args: Array<String>) {
Application.launch(JavaFXBidiText::class.java, *args)
}
Now let's run it. Initially, everything is LTR:
But when I press click, the result is this:
I implemented my idea. I mean wrapping the text flow, caret, and selection into a common parent, namely a Region or Group. In this way, all issues, except the last one, are resolved. @Jugen Could you please take a look at my work?
If my solution is correct and ok with you the next step is solving the problem of trailing space(Problem 5). It seems that it is a bug comming up from the lower layers. I asked a question about this issue on SO. How do you feel about that?
That's all great, thanks for the effort. From SO looks like it may be a Mac thing ?
With regards to your work its hard to see exactly what you have changes as your IDE has auto formatted some stuff. Could you please either provide a version with just your changes (preferred), or make an initial commit with just the IDE changes and then your changes on top of that. Then it'll be far easier to assess. Thanks :-)
I'm sorry for my messy commit. I tidied it up. Could you please take a look at it once again?
It seems that my work solve the selection and caret issues but generate a new bug :) If you change the style of a word, the previous word moves to left. For example, in the following figure, I didn't delete the space between the first and second words. I just set underline for the second word.
By the way, I tested the trailing space problem in Linux. It never happens. You are right.
I just found out that my work also has ruined line wrapping feature, unfortunately. But I still think that it could be a good starting point to solve the RTL issues step by step.
Okay so I've had a look at your proposal and I see what you've done and I think I understand why it works. :-) The line wrap isn't problem, just binding the TextFlow width & height to ParagraphText seems to fix it:
textFlow = new TextFlowExt();
textFlow.prefHeightProperty().bind( heightProperty() );
textFlow.prefWidthProperty().bind( widthProperty() );
With regards to styling: underlining is a special case, can you try strike-through instead and see if it also misbehaves or is it just underlining ?
I also think additional changes besides the ones you've already made need to be made in GenericStyledAreaBehavior so that [Home], [End], and [Ctrl]+[A] also work correctly.
According to your suggestion, line wrapping issue is resolved. I just pushed a new commit.
And about underline, not only underline and strike-through but also other kinds of formatting, e.g. changing text color or bolding, have the same issue, I explained.
There are also some other wrong behaviors. For example, changing background color doesn't make any change. A disaster case happens when applying bold and italic on a single word. It makes a complete mess. letters become changed and broken.
I also had a look at GenericStyledAreaBehavior to correct the aforementioned shortcuts. But couldn't understand where I should start. I need your help.
By the way, I would like to apologize for my poor English.
Of course your solution for line wrapping cause a new issue. The text doesn't adhere to the side of window. If you resize the window from its right side text doesn't move with it.
Okay I'll have a look at that. I started looking at the GenericStyledAreaBehavior issues which led me to a RTL bug in TextFlowLayout which I'll also need to fix. I'm hoping this will also fix some of the other issues you're experiencing, we'll see.
By the way, your English is fine and certainly not poor :-)
This week I have a lot of time. I hope that I could fix the bug. Could you please explain it?
I have fixed the TextFlowLayout issue. Replace method getLineCount
with:
int getLineCount() {
if ( lineCount > -1 ) return lineCount;
lineCount = 0;
lineMetrics.clear();
boolean ltr = NodeOrientation.LEFT_TO_RIGHT == flow.getEffectiveNodeOrientation();
double totLines = 0.0, prevMaxY = -1.0;
int totCharSoFar = 0;
for ( Node n : flow.getChildrenUnmodifiable() ) if ( n.isManaged() ) {
Bounds nodeBounds = n.getBoundsInParent();
int length = (n instanceof Text) ? ((Text) n).getText().length() : 1;
PathElement[] shape = flow.rangeShape( totCharSoFar, totCharSoFar+length );
double lines = Math.max( 1.0, Math.floor( shape.length / 5 ) );
double nodeMinY = Math.max( 0.0, nodeBounds.getMinY() );
if ( lines > 1.0 ) { // Staggered multiline text node
if ( totLines > 0.0 ) lines--;
totLines += lines;
}
else if ( nodeMinY >= prevMaxY ) { // Node is on next line
totLines++;
}
if ( lineMetrics.size() < totLines ) { // Add additional lines
if ( shape.length == 0 ) {
lineMetrics.add( new TextFlowSpan( totCharSoFar, length, nodeMinY, nodeBounds.getWidth(), nodeBounds.getHeight() ) );
totCharSoFar += length;
}
else for ( int ele = (ltr?1:0); ele < shape.length; ele += 5 ) {
double charHeight, lineMinY, segWidth;
Point2D endPoint;
// Calculate the segment's line's length and width up to this point
if ( ltr ) {
LineTo eleLine = (LineTo) shape[ele];
segWidth = eleLine.getX(); lineMinY = eleLine.getY();
charHeight = ((LineTo) shape[ele+1]).getY() - lineMinY;
endPoint = new Point2D( segWidth-1, lineMinY + charHeight / 2 );
} else {
MoveTo eleLine = (MoveTo) shape[ele];
double endX = eleLine.getX(); lineMinY = eleLine.getY();
charHeight = ((LineTo) shape[ele+3]).getY() - lineMinY;
endPoint = new Point2D( endX+1, lineMinY + charHeight / 2 );
segWidth = endX - ((LineTo) shape[ele+1]).getX();
}
// hitTest queries TextFlow layout internally and returns the position of the
// last char (nearest endPoint) on the line, irrespective of the current Text node !
int segLen = flow.hitTest( endPoint ).getCharIndex();
segLen -= totCharSoFar - 1;
if ( ele < 5 && nodeMinY < prevMaxY ) {
if ( ltr ) segWidth -= ((MoveTo) shape[ele-1]).getX();
adjustLineMetrics( segLen, segWidth, charHeight );
}
else {
lineMetrics.add( new TextFlowSpan( totCharSoFar, segLen, lineMinY, segWidth, charHeight ) );
}
totCharSoFar += segLen;
}
}
else {
// Adjust current line metrics with additional Text or Node embedded in this line
adjustLineMetrics( length, nodeBounds.getWidth(), nodeBounds.getHeight() );
totCharSoFar += length;
}
prevMaxY = Math.max( prevMaxY, nodeBounds.getMaxY() );
}
lineCount = (int) totLines;
if ( lineCount > 0 ) return lineCount;
lineCount = -1;
return 0;
}
This fixes [Home] and [End] behavior.
Unfortunately I haven't had time to fix the width resizing problem. It seems that a refresh layout isn't being immediately triggered, but if the text changes then it's updated.
Also have a look at [Ctrl]+[Left Arrow] and [Ctrl]+[Right Arrow] and see if you can reverse their behavior as you did for Left and Right arrow.
I am sorry. Last week I got cold and couldn't spend any time. I just tested your update on getLineCount method. At this time I can find these things:
Many thanks for your time.
Case 3 is resolved by replacing textFlow.prefHeightProperty().bind(heightProperty())
with prefHeightProperty().bind(textFlow.heightProperty())
in the constructor of ParagraphText.
Case 4 is also seems a mac thing. I will test it on Linux.
JDK-8319844 may relate to this ?
The PR for JDK-8319844 has been integrated for FX 23, so we can reassess this after its release when we have time.
Yes, we will!
Hi,
I am working on a text editor project. Our editor is supposed to be able to handle some documents those are entirely RTL. For instance, suppose that our documents are entirely Farsi or Arabic and contain no word from English and other LTR languages.
I cloned RichtextFX project with its demos and started from
org.fxmisc.richtext.demo.richtext.RichTextDemo
. I thought that I can provide RTL support just by changing the node orientation of thearea
. Therefore, I changed the default constructor ofFoldableStyledArea
like the following:But the result isn't that good as it expected. When I start to type, text is being rendered correctly from right to left, but there are some problems.
To be clear, I captured my sample work to demonstrate the above problems. Please see this screen record.
Best, Thanks.