Skip to content

Commit 7be6dc4

Browse files
JFormDesignerJugen
authored andcommitted
ParagraphText: immediately remove listeners from selections and carets on disposal (#791)
* ParagraphText: immediately remove listeners from SelectionPath and Caret on dispose instead of waiting for GC (this improves PR #779, issue #627) * ParagraphText: since weak listeners are no longer required for selection and caret listeners, remove listener classes again and move listener code to constructor (saves 100 lines of code) * ParagraphText: MapChangeListener.Change.wasAdded() and wasRemoved() may both return true when replacing an item. So check both and handle wasRemoved() before wasAdded(). * Fixed (rare) NPE in class ParagraphText when GC frees object between two WeakReference.get() invocations
1 parent 74efc78 commit 7be6dc4

3 files changed

Lines changed: 54 additions & 132 deletions

File tree

richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,7 @@ public void dispose() {
14631463
box.wrapTextProperty().unbind();
14641464
box.graphicFactoryProperty().unbind();
14651465
box.graphicOffset.unbind();
1466+
box.dispose();
14661467

14671468
firstParPseudoClass.unsubscribe();
14681469
lastParPseudoClass.unsubscribe();

richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import javafx.geometry.Insets;
2323
import javafx.geometry.Point2D;
2424
import javafx.scene.Node;
25-
import javafx.scene.control.IndexRange;
2625
import javafx.scene.layout.Region;
2726
import javafx.scene.paint.Paint;
2827
import javafx.scene.text.TextFlow;
@@ -118,6 +117,10 @@ public final ObservableMap<Selection<PS, SEG, S>, SelectionPath> selectionsPrope
118117
graphicOffset.addListener(obs -> requestLayout());
119118
}
120119

120+
void dispose() {
121+
text.dispose();
122+
}
123+
121124
@Override
122125
public String toString() {
123126
return String.format(

richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java

Lines changed: 49 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.fxmisc.richtext;
22

3-
import java.lang.ref.WeakReference;
43
import java.util.Arrays;
54
import java.util.Collection;
65
import java.util.HashMap;
@@ -19,7 +18,6 @@
1918
import javafx.beans.property.ObjectProperty;
2019
import javafx.beans.property.SimpleObjectProperty;
2120
import javafx.beans.value.ChangeListener;
22-
import javafx.beans.value.ObservableValue;
2321
import javafx.collections.FXCollections;
2422
import javafx.collections.MapChangeListener;
2523
import javafx.collections.ObservableMap;
@@ -62,6 +60,9 @@ class ParagraphText<PS, SEG, S> extends TextFlowExt {
6260
FXCollections.observableMap(new HashMap<>(1));
6361
public final ObservableMap<Selection<PS, SEG, S>, SelectionPath> selectionsProperty() { return selections; }
6462

63+
private final ChangeListener<IndexRange> selectionRangeListener;
64+
private final ChangeListener<Integer> caretPositionListener;
65+
6566
// FIXME: changing it currently has not effect, because
6667
// Text.impl_selectionFillProperty().set(newFill) doesn't work
6768
// properly for Text node inside a TextFlow (as of JDK8-b100).
@@ -95,11 +96,47 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
9596
Val<Double> leftInset = Val.map(insetsProperty(), Insets::getLeft);
9697
Val<Double> topInset = Val.map(insetsProperty(), Insets::getTop);
9798

98-
ChangeListener<IndexRange> requestLayout1 = new SelectionRangeChangeListener<>(this);
99-
selections.addListener(new SelectionsSetListener<>(leftInset, requestLayout1, topInset, this));
99+
selectionRangeListener = (obs, ov, nv) -> requestLayout();
100+
selections.addListener((MapChangeListener.Change<? extends Selection<PS, SEG, S>, ? extends SelectionPath> change) -> {
101+
if (change.wasRemoved()) {
102+
SelectionPath p = change.getValueRemoved();
103+
p.rangeProperty().removeListener(selectionRangeListener);
104+
p.layoutXProperty().unbind();
105+
p.layoutYProperty().unbind();
100106

101-
ChangeListener<Integer> requestLayout2 = new CaretPositionChangeListener<>(this);
102-
carets.addListener(new CaretsChangeListener<>(leftInset, requestLayout2, topInset, this));
107+
getChildren().remove(p);
108+
}
109+
if (change.wasAdded()) {
110+
SelectionPath p = change.getValueAdded();
111+
p.rangeProperty().addListener(selectionRangeListener);
112+
p.layoutXProperty().bind(leftInset);
113+
p.layoutYProperty().bind(topInset);
114+
115+
getChildren().add(selectionShapeStartIndex, p);
116+
updateSingleSelection(p);
117+
}
118+
});
119+
120+
caretPositionListener = (obs, ov, nv) -> requestLayout();
121+
carets.addListener((SetChangeListener.Change<? extends CaretNode> change) -> {
122+
if (change.wasRemoved()) {
123+
CaretNode caret = change.getElementRemoved();
124+
caret.columnPositionProperty().removeListener(caretPositionListener);
125+
caret.layoutXProperty().unbind();
126+
caret.layoutYProperty().unbind();
127+
128+
getChildren().remove(caret);
129+
}
130+
if (change.wasAdded()) {
131+
CaretNode caret = change.getElementAdded();
132+
caret.columnPositionProperty().addListener(caretPositionListener);
133+
caret.layoutXProperty().bind(leftInset);
134+
caret.layoutYProperty().bind(topInset);
135+
136+
getChildren().add(caret);
137+
updateSingleCaret(caret);
138+
}
139+
});
103140

104141
// XXX: see the note at highlightTextFill
105142
// highlightTextFill.addListener(new ChangeListener<Paint>() {
@@ -182,6 +219,12 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
182219
);
183220
}
184221

222+
void dispose() {
223+
// this removes listeners (in selections and carets listeners) and avoids memory leaks
224+
selections.clear();
225+
carets.clear();
226+
}
227+
185228
public Paragraph<PS, SEG, S> getParagraph() {
186229
return paragraph;
187230
}
@@ -402,131 +445,6 @@ protected void layoutChildren() {
402445
updateBackgroundShapes();
403446
}
404447

405-
private static final class SelectionsSetListener<PS, SEG, S> implements
406-
MapChangeListener<Selection<PS, SEG, S>, SelectionPath> {
407-
private final Val<Double> leftInset;
408-
private final ChangeListener<IndexRange> requestLayout1;
409-
private final Val<Double> topInset;
410-
private final WeakReference<ParagraphText<PS, SEG, S>> ref;
411-
412-
public SelectionsSetListener(
413-
Val<Double> leftInset,
414-
ChangeListener<IndexRange> requestLayout1,
415-
Val<Double> topInset,
416-
ParagraphText<PS, SEG, S> paragraphText) {
417-
this.leftInset = leftInset;
418-
this.requestLayout1 = requestLayout1;
419-
this.topInset = topInset;
420-
ref = new WeakReference<>(paragraphText);
421-
}
422-
423-
@Override
424-
public void onChanged(
425-
javafx.collections.MapChangeListener.Change<? extends Selection<PS, SEG, S>, ? extends SelectionPath> change) {
426-
ParagraphText<PS, SEG, S> paragraphText = ref.get();
427-
if (null == paragraphText) {
428-
change.getMap().removeListener(this);
429-
return;
430-
}
431-
432-
if (change.wasAdded()) {
433-
SelectionPath p = change.getValueAdded();
434-
p.rangeProperty().addListener(requestLayout1);
435-
436-
p.layoutXProperty().bind(leftInset);
437-
p.layoutYProperty().bind(topInset);
438-
439-
paragraphText.getChildren().add(paragraphText.selectionShapeStartIndex, p);
440-
paragraphText.updateSingleSelection(p);
441-
} else if (change.wasRemoved()) {
442-
SelectionPath p = change.getValueRemoved();
443-
p.rangeProperty().removeListener(requestLayout1);
444-
445-
p.layoutXProperty().unbind();
446-
p.layoutYProperty().unbind();
447-
448-
paragraphText.getChildren().remove(p);
449-
}
450-
}
451-
}
452-
453-
private static final class SelectionRangeChangeListener<PS, SEG, S> implements ChangeListener<IndexRange> {
454-
private final WeakReference<ParagraphText<PS, SEG, S>> ref;
455-
456-
public SelectionRangeChangeListener(ParagraphText<PS, SEG, S> paragraphText) {
457-
ref = new WeakReference<>(paragraphText);
458-
}
459-
460-
@Override
461-
public void changed(ObservableValue<? extends IndexRange> observable, IndexRange oldValue, IndexRange newValue) {
462-
if (null == ref.get()) {
463-
observable.removeListener(this);
464-
} else {
465-
ref.get().requestLayout();
466-
}
467-
}
468-
}
469-
470-
private static final class CaretPositionChangeListener<PS, SEG, S> implements ChangeListener<Integer> {
471-
private final WeakReference<ParagraphText<PS, SEG, S>> ref;
472-
473-
public CaretPositionChangeListener(ParagraphText<PS, SEG, S> paragraphText) {
474-
ref = new WeakReference<>(paragraphText);
475-
}
476-
477-
@Override
478-
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue) {
479-
if (null == ref.get()) {
480-
observable.removeListener(this);
481-
} else {
482-
ref.get().requestLayout();
483-
}
484-
}
485-
}
486-
487-
private static final class CaretsChangeListener<PS, SEG, S> implements SetChangeListener<CaretNode> {
488-
private final Val<Double> leftInset;
489-
private final ChangeListener<Integer> requestLayout2;
490-
private final Val<Double> topInset;
491-
private final WeakReference<ParagraphText<PS, SEG, S>> ref;
492-
493-
private CaretsChangeListener(
494-
Val<Double> leftInset,
495-
ChangeListener<Integer> requestLayout2,
496-
Val<Double> topInset,
497-
ParagraphText<PS, SEG, S> paragraphText) {
498-
ref = new WeakReference<>(paragraphText);
499-
this.leftInset = leftInset;
500-
this.requestLayout2 = requestLayout2;
501-
this.topInset = topInset;
502-
}
503-
504-
@Override
505-
public void onChanged(Change<? extends CaretNode> change) {
506-
ParagraphText<PS, SEG, S> paragraphText = ref.get();
507-
if (null == paragraphText) {
508-
change.getSet().removeListener(this);
509-
return;
510-
}
511-
if (change.wasAdded()) {
512-
CaretNode caret = change.getElementAdded();
513-
caret.columnPositionProperty().addListener(requestLayout2);
514-
caret.layoutXProperty().bind(leftInset);
515-
caret.layoutYProperty().bind(topInset);
516-
517-
paragraphText.getChildren().add(caret);
518-
paragraphText.updateSingleCaret(caret);
519-
} else if (change.wasRemoved()) {
520-
CaretNode caret = change.getElementRemoved();
521-
caret.columnPositionProperty().removeListener(requestLayout2);
522-
caret.layoutXProperty().unbind();
523-
caret.layoutYProperty().unbind();
524-
525-
paragraphText.getChildren().remove(caret);
526-
}
527-
}
528-
}
529-
530448
private static class CustomCssShapeHelper<T> {
531449

532450
private final List<Tuple2<T, IndexRange>> ranges = new LinkedList<>();

0 commit comments

Comments
 (0)