I have a few custom components, some bigger ones and some smaller ones. I'd like to know if the structure is correct. As in: Is the code to do x in the right places. For starters, I'd just like to show a very small one, take your feedback and adjust my other components accordingly.
It can be found here.
Before you click on the GitHub repo and have a look at it:
It's just very small adjustments made to TextField to make it feel like an editable label. I know that there is a lot more missing to make it a true editable label, but for that I'd have to extend a base class closer to control itself. This question is purely regarding the custom component structure in general + this implementation of EditableLabel fits all that I need it to do, that's why I chose to not go deeper into the rabbit hole with that.
- Did I structure the methods and functionality into the correct classes (control, skin, behavior)? What could I have done better there?
- Should I do more comments? Could you give me an example (or more)?
- Are there any monstrously wrong implementations in there?
- Are there smaller mistakes in the implementations?
EditableLabel.java
package com.github.rjwestman.editableLabel;
import javafx.beans.property.*;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import java.net.URL;
/**
* A TextField, that implements some Label functionality
*
* It acts as a Label, by removing the TextField style and making it non-editable.
* It is also not focus traversable.
*
* When clicking on it, it will switch to editable mode
* Changing focus away from the EditableLabel or pressing ENTER will save the changes made and deactivate editable mode.
* When pressing ESC it will exit editable mode without saving the changes made.
*
* @sa EditableLabelSkin, EditableLabelBehavior
*/
public class EditableLabel extends TextField {
/************************************************************************
* *
* *
* \defgroup Constructors *
* Constructors and helper methods for constructors *
* *
* @{ *
***********************************************************************/
public EditableLabel() {
this("");
}
public EditableLabel(String text) {
super(text);
getStyleClass().setAll("editable-label");
init();
}
private void init() {
editableClicks = new SimpleIntegerProperty(1);
baseText = new SimpleStringProperty(getText());
setFocusTraversable(false);
setEditable(false);
}
/************************************************************************
* @} *
* *
* \defgroup Properties *
* Declaration, getters and setters for the properties of this control *
* *
* @{ *
***********************************************************************/
/**
* Clicks needed to enter editable-mode
*/
private IntegerProperty editableClicks;
public int getEditableClicks() { return editableClicks.get(); }
public IntegerProperty editableClicksProperty() { return editableClicks; }
public void setEditableClicks(int editableClicks) { this.editableClicks.set(editableClicks); }
/**
* This saves the text that is to be displayed
*
* Since we can't override the final set/get methods of the super class, we need to use this
* to set the text that is to be displayed.
* Since the displayed text can be a truncated base text we need to save the base in it's own property.
*/
private StringProperty baseText;
public String getBaseText() { return baseText.get(); }
public StringProperty baseTextProperty() { return baseText; }
public void setBaseText(String baseText) { this.baseText.set(baseText); }
/************************************************************************
* @} *
* *
* \defgroup Methods *
* *
* @{ *
***********************************************************************/
@Override
protected Skin<?> createDefaultSkin() { return new EditableLabelSkin(this); }
/************************************************************************
* @} *
* *
* \defgroup StylesheetRelated *
* *
* @{ *
***********************************************************************/
@Override
public String getUserAgentStylesheet() {
URL pathToCSS = EditableLabel.class.getResource("editablelabel.css");
if ( pathToCSS != null ) {
return pathToCSS.toExternalForm();
} else {
System.err.println("CSS file for EditableLabel could not be found.");
return null;
}
}
/** @} */
}
EditableLabelSkin.java
package com.github.rjwestman.editableLabel;
import com.sun.javafx.scene.control.skin.TextFieldSkin;
import javafx.application.Platform;
import javafx.collections.SetChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.text.Text;
/**
* The Skin Class for EditableLabel
*
* @sa EditableLabel, EditableLabelBehavior
*/
public class EditableLabelSkin extends TextFieldSkin {
private EditableLabel editableLabel;
private Boolean editableState;
/************************************************************************
* *
* *
* \defgroup Constructors *
* Constructors and helper methods for constructors *
* *
* @{ *
***********************************************************************/
public EditableLabelSkin(final EditableLabel editableLabel) {
this(editableLabel, new EditableLabelBehavior(editableLabel));
}
public EditableLabelSkin(final EditableLabel editableLabel, final EditableLabelBehavior editableLabelBehavior) {
super(editableLabel, editableLabelBehavior);
this.editableLabel = editableLabel;
init();
}
private void init() {
editableState = false;
Platform.runLater(this::updateVisibleText);
// Register listeners and binds
editableLabel.getPseudoClassStates().addListener( (SetChangeListener<PseudoClass>) e -> {
if (e.getSet().contains(PseudoClass.getPseudoClass("editable"))) {
if ( !editableState ) {
// editableState change to editable
editableState = true;
updateVisibleText();
}
} else {
if ( editableState ) {
// editableState change to not editable
editableState = false;
updateVisibleText();
}
}
});
editableLabel.widthProperty().addListener( observable -> updateVisibleText() );
editableLabel.baseTextProperty().addListener( observable -> updateVisibleText() );
}
/************************************************************************
* @} *
* *
* \defgroup ControlStateChanges *
* Handles visual changes on state change that are not or cannot be *
* handled via css *
* *
* @{ *
***********************************************************************/
/**
* Updates the visual text using the baseText
*/
private void updateVisibleText() {
String baseText = editableLabel.getBaseText();
if ( !editableState ) {
editableLabel.setText(calculateClipString(baseText));
} else {
editableLabel.setText(baseText);
editableLabel.positionCaret(baseText.length());
}
}
/**
* Truncates text to fit into the EditableLabel
*
* @param text The text that needs to be truncated
* @return The truncated text with an appended "..."
*/
private String calculateClipString(String text) {
double labelWidth = editableLabel.getWidth();
Text layoutText = new Text(text);
layoutText.setFont(editableLabel.getFont());
if ( layoutText.getLayoutBounds().getWidth() < labelWidth ) {
return text;
} else {
layoutText.setText(text+"...");
while ( layoutText.getLayoutBounds().getWidth() > labelWidth ) {
text = text.substring(0, text.length()-1);
layoutText.setText(text+"...");
}
return text+"...";
}
}
/************************************************************************
* @} *
* *
* \defgroup SkinLayout *
* Lays out the elements of the control *
* (e.g. calculating and setting sizes and bounds or changing number *
* of grid rows and columns) *
* *
* @{ *
***********************************************************************/
/** @} */
}
EditableLabelBehavior.java
package com.github.rjwestman.editableLabel;
import com.sun.javafx.scene.control.behavior.TextFieldBehavior;
import javafx.css.PseudoClass;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
/**
* The Behavior Class for EditableLabel
*
* @sa EditableLabel, EditableLabelSkin
*/
public class EditableLabelBehavior extends TextFieldBehavior {
private EditableLabel editableLabel;
private Boolean focusTraversable;
/************************************************************************
* *
* \defgroup Constructors *
* Constructors and helper methods for constructors *
* *
* @{ *
***********************************************************************/
public EditableLabelBehavior(final EditableLabel editableLabel) {
super(editableLabel);
this.editableLabel = editableLabel;
init();
}
private void init() {
focusTraversable = false;
// Register listeners and events
editableLabel.setOnMouseClicked(this::handleMouseClicked);
editableLabel.setOnKeyPressed(this::handleKeyPressed);
editableLabel.focusedProperty().addListener( (observable, oldValue, newValue) -> handleFocusChange(newValue));
editableLabel.focusTraversableProperty().addListener( (observable, oldValue, newValue) -> handleFocusTraversableChange(newValue));
}
/************************************************************************
* @} *
* *
* \defgroup BehaviorMethods *
* *
* @{ *
***********************************************************************/
private void handleKeyPressed(KeyEvent event) {
switch ( event.getCode() ) {
case ENTER:
editableLabel.setBaseText(editableLabel.getText());
exitEditableMode();
break;
case ESCAPE:
exitEditableMode();
break;
}
}
private void handleMouseClicked(MouseEvent event) {
if ( event.getClickCount() == editableLabel.getEditableClicks() && !this.isEditing()) {
enterEditableMode();
}
}
private void handleFocusChange(Boolean newValue) {
if ( !newValue ) {
// Save changes and exit editable mode
editableLabel.setBaseText(editableLabel.getText());
exitEditableMode();
} else if ( focusTraversable ){
enterEditableMode();
}
}
private void handleFocusTraversableChange(Boolean newValue) {
focusTraversable = newValue;
}
private void enterEditableMode() {
editableLabel.setEditable(true);
editableLabel.deselect();
editableLabel.pseudoClassStateChanged(PseudoClass.getPseudoClass("editable"), true);
}
private void exitEditableMode() {
editableLabel.setEditable(false);
editableLabel.deselect();
editableLabel.pseudoClassStateChanged(PseudoClass.getPseudoClass("editable"), false);
}
/** @} */
}
editablelabel.css
.editable-label:editable {
-fx-background-color: rgba(0, 191, 255, 0.2);
}
TextFieldinto a what you're looking for? I must be missing something. \$\endgroup\$