0

I am planning out a program that, among other things, allows commenting on specific text elements of a text document. Essentially, I would like to create the ability to highlight a specific section of text and add a comment to it. I'm planning on using JavaFX, though I would be open to other java packages, like swing, or even something in JS.

Word does it well, though I would like to add other features, but it serves as a good base. Another product that does some of what I want is Google Books. Comments can be added and highlights placed. PDF readers all are nice too, but I want to avoid working with PDF files, at least for now.

I have looked around for some other advice. There are some resources for adding highlights, which seem a little complex but not impossible. TextFlow objects might handle this well. I'll be looking into that more, but the commenting feature is vital, so I don't want to go a route that has no chance of working out completely.

Any advice on how to do this in JavaFX? Or in another framework?

Edit: The user should be able to modify the comments and delete them, but generally they should be permanent so that when the user opens the file in question after having saved a comment, that same comment should be retained.

buggaby
  • 359
  • 2
  • 13
  • Have you considered the lifetime of your comments? – Peter Jun 22 '16 at 18:15
  • I'm not sure I understand. Like, are they going to be removed by the end user? That answer is that users can remove them if they choose but generally comments are permanent. – buggaby Jun 22 '16 at 19:50
  • Update your question to include that so potential answers can consider being able to reload the comments. Generally they are permanent, but it could have been some sort of shared view / presentation where you would only need the comments for its duration – Peter Jun 22 '16 at 19:57

1 Answers1

2

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:

    Add comment option Example of a comment being added

  • 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:

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();
    }
}
Community
  • 1
  • 1
Peter
  • 1,592
  • 13
  • 20
  • 1
    That's a great start indeed! A treasure trove. Regarding the commenting, I was thinking of unifying the files in a manner similar to ebook formats, treating comments with custom html tags. But a separate file is great too. I'll have to poke around at both ways. Much appreciate the work! – buggaby Jun 23 '16 at 11:56
  • I keep going back and forth between a comment pane, and some type of method to indicate the comment contents at its location, like with the speech bubbles in Word or PDF readers. But I suspect this would take manual creation of those UI elements. The pane option you devised seems much more accessible. – buggaby Jun 23 '16 at 12:00