I've provided a JavaFX approach to get ideas flowing. It includes some of the key features you're after, however you'll probably want to modify them to better suit your needs - there are a few suggestions under notes on this
Features:
Add comments: Selecting and right-clicking text of interest will expose an additional option to the default ContextMenu
to prompt the user for the comment:

Highlight text: The keyword is highlighted red in the comments pane. When the comment is selected within this pane, the TextArea
will highlight/select the text again like the above step
- Reading old comments back in: I've included a dirty implementation of this with a
ToDo
as I expect you have you own method of how you would like to store and read the comments:
/*ToDo: Assumes structure: rangeStart , rangeEnd | associated text |
comment
Either add validation to confirm this, or replace it completely with a
better structure such as xml
Implement the save-based functionality of whichever approach you decide on
*/
Example from Lorem Ipsum in-case you want to build on the implementation below:
6, 11|ipsum|Test comment to apply against ipsum
409, 418|deserunt |This is a long comment that should wrap around
0, 11|Lorem ipsum|Lorem ipsum inception: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Notes:
- This implementation doesn't provide a method of removing comments, but it's possible with a combination of
setOnContextMenuRequested
and show
. With this approach you could add additional functionality via the ContextMenu
used
- You could use a
HTMLEditor
/ WebView
to style the document as opposed to re-selecting the text. Here are some posts that may help or provide alternatives:
SSCCE:
public class CommentAnnotation extends Application{
public class DocumentAnnotator extends HBox {
private File documentToAnnotate;
private ScrollPane textViewer, commentViewer;
private TextArea textArea;
private VBox commentContainer;
private MenuItem addCommentItem;
private String textInViewer;
private ObservableList<Node> comments = FXCollections.observableArrayList();
public DocumentAnnotator(File document){
documentToAnnotate = document;
readInFile();
setupElements();
}
public DocumentAnnotator(File document, File storedComments){
this(document);
//Re-load previous comments
try{
Files.lines(storedComments.toPath()).forEach(this::parseAndPopulateComment);
} catch (IOException e) {
e.printStackTrace();
}
}
private void readInFile(){
try {
textInViewer = Files.lines(documentToAnnotate.toPath())
.collect(Collectors.joining("\n"));
} catch (IOException e) {
e.printStackTrace();
}
}
private void parseAndPopulateComment(String comment){
/*ToDo: Assumes structure: rangeStart , rangeEnd | associated text | comment
Either add validation to confirm this, or replace it completely with a
better structure such as xml
Implement the save-based functionality of whichever approach you decide on
*/
String[] splitResult = comment.split("\\|");
createComment(IndexRange.valueOf(splitResult[0]), splitResult[1], splitResult[2]);
}
private void setupElements(){
setupViewers();
getChildren().setAll(textViewer, new Separator(Orientation.VERTICAL), commentViewer);
}
private void setupViewers(){
setupTextViewer();
setupCommentViewer();
}
private void setupTextViewer(){
setupTextArea();
textViewer = new ScrollPane(textArea);
textViewer.minHeightProperty().bind(heightProperty());
textViewer.maxHeightProperty().bind(heightProperty());
textArea.maxWidthProperty().bind(textViewer.widthProperty());
textArea.minHeightProperty().bind(textViewer.heightProperty());
}
private void setupTextArea(){
textArea = new TextArea(textInViewer);
//Ensure that if this controls dimensions change, the text will wrap around again
textArea.setWrapText(true);
addCommentItem = new MenuItem("Add comment");
addCommentItem.setOnAction(event -> {
IndexRange range = textArea.getSelection();
String selectedText = textArea.getSelectedText();
String commentText = promptUserForComment(selectedText);
if(selectedText.isEmpty() || commentText.isEmpty()){ return; }
createComment(range, selectedText, commentText);
});
//Append an "Add comment" option to the default menu which contains cut|copy|paste etc
TextAreaSkin modifiedSkin = new TextAreaSkin(textArea){
@Override
public void populateContextMenu(ContextMenu contextMenu) {
super.populateContextMenu(contextMenu);
contextMenu.getItems().add(0, addCommentItem);
contextMenu.getItems().add(1, new SeparatorMenuItem());
}
};
textArea.setSkin(modifiedSkin);
textArea.setEditable(false);
}
private String promptUserForComment(String selectedText){
TextInputDialog inputDialog = new TextInputDialog();
inputDialog.setHeaderText(null);
inputDialog.setTitle("Input comment");
inputDialog.setContentText("Enter the comment to associate against: " + selectedText);
return inputDialog.showAndWait().get();
}
private void setupCommentViewer(){
commentContainer = new VBox(5);
Bindings.bindContentBidirectional(commentContainer.getChildren(), comments);
commentViewer = new ScrollPane(commentContainer);
//Use 30% of the control's width to display comments
commentViewer.minWidthProperty().bind(widthProperty().multiply(0.30));
commentViewer.maxWidthProperty().bind(widthProperty().multiply(0.30));
commentViewer.minHeightProperty().bind(heightProperty());
commentViewer.maxHeightProperty().bind(heightProperty());
//Imitate wrapping
commentViewer.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
//Account for scroller width
commentContainer.maxWidthProperty().bind(commentViewer.widthProperty().subtract(5));
}
private void createComment(IndexRange range, String selectedText, String commentText){
AssociatedComment comment = new AssociatedComment(range, selectedText, commentText);
//Re-select the range when the comment is clicked
comment.setOnMouseClicked(clickEvent -> textArea.selectRange(
comment.getAssociatedRange().getStart(), comment.getAssociatedRange().getEnd()));
comments.add(comment);
}
}
public class AssociatedComment extends TextFlow {
private IndexRange associatedRange;
private Text associatedText, associatedComment;
public AssociatedComment(IndexRange range, String text, String comment){
associatedRange = range;
associatedText = new Text(text);
associatedText.setFill(Color.RED);
associatedComment = new Text(comment);
getChildren().setAll(associatedText, new Text(" : "), associatedComment);
}
public IndexRange getAssociatedRange(){
return associatedRange;
}
public String getAssociatedText(){
return associatedText.getText();
}
public String getAssociatedComment(){
return associatedComment.getText();
}
}
@Override
public void start(Stage primaryStage) throws Exception {
File lorem = new File(getClass().getClassLoader().getResource("loremIpsum.txt").toURI());
File loremComments = new File(getClass().getClassLoader().getResource("loremComments.txt").toURI());
DocumentAnnotator annotator = new DocumentAnnotator(lorem);
//DocumentAnnotator annotator = new DocumentAnnotator(lorem, loremComments);
Scene scene = new Scene(annotator, 600, 400);
primaryStage.setScene(scene);
primaryStage.setTitle("Document annotation");
primaryStage.show();
}
}