Skip to content

Commit 99770e5

Browse files
Merge pull request #550 from JordanMartinez/hyperlinkDemo
Add Hyperlink demo - minimum needed for custom object integration
2 parents b5a167d + 116a661 commit 99770e5

6 files changed

Lines changed: 579 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.fxmisc.richtext.demo.hyperlink;
2+
3+
public class Hyperlink<S> {
4+
5+
private final String originalDisplayedText;
6+
private final String displayedText;
7+
private final S style;
8+
private final String link;
9+
10+
Hyperlink(String originalDisplayedText, String displayedText, S style, String link) {
11+
this.originalDisplayedText = originalDisplayedText;
12+
this.displayedText = displayedText;
13+
this.style = style;
14+
this.link = link;
15+
}
16+
17+
public boolean isEmpty() {
18+
return length() == 0;
19+
}
20+
21+
public boolean isReal() {
22+
return length() > 0;
23+
}
24+
25+
public boolean shareSameAncestor(Hyperlink<S> other) {
26+
return link.equals(other.link) && originalDisplayedText.equals(other.originalDisplayedText);
27+
}
28+
29+
public int length() {
30+
return displayedText.length();
31+
}
32+
33+
public char charAt(int index) {
34+
return isEmpty() ? '\0' : displayedText.charAt(index);
35+
}
36+
37+
public String getOriginalDisplayedText() { return originalDisplayedText; }
38+
39+
public String getDisplayedText() {
40+
return displayedText;
41+
}
42+
43+
public String getLink() {
44+
return link;
45+
}
46+
47+
public Hyperlink<S> subSequence(int start, int end) {
48+
return new Hyperlink<>(originalDisplayedText, displayedText.substring(start, end), style, link);
49+
}
50+
51+
public Hyperlink<S> subSequence(int start) {
52+
return new Hyperlink<>(originalDisplayedText, displayedText.substring(start), style, link);
53+
}
54+
55+
public S getStyle() {
56+
return style;
57+
}
58+
59+
public Hyperlink<S> setStyle(S style) {
60+
return new Hyperlink<>(originalDisplayedText, displayedText, style, link);
61+
}
62+
63+
public Hyperlink<S> mapDisplayedText(String text) {
64+
return new Hyperlink<>(originalDisplayedText, text, style, link);
65+
}
66+
67+
@Override
68+
public String toString() {
69+
return isEmpty()
70+
? String.format("EmptyHyperlink[original=%s style=%s link=%s]", originalDisplayedText, style, link)
71+
: String.format("RealHyperlink[original=%s displayedText=%s, style=%s, link=%s]",
72+
originalDisplayedText, displayedText, style, link);
73+
}
74+
75+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.fxmisc.richtext.demo.hyperlink;
2+
3+
import com.sun.deploy.uitoolkit.impl.fx.HostServicesFactory;
4+
import com.sun.javafx.application.HostServicesDelegate;
5+
import javafx.application.Application;
6+
import javafx.application.HostServices;
7+
import javafx.scene.Scene;
8+
import javafx.stage.Stage;
9+
import org.fxmisc.flowless.VirtualizedScrollPane;
10+
11+
import java.util.function.Consumer;
12+
13+
/**
14+
* Demonstrates the minimum needed to support custom objects (in this case, hyperlinks) alongside of text.
15+
*
16+
* Note: demo does not handle cases where the link changes its state when it has already been visited
17+
*/
18+
public class HyperlinkDemo extends Application {
19+
20+
public static void main(String[] args) {
21+
launch(args);
22+
}
23+
24+
@Override
25+
public void start(Stage primaryStage) {
26+
Consumer<String> showLink = HostServicesFactory.getInstance(this)::showDocument;
27+
TextHyperlinkArea area = new TextHyperlinkArea(showLink);
28+
29+
area.appendText("Some text in the area\n");
30+
area.appendWithLink("Google.com", "http://www.google.com");
31+
32+
VirtualizedScrollPane<TextHyperlinkArea> vsPane = new VirtualizedScrollPane<>(area);
33+
34+
Scene scene = new Scene(vsPane, 500, 500);
35+
primaryStage.setScene(scene);
36+
primaryStage.show();
37+
}
38+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package org.fxmisc.richtext.demo.hyperlink;
2+
3+
import org.fxmisc.richtext.model.SegmentOps;
4+
5+
import java.util.Optional;
6+
7+
public class HyperlinkOps<S> implements SegmentOps<Hyperlink<S>, S> {
8+
9+
@Override
10+
public int length(Hyperlink<S> hyperlink) {
11+
return hyperlink.length();
12+
}
13+
14+
@Override
15+
public char charAt(Hyperlink<S> hyperlink, int index) {
16+
return hyperlink.charAt(index);
17+
}
18+
19+
@Override
20+
public String getText(Hyperlink<S> hyperlink) {
21+
return hyperlink.getDisplayedText();
22+
}
23+
24+
@Override
25+
public Hyperlink<S> subSequence(Hyperlink<S> hyperlink, int start, int end) {
26+
return hyperlink.subSequence(start, end);
27+
}
28+
29+
@Override
30+
public Hyperlink<S> subSequence(Hyperlink<S> hyperlink, int start) {
31+
return hyperlink.subSequence(start);
32+
}
33+
34+
@Override
35+
public S getStyle(Hyperlink<S> hyperlink) {
36+
return hyperlink.getStyle();
37+
}
38+
39+
@Override
40+
public Hyperlink<S> setStyle(Hyperlink<S> hyperlink, S style) {
41+
return hyperlink.setStyle(style);
42+
}
43+
44+
@Override
45+
public Optional<Hyperlink<S>> join(Hyperlink<S> currentSeg, Hyperlink<S> nextSeg) {
46+
if (currentSeg.isEmpty()) {
47+
if (nextSeg.isEmpty()) {
48+
return Optional.empty();
49+
} else {
50+
return Optional.of(nextSeg);
51+
}
52+
} else {
53+
if (nextSeg.isEmpty()) {
54+
return Optional.of(currentSeg);
55+
} else {
56+
return concatHyperlinks(currentSeg, nextSeg);
57+
}
58+
}
59+
}
60+
61+
private Optional<Hyperlink<S>> concatHyperlinks(Hyperlink<S> leftSeg, Hyperlink<S> rightSeg) {
62+
if (!leftSeg.shareSameAncestor(rightSeg)) {
63+
return Optional.empty();
64+
}
65+
66+
String original = leftSeg.getOriginalDisplayedText();
67+
String leftText = leftSeg.getDisplayedText();
68+
String rightText = rightSeg.getDisplayedText();
69+
int leftOffset = 0;
70+
int rightOffset = 0;
71+
for (int i = 0; i <= original.length() - leftText.length(); i++) {
72+
if (original.regionMatches(i, leftText, 0, leftText.length())) {
73+
leftOffset = i;
74+
break;
75+
}
76+
}
77+
for (int i = 0; i <= original.length() - rightText.length(); i++) {
78+
if (original.regionMatches(i, rightText, 0, rightText.length())) {
79+
rightOffset = i;
80+
break;
81+
}
82+
}
83+
84+
if (rightOffset + rightText.length() == leftOffset) {
85+
return Optional.of(leftSeg.mapDisplayedText(rightText + leftText));
86+
} else if (leftOffset + leftText.length() == rightOffset) {
87+
return Optional.of(leftSeg.mapDisplayedText(leftText + rightText));
88+
} else {
89+
return Optional.empty();
90+
}
91+
}
92+
93+
@Override
94+
public Hyperlink<S> createEmpty() {
95+
return new Hyperlink<>("", "", null, "");
96+
}
97+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.fxmisc.richtext.demo.hyperlink;
2+
3+
import javafx.geometry.VPos;
4+
import org.fxmisc.richtext.GenericStyledArea;
5+
import org.fxmisc.richtext.TextExt;
6+
import org.fxmisc.richtext.model.ReadOnlyStyledDocument;
7+
import org.fxmisc.richtext.model.StyledText;
8+
import org.fxmisc.richtext.model.TextOps;
9+
import org.reactfx.util.Either;
10+
11+
import java.util.function.Consumer;
12+
13+
public class TextHyperlinkArea extends GenericStyledArea<Void, Either<StyledText<TextStyle>, Hyperlink<TextStyle>>, TextStyle> {
14+
15+
private static final TextOps<StyledText<TextStyle>, TextStyle> STYLED_TEXT_OPS = StyledText.textOps();
16+
private static final HyperlinkOps<TextStyle> HYPERLINK_OPS = new HyperlinkOps<>();
17+
private static final TextOps<Either<StyledText<TextStyle>, Hyperlink<TextStyle>>, TextStyle> EITHER_OPS = STYLED_TEXT_OPS._or(HYPERLINK_OPS);
18+
19+
public TextHyperlinkArea(Consumer<String> showLink) {
20+
super(
21+
null,
22+
(t, p) -> {},
23+
TextStyle.EMPTY,
24+
EITHER_OPS,
25+
e -> e.unify(
26+
styledText ->
27+
createStyledTextNode(t -> {
28+
t.setText(styledText.getText());
29+
t.setStyle(styledText.getStyle().toCss());
30+
}),
31+
hyperlink ->
32+
createStyledTextNode(t -> {
33+
if (hyperlink.isReal()) {
34+
t.setText(hyperlink.getDisplayedText());
35+
t.getStyleClass().add("hyperlink");
36+
t.setOnMouseClicked(ae -> showLink.accept(hyperlink.getLink()));
37+
}
38+
})
39+
)
40+
);
41+
42+
getStyleClass().add("text-hyperlink-area");
43+
getStylesheets().add(TextHyperlinkArea.class.getResource("text-hyperlink-area.css").toExternalForm());
44+
}
45+
46+
public void appendWithLink(String displayedText, String link) {
47+
replaceWithLink(getLength(), getLength(), displayedText, link);
48+
}
49+
50+
public void replaceWithLink(int start, int end, String displayedText, String link) {
51+
replace(start, end, ReadOnlyStyledDocument.fromSegment(
52+
Either.right(new Hyperlink<>(displayedText, displayedText, TextStyle.EMPTY, link)),
53+
null,
54+
TextStyle.EMPTY,
55+
EITHER_OPS
56+
));
57+
}
58+
59+
public static TextExt createStyledTextNode(Consumer<TextExt> applySegment) {
60+
TextExt t = new TextExt();
61+
t.setTextOrigin(VPos.TOP);
62+
applySegment.accept(t);
63+
64+
// XXX: binding selectionFill to textFill,
65+
// see the note at highlightTextFill
66+
t.impl_selectionFillProperty().bind(t.fillProperty());
67+
return t;
68+
}
69+
}

0 commit comments

Comments
 (0)