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 );
}
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();
}
}
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);
}
Please I don't this line of code. Can you please explain further? TextField[] tfs = new TextField[] { tfNewVersion, tfFilters, tfRootDir };
ReplyDeleteThat's to support a core programming technique that allows me to iterate over a set of TextFields.
DeleteThank you very much!
ReplyDeleteGreat read, thankyou
ReplyDelete