Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Tuesday, October 28, 2014

JavaFX Cut, Copy and Paste from a MenuBar

With no code at all, you can paste text contents from another program into your javafx.scene.control.TextField using key commands (Cmd-Shift-V on a Mac).  Similarly, you can copy and cut from your TextFields.  However, most applications have a MenuBar that is an alternative to the keyboard shortcuts.  This blog post shows how to interact with the System Clipboard to get and put text into TextFields using MenuBar commands.  This is also the foundation for more complex Clipboard operations such as those involving binary data.

This video demonstrates the Clipboard functions as executed via the MenuBar.



FXML

In SceneBuilder, I define add a MenuBar to the container which includes several Menus and MenuItems.  I added an fx:id and an onAction method to each of the MenuItems in this demo: Cut, Copy, Paste.

Menu Structure as Defined in SceneBuilder

The Clipboard

To work with the Clipboard, get the singleton object from a static factory method.

Clipboard systemClipboard = Clipboard.getSystemClipboard();

To cut or copy something into the Clipboard, define the payload using a ClipboardContent object.  "text" is a java.lang.String.

ClipboardContent content = new ClipboardContent();
content.putString(text);

Call setContent() to add add the payload to the Clipboard.

systemClipboard.setContent(content);

For retrieving data from the Clipboard -- say for a paste operation -- call one of the getters.  In this app, I'm working with text, so I call getString();

String clipboardText = systemClipboard.getString();

Selection and Copy

Text added to the Clipboard is made from selections.  The text is not usually taken from a TextField with the getText() method.  Rather, the user can select portions of the contents (possibly all) that will be placed in the Clipboard.  For this requirement, work with the TextField method getSelectedText().

This method shows 3 TextFields that have been registered as Clipboard-producing candidates.  The TextField that has a selection returns its text for the copy operation.

private String getSelectedText() {
TextField[] tfs = new TextField[] { tfNewVersion, tfFilters, tfRootDir };
for( TextField tf : tfs ) {
if( StringUtils.isNotEmpty(tf.getSelectedText() ) ) {
return tf.getSelectedText();
}
}
return null;
}

This is called for the copy() operation.

public void copy() {
String text = getSelectedText();

ClipboardContent content = new ClipboardContent();
content.putString(text);
systemClipboard.setContent(content);
}

The copy() operation is a delegate that is called directly from an @FXML action registered on the MenuItem.

Cut

Like copy, cut will make a setContent() call on the Clipboard.  However, Cut needs to work with the TextField rather than the contents of the TextField.  This is to meet the requirement that text be removed from the TextField as it's being copied.  So, I use a different method to retrieve the focused TextField.

private TextField getFocusedTextField() {
TextField[] tfs = new TextField[] { tfNewVersion, tfFilters, tfRootDir };
for( TextField tf : tfs ) {
if( tf.isFocused() ) {
return tf;
}
}
return null;
}

This returns the focused TextField from the list of registered TextFields.

While getSelectedText() removes the need for me to work with the selection range itself, I need to work with a selection range when going back over the source TextField to delete the selection.  The block of code starting with IndexRange removes a section (possibly all) of the source text.

public void cut() {
TextField focusedTF = getFocusedTextField();

String text = focusedTF.getSelectedText();
ClipboardContent content = new ClipboardContent();
content.putString(text);
systemClipboard.setContent(content);
IndexRange range = focusedTF.getSelection();
String origText = focusedTF.getText();
String firstPart = StringUtils.substring( origText, 0, range.getStart() );
String lastPart = StringUtils.substring( origText, range.getEnd(), StringUtils.length(origText) );
focusedTF.setText( firstPart + lastPart );
focusedTF.positionCaret( range.getStart() );

}

The StringUtils method is a null-safe call using a library called Commons Lang.  StringUtils.substring() is used to extract the non-selected portions of the text source and recombine them minus the cut part using setText().  positionCaret() places the cursor in the correct position.

Like the copy() operation, cut() is called from an @FXML action.

Paste

Paste is the inverse of the copy and cut operations, using getString() on the ClipboardContent payload to retrieve data for adding to a TextField.  Like cut, paste has to work with ranges and adjust the cursor.

public void paste() {
if( !systemClipboard.hasContent(DataFormat.PLAIN_TEXT) ) {
adjustForEmptyClipboard();
return;
}
String clipboardText = systemClipboard.getString();
TextField focusedTF = getFocusedTextField();
IndexRange range = focusedTF.getSelection();
String origText = focusedTF.getText();
int endPos = 0;
String updatedText = "";
String firstPart = StringUtils.substring( origText, 0, range.getStart() );
String lastPart = StringUtils.substring( origText, range.getEnd(), StringUtils.length(origText) );

updatedText = firstPart + clipboardText + lastPart;
if( range.getStart() == range.getEnd() ) {
endPos = range.getEnd() + StringUtils.length(clipboardText);
} else {
endPos = range.getStart() + StringUtils.length(clipboardText);
}
focusedTF.setText( updatedText );
focusedTF.positionCaret( endPos );
}

The clause checking for PLAIN_TEXT makes sure that only plain text will be inserted into a TextField. If the Clipboard doesn't have anything valid to paste, the operation returns.  adjustForEmptyClipboard() will be described in the following section.

focusedTF is the target TextField.  It may have a selection or it may be set at an insertion point.  The String manipulations result in a setText() / positionCaret() pair to overwrite or insert into the TextField.

Usability

At this point, the cut, copy and paste operations are fully functional.  However, applications disable and enable the MenuItems based on the Clipboard contents.  The absence of content in the Clipboard disables the Paste MenuItem whereas adding content will make it available for use.

I'm using the On Showing action on the Menu (Edit, not the MenuItems) to manage the display of the MenuItems.  Since the Menu is hidden until it's accessed, this provides an ideal initialization for the set of MenuItems prior to display.  This type of lazy initializations means that we don't have to track program state and makes for a cleaner solution.

This is an @FXML method linked to a HideShow / On Showing action.

public void showingEditMenu() {
if( systemClipboard == null ) {
systemClipboard = Clipboard.getSystemClipboard();
}
if( systemClipboard.hasString() ) {
adjustForClipboardContents();
} else {
adjustForEmptyClipboard();
}
if( anythingSelected() ) {
adjustForSelection();

} else {
adjustForDeselection();
}
}

systemClipboard is a variable on a delegate class that is created once per application.  The first event is the Menu On Showing event so systemClipboard will be set by the time the user clicks the Cut, Copy, or Paste MenuItems.

If systemClipboard has String content, then show the Paste MenuItem.  If there is a selection in one of the TextFields, show the Cut and Copy MenuItems.  Like some of my other methods, I iterate over the register TextFields looking for a selection.

private boolean anythingSelected() {
TextField[] tfs = new TextField[] { tfNewVersion, tfFilters, tfRootDir };
for( TextField tf : tfs ) {
if( StringUtils.isNotEmpty(tf.getSelectedText() ) ) {
return true;
}
}
return false;

}

The adjust* methods all toggle different patterns of the Cut, Copy, and Paste MenuItems.

private void adjustForEmptyClipboard() {
miPaste.setDisable(true);  // nothing to paste
}

private void adjustForClipboardContents() {
miPaste.setDisable(false);  // something to paste
}
private void adjustForSelection() {
miCut.setDisable(false);
miCopy.setDisable(false);
}

private void adjustForDeselection() {
miCut.setDisable(true);
miCopy.setDisable(true);
}

Out-of-the-box, cut, copy, and paste work with the key commands.  But most applications provide a MenuBar and an Edit menu with Clipboard operations is included for completeness.  This blog post uses an On Showing action to initialize MenuItems providing a UI that steers users around invalid choices.  Then the Clipboard getContent() / setContent() is called, working with an IndexRange for the text selection requirements of cut and paste.

4 comments:

  1. Please I don't this line of code. Can you please explain further? TextField[] tfs = new TextField[] { tfNewVersion, tfFilters, tfRootDir };

    ReplyDelete
    Replies
    1. That's to support a core programming technique that allows me to iterate over a set of TextFields.

      Delete