Skip to content

Commit 37fe88c

Browse files
authored
Paragraph Folding (#965)
1 parent fc1aa59 commit 37fe88c

10 files changed

Lines changed: 657 additions & 58 deletions

File tree

richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/JavaKeywordsDemo.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import javafx.application.Application;
1212
import javafx.application.Platform;
1313
import javafx.scene.Scene;
14+
import javafx.scene.control.ContextMenu;
15+
import javafx.scene.control.MenuItem;
1416
import javafx.scene.input.KeyCode;
1517
import javafx.scene.input.KeyEvent;
1618
import javafx.scene.layout.StackPane;
@@ -20,6 +22,7 @@
2022
import org.fxmisc.richtext.CodeArea;
2123
import org.fxmisc.richtext.GenericStyledArea;
2224
import org.fxmisc.richtext.LineNumberFactory;
25+
import org.fxmisc.richtext.model.Paragraph;
2326
import org.fxmisc.richtext.model.StyleSpans;
2427
import org.fxmisc.richtext.model.StyleSpansBuilder;
2528
import org.reactfx.collection.ListModification;
@@ -92,6 +95,7 @@ public void start(Stage primaryStage) {
9295

9396
// add line numbers to the left of area
9497
codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
98+
codeArea.setContextMenu( new DefaultContextMenu() );
9599
/*
96100
// recompute the syntax highlighting for all text, 500 ms after user stops editing area
97101
Subscription cleanupWhenNoLongerNeedIt = codeArea
@@ -161,7 +165,7 @@ private StyleSpans<Collection<String>> computeHighlighting(String text) {
161165
return spansBuilder.create();
162166
}
163167

164-
private class VisibleParagraphStyler<PS, SEG, S> implements Consumer<ListModification>
168+
private class VisibleParagraphStyler<PS, SEG, S> implements Consumer<ListModification<? extends Paragraph<PS, SEG, S>>>
165169
{
166170
private final GenericStyledArea<PS, SEG, S> area;
167171
private final Function<String,StyleSpans<S>> computeStyles;
@@ -174,7 +178,7 @@ public VisibleParagraphStyler( GenericStyledArea<PS, SEG, S> area, Function<Stri
174178
}
175179

176180
@Override
177-
public void accept( ListModification lm )
181+
public void accept( ListModification<? extends Paragraph<PS, SEG, S>> lm )
178182
{
179183
if ( lm.getAddedSize() > 0 )
180184
{
@@ -192,4 +196,41 @@ public void accept( ListModification lm )
192196
}
193197
}
194198

199+
private class DefaultContextMenu extends ContextMenu
200+
{
201+
private MenuItem fold, unfold, print;
202+
203+
public DefaultContextMenu()
204+
{
205+
fold = new MenuItem( "Fold selected text" );
206+
fold.setOnAction( AE -> { hide(); fold(); } );
207+
208+
unfold = new MenuItem( "Unfold from cursor" );
209+
unfold.setOnAction( AE -> { hide(); unfold(); } );
210+
211+
print = new MenuItem( "Print" );
212+
print.setOnAction( AE -> { hide(); print(); } );
213+
214+
getItems().addAll( fold, unfold, print );
215+
}
216+
217+
/**
218+
* Folds multiple lines of selected text, only showing the first line and hiding the rest.
219+
*/
220+
private void fold() {
221+
((CodeArea) getOwnerNode()).foldSelectedParagraphs();
222+
}
223+
224+
/**
225+
* Unfold the CURRENT line/paragraph if it has a fold.
226+
*/
227+
private void unfold() {
228+
CodeArea area = (CodeArea) getOwnerNode();
229+
area.unfoldParagraphs( area.getCurrentParagraph() );
230+
}
231+
232+
private void print() {
233+
System.out.println( ((CodeArea) getOwnerNode()).getText() );
234+
}
235+
}
195236
}

richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/BulletFactory.java

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,68 @@
22

33
import java.util.function.IntFunction;
44

5-
import org.fxmisc.richtext.GenericStyledArea;
6-
5+
import javafx.application.Platform;
76
import javafx.geometry.Insets;
7+
import javafx.geometry.Pos;
8+
import javafx.scene.Cursor;
89
import javafx.scene.Node;
910
import javafx.scene.control.ContentDisplay;
1011
import javafx.scene.control.Label;
12+
import javafx.scene.layout.Priority;
1113
import javafx.scene.layout.VBox;
1214
import javafx.scene.paint.Color;
1315
import javafx.scene.shape.Circle;
1416
import javafx.scene.shape.Rectangle;
17+
import javafx.scene.text.Font;
18+
import javafx.scene.text.FontPosture;
19+
20+
import org.fxmisc.richtext.demo.richtext.RichTextDemo.FoldableStyledArea;
1521

1622
public class BulletFactory implements IntFunction<Node>
1723
{
18-
private GenericStyledArea<ParStyle,?,?> area;
19-
20-
public BulletFactory( GenericStyledArea<ParStyle,?,?> area )
24+
private FoldableStyledArea area;
25+
26+
private static final Font DEFAULT_FONT = Font.font("monospace", FontPosture.ITALIC, 13);
27+
28+
public BulletFactory( FoldableStyledArea area )
2129
{
30+
area.getParagraphs().sizeProperty().addListener( (ob,ov,nv) -> {
31+
if ( nv <= ov ) Platform.runLater( () -> deleteParagraphCheck() );
32+
else Platform.runLater( () -> insertParagraphCheck() );
33+
});
2234
this.area = area;
23-
}
35+
}
2436

2537
@Override
2638
public Node apply( int value )
2739
{
28-
if ( value < 0 ) return null;
29-
3040
ParStyle ps = area.getParagraph( value ).getParagraphStyle();
31-
if ( ! ps.indent.isPresent() ) return null;
41+
return createGraphic( ps, value );
42+
}
43+
44+
private Node createGraphic( ParStyle ps, int idx )
45+
{
46+
Label foldIndicator = new Label( " " );
47+
VBox.setVgrow( foldIndicator, Priority.ALWAYS );
48+
foldIndicator.setMaxHeight( Double.MAX_VALUE );
49+
foldIndicator.setAlignment( Pos.TOP_LEFT );
50+
foldIndicator.setFont( DEFAULT_FONT );
51+
52+
if ( area.getParagraphs().size() > idx+1 ) {
53+
if ( area.getParagraph( idx+1 ).getParagraphStyle().isFolded() && ! ps.isFolded() ) {
54+
foldIndicator.setOnMouseClicked( ME -> area.unfoldParagraphs( idx ) );
55+
foldIndicator.getStyleClass().add( "fold-indicator" );
56+
foldIndicator.setCursor( Cursor.HAND );
57+
foldIndicator.setText( "+ " );
58+
}
59+
}
60+
61+
if ( ps.isIndented() && ! ps.isFolded() ) {
62+
foldIndicator.setGraphic( createBullet( ps.getIndent() ) );
63+
foldIndicator.setContentDisplay( ContentDisplay.RIGHT );
64+
}
3265

33-
return createBullet( ps.indent.get() );
66+
return new VBox( 0, foldIndicator );
3467
}
3568

3669
private Node createBullet(Indent in) {
@@ -68,10 +101,49 @@ private Node createBullet(Indent in) {
68101
}
69102
break;
70103
}
71-
72-
Label l = new Label( " ", result );
73-
l.setPadding( new Insets( 0, 0, 0, in.level*in.width ) );
74-
l.setContentDisplay( ContentDisplay.LEFT );
75-
return new VBox( 0, l );
104+
105+
Label bullet = new Label( " ", result );
106+
bullet.setPadding( new Insets( 0, 0, 0, in.level*in.width ) );
107+
bullet.setContentDisplay( ContentDisplay.LEFT );
108+
return bullet;
109+
}
110+
111+
private void deleteParagraphCheck()
112+
{
113+
int p = area.getCurrentParagraph();
114+
// Was the deleted paragraph in the viewport ?
115+
if ( p >= area.firstVisibleParToAllParIndex() && p <= area.lastVisibleParToAllParIndex() )
116+
{
117+
int col = area.getCaretColumn();
118+
// Delete was pressed on an empty paragraph, and so the cursor is now at the start of the next paragraph.
119+
if ( col == 0 ) {
120+
// Check if the now current paragraph is folded.
121+
if ( area.getParagraph( p ).getParagraphStyle().isFolded() ) {
122+
p = Math.max( p-1, 0 ); // Adjust to previous paragraph.
123+
area.recreateParagraphGraphic( p ); // Show fold/unfold indicator on previous paragraph.
124+
area.moveTo( p, 0 ); // Move cursor to previous paragraph.
125+
}
126+
}
127+
// Backspace was pressed on an empty paragraph, and so the cursor is now at the end of the previous paragraph.
128+
else if ( col == area.getParagraph( p ).length() ) {
129+
area.recreateParagraphGraphic( p ); // Shows fold/unfold indicator on current paragraph if needed.
130+
}
131+
// In all other cases the paragraph graphic is created/updated automatically.
132+
}
133+
}
134+
135+
private void insertParagraphCheck()
136+
{
137+
int p = area.getCurrentParagraph();
138+
// Is the inserted paragraph in the viewport ?
139+
if ( p > area.firstVisibleParToAllParIndex() && p <= area.lastVisibleParToAllParIndex() ) {
140+
// Check limits, as p-1 and p+1 are accessed ...
141+
if ( p > 0 && p+1 < area.getParagraphs().size() ) {
142+
// Now check if the inserted paragraph is before a folded block ?
143+
if ( area.getParagraph( p+1 ).getParagraphStyle().isFolded() ) {
144+
area.recreateParagraphGraphic( p-1 ); // Remove the unfold indicator.
145+
}
146+
}
147+
}
76148
}
77149
}

richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/ParStyle.java

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@ public String getName() {
3636
public void encode(DataOutputStream os, ParStyle t) throws IOException {
3737
OPT_ALIGNMENT_CODEC.encode(os, t.alignment);
3838
OPT_COLOR_CODEC.encode(os, t.backgroundColor);
39+
os.writeInt( t.indent.map( i -> Integer.valueOf( i.level ) ).orElse( 0 ) );
40+
os.writeInt( t.foldCount );
3941
}
4042

4143
@Override
4244
public ParStyle decode(DataInputStream is) throws IOException {
4345
return new ParStyle(
4446
OPT_ALIGNMENT_CODEC.decode(is),
45-
OPT_COLOR_CODEC.decode(is));
47+
OPT_COLOR_CODEC.decode(is),
48+
Optional.of( new Indent( is.readInt() ) ),
49+
is.readInt() );
4650
}
4751

4852
};
@@ -52,28 +56,28 @@ public ParStyle decode(DataInputStream is) throws IOException {
5256
public static ParStyle alignRight() { return EMPTY.updateAlignment(RIGHT); }
5357
public static ParStyle alignJustify() { return EMPTY.updateAlignment(JUSTIFY); }
5458
public static ParStyle backgroundColor(Color color) { return EMPTY.updateBackgroundColor(color); }
59+
public static ParStyle folded() { return EMPTY.updateFold(Boolean.TRUE); }
60+
public static ParStyle unfolded() { return EMPTY.updateFold(Boolean.FALSE); }
5561

5662
final Optional<TextAlignment> alignment;
5763
final Optional<Color> backgroundColor;
5864
final Optional<Indent> indent;
65+
final int foldCount;
5966

60-
public ParStyle() {
61-
this(Optional.empty(), Optional.empty(), Optional.empty());
67+
private ParStyle() {
68+
this(Optional.empty(), Optional.empty(), Optional.empty(), 0);
6269
}
6370

64-
public ParStyle(Optional<TextAlignment> alignment, Optional<Color> backgroundColor) {
65-
this(alignment, backgroundColor, Optional.empty());
66-
}
67-
68-
public ParStyle(Optional<TextAlignment> alignment, Optional<Color> backgroundColor, Optional<Indent> indent) {
71+
private ParStyle(Optional<TextAlignment> alignment, Optional<Color> backgroundColor, Optional<Indent> indent, int folds) {
6972
this.alignment = alignment;
7073
this.backgroundColor = backgroundColor;
74+
this.foldCount = folds;
7175
this.indent = indent;
7276
}
7377

7478
@Override
7579
public int hashCode() {
76-
return Objects.hash(alignment, backgroundColor, indent);
80+
return Objects.hash(alignment, backgroundColor, indent, foldCount);
7781
}
7882

7983
@Override
@@ -82,7 +86,8 @@ public boolean equals(Object other) {
8286
ParStyle that = (ParStyle) other;
8387
return Objects.equals(this.alignment, that.alignment) &&
8488
Objects.equals(this.backgroundColor, that.backgroundColor) &&
85-
Objects.equals(this.indent, that.indent);
89+
Objects.equals(this.indent, that.indent) &&
90+
this.foldCount == that.foldCount;
8691
} else {
8792
return false;
8893
}
@@ -112,26 +117,29 @@ public String toCss() {
112117
sb.append("-fx-background-color: " + TextStyle.cssColor(color) + ";");
113118
});
114119

120+
if ( foldCount > 0 ) sb.append( "visibility: collapse;" );
121+
115122
return sb.toString();
116123
}
117124

118125
public ParStyle updateWith(ParStyle mixin) {
119126
return new ParStyle(
120127
mixin.alignment.isPresent() ? mixin.alignment : alignment,
121128
mixin.backgroundColor.isPresent() ? mixin.backgroundColor : backgroundColor,
122-
mixin.indent.isPresent() ? mixin.indent : indent );
129+
mixin.indent.isPresent() ? mixin.indent : indent,
130+
mixin.foldCount + foldCount);
123131
}
124132

125133
public ParStyle updateAlignment(TextAlignment alignment) {
126-
return new ParStyle(Optional.of(alignment), backgroundColor, indent);
134+
return new ParStyle(Optional.of(alignment), backgroundColor, indent, foldCount);
127135
}
128136

129137
public ParStyle updateBackgroundColor(Color backgroundColor) {
130-
return new ParStyle(alignment, Optional.of(backgroundColor), indent);
138+
return new ParStyle(alignment, Optional.of(backgroundColor), indent, foldCount);
131139
}
132140

133141
public ParStyle updateIndent(Indent indent) {
134-
return new ParStyle(alignment, backgroundColor, Optional.ofNullable(indent));
142+
return new ParStyle(alignment, backgroundColor, Optional.ofNullable(indent), foldCount);
135143
}
136144

137145
public ParStyle increaseIndent() {
@@ -143,4 +151,20 @@ public ParStyle decreaseIndent() {
143151
.map( Indent::decrease ).orElse( null ) );
144152
}
145153

154+
public Indent getIndent() {
155+
return indent.get();
156+
}
157+
158+
public boolean isIndented() {
159+
return indent.map( in -> in.level > 0 ).orElse( false );
160+
}
161+
162+
public ParStyle updateFold(boolean fold) {
163+
int foldLevels = fold ? foldCount+1 : Math.max( 0, foldCount-1 );
164+
return new ParStyle(alignment, backgroundColor, indent, foldLevels);
165+
}
166+
167+
public boolean isFolded() {
168+
return foldCount > 0;
169+
}
146170
}

0 commit comments

Comments
 (0)