I am looking for an alternative of Swing InputVerifier to JavaFX TextField.
The Swing InputVerifier will prevent input that does verify.
Consider the following Swing JTextField code:
InputVerifier iv = new InputVerifier() {
/* (non-Javadoc)
* #see javax.swing.InputVerifier#verify(javax.swing.JComponent)
*/
#Override
public boolean verify(JComponent input) {
JTextField tf = (JTextField) input;
if (!myRegExTool.matches(tf.getText())) {
return false;
}
return true;
}
};
jinstance.setInputVerifier(iv);
I could use TextField.setOnKeyTyped or a listener to TextField.textProperty to check the typed text. However that will not prevent invalid text to get into the TextField. I could however delete typed text that does not verify, but that is not a good solution.
Solution:
As suggested by James_D a TextFormatter with a filter was the perfect solution
UnaryOperator<Change> textFilter = change -> {
String input = change.getText();
if (!myRegExTool.matches(input)) {
return null;
}
return change;
};
instance.setTextFormatter(new TextFormatter<String>(textFilter));
I have IntegerField control with StringConverter, UnaryOperator and TextFormatter. Maybe helpful for you!
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.util.StringConverter;
import javafx.util.converter.IntegerStringConverter;
import java.util.function.UnaryOperator;
public class IntegerField extends TextField {
public IntegerField() {
super();
StringConverter<Integer> integerStringConverter = new IntegerStringConverter();
UnaryOperator<TextFormatter.Change> filter = getFilter();
setTextFormatter(new TextFormatter<>(integerStringConverter, null, filter));
setOnAction(event -> integerStringConverter.fromString(this.getText()));
}
private UnaryOperator<TextFormatter.Change> getFilter() {
return change -> {
String text = change.getText();
if (!change.isContentChange()) {
return change;
}
if (text.matches("[0-9]*")) {
return change;
}
return null;
};
}
public Integer getValue() {
return Integer.valueOf(this.getText());
}
}
As first suggested by James_D a TextFormatter with filter was the solution.
TextField instance = new TextField();
UnaryOperator<Change> textFilter = change -> {
String input = change.getText();
if (!change.isContentChange()) {
return change;
}
if (!myRegExTool.matches(input)) {
return null;
}
return change;
};
instance.setTextFormatter(new TextFormatter<String>(textFilter));
Related
In JavaFx-14 resizeColumnToFitContent method a method to autosize columns to fit the size of the header and the column data was estabished.
In the main this works, at least for simple columns, but when I try to use a column whose cellFactory is set to CheckBoxTableCell.forTableColumn or when I try to create my own TableCell (as it happens for a filtered combobox) all I get is the default width (of 80), which is independent of either the header width or the data width.
Obviously in the case of the CheckBoxTableCell, the cell is almost always smaller than the header, but even here the column header is truncated. In the image below the first two columns seem to work as expected, but the Profession column has had both the header text and the row data text truncated, and in the case of the CheckBoxTableCell it is only the header (which should read "Enabled Test") which is truncated.
I presume that there is something that the default TableCell used by a simple TableColumn has that I need to add to my explicit cells, although it is has to see how I would add this to the .forTableColumn generated TableCell.
Code to show the problem with the checkbox column (in scala, sorry my Java is rather rusty these days):-
import scalafx.application.JFXApp3
import scalafx.application.JFXApp3.PrimaryStage
import scalafx.scene.Scene
import scalafx.scene.control.TableView
import scalafx.scene.control.TableColumn
import scalafx.scene.control.cell.CheckBoxTableCell
import scalafx.beans.property.BooleanProperty
import scalafx.beans.value.ObservableValue
import scalafx.collections.ObservableBuffer
import javafx.scene.control.TableColumnBase
import javafx.scene.control.skin.{NestedTableColumnHeader,TableColumnHeader,TableHeaderRow,TableViewSkin}
import scalafx.application.Platform
object TableColumnWidth extends JFXApp3 {
override def start(): Unit = {
stage = new PrimaryStage {
title = "Test Table Column Width Problem"
scene = new Scene {
val ob = ObservableBuffer[TableColumnData]()
val table = new TableView[TableColumnData](ob) {
def createDefaultSkin = new TableViewSkin(this) {
override protected def createTableHeaderRow:TableHeaderRow = {
new TableHeaderRow(this) {
override protected def createRootHeader:NestedTableColumnHeader = {
new NestedTableColumnHeader(null) {
override protected def createTableColumnHeader(col:TableColumnBase[_,_]) = {
if(col == null || col.getColumns.isEmpty || col == getTableColumn) new FormFxTableColumnHeader(col)
else new NestedTableColumnHeader(col)
}
}
}
}
}
}
columns ++= Seq{new TableColumn[TableColumnData,java.lang.Boolean](){
text = "Enabled Test"
cellValueFactory = { c =>
(new BooleanProperty(this,"Enabled",c.value.enabled) {
onChange{ (_,_,newValue) => c.value.enabled = newValue }
}).asInstanceOf[ObservableValue[java.lang.Boolean,java.lang.Boolean]]
}
cellFactory = CheckBoxTableCell.forTableColumn(this)
editable = true
resizable = false
}
}
editable = true
}
root = table
ob ++= Seq(TableColumnData("First",false),
TableColumnData("Second",false))
Platform.runLater(() -> {
val w = table.columns.map { col =>
// To find the TableColumnHeader we can use column.getStyleableNode as suggested by kleopatra on StackOverflow:-
// you get the header from column.getStyleableNode (took a moment, had to check if it's really implemented) – kleopatra Jul 1 at 20:46
col.getStyleableNode() match {
case mtch:FormFxTableColumnHeader => mtch.resizeCol()
case u => col.getWidth()
}
}.sum + 25
// if(width.value > w) {
table.prefWidth = w
table.minWidth = w
table.maxWidth = w
})
sizeToScene()
}
}
}
}
class FormFxTableColumnHeader(tc:TableColumnBase[_,_]) extends TableColumnHeader(tc) {
def resizeCol():Double = {
resizeColumnToFitContent(-1)
// println(s"in resizeCol for ${tc.getText()}, width ${tc.width.value}}")
tc.getWidth()
}
}
class TableColumnData(var name:String,var enabled:Boolean)
The header in this one will be truncated to "Enabled..."
Anyone have any ideas as to what is needed?
As already mentioned in the comments, I don't see any issues in using the resizeColumnToFitContent method.
Below is your example re-written in java (FX18). And it works as expected to resize the table.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Skin;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.skin.NestedTableColumnHeader;
import javafx.scene.control.skin.TableColumnHeader;
import javafx.scene.control.skin.TableHeaderRow;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class FitTableViewToColumnsDemo extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.add(new Person("Bruce Willis", "bruce.willis#disaster.com", "Actor"));
persons.add(new Person("Liz Truss", "liz.truss#gov.uk", "Prime Minister"));
persons.add(new Person("Vincent van Gough", "vincent#sunflower.com", "Artist"));
TableColumn<Person, String> nameCol = new TableColumn<>("Name");
nameCol.setCellValueFactory(param -> param.getValue().nameProperty());
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
emailCol.setCellValueFactory(param -> param.getValue().emailProperty());
TableColumn<Person, Boolean> enabledCol = new TableColumn<>("Enabled Test");
enabledCol.setCellValueFactory(param -> param.getValue().enabledProperty());
enabledCol.setCellFactory(CheckBoxTableCell.forTableColumn(enabledCol));
TableColumn<Person, String> profCol = new TableColumn<>("Profession");
profCol.setCellValueFactory(param -> param.getValue().professionProperty());
TableView<Person> tableView = new TableView<>() {
#Override
protected Skin<?> createDefaultSkin() {
return new MyTableViewSkin(this);
}
};
tableView.getColumns().addAll(nameCol, emailCol, profCol, enabledCol);
tableView.setItems(persons);
VBox root = new VBox();
root.getChildren().addAll(tableView);
VBox.setVgrow(tableView, Priority.ALWAYS);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle("FX18 FitTableViewToColumnsDemo");
primaryStage.show();
Platform.runLater(() -> {
double totalWidth = tableView.getColumns().stream().map(tc -> {
if (tc.getStyleableNode() instanceof FormFxTableColumnHeader header) {
return header.resizeCol();
} else {
return tc.getWidth();
}
}).mapToDouble(f -> f.doubleValue()).sum();
totalWidth = totalWidth + 25;
tableView.setPrefWidth(totalWidth);
tableView.setMinWidth(totalWidth);
tableView.setMaxWidth(totalWidth);
primaryStage.sizeToScene();
});
}
class MyTableViewSkin extends TableViewSkin<Person> {
public MyTableViewSkin(TableView<Person> tableView) {
super(tableView);
}
#Override
protected TableHeaderRow createTableHeaderRow() {
return new TableHeaderRow(this) {
#Override
protected NestedTableColumnHeader createRootHeader() {
return new NestedTableColumnHeader(null) {
#Override
protected TableColumnHeader createTableColumnHeader(TableColumnBase col) {
if (col == null || col.getColumns().isEmpty() || col == getTableColumn()) {
return new FormFxTableColumnHeader(col);
} else {
return new NestedTableColumnHeader(col);
}
}
};
}
};
}
}
class FormFxTableColumnHeader extends TableColumnHeader {
TableColumnBase tcb;
public FormFxTableColumnHeader(TableColumnBase tc) {
super(tc);
tcb = tc;
}
public double resizeCol() {
resizeColumnToFitContent(-1);
return tcb.getWidth();
}
}
class Person {
private StringProperty name = new SimpleStringProperty();
private StringProperty email = new SimpleStringProperty();
private StringProperty profession = new SimpleStringProperty();
private BooleanProperty enabled = new SimpleBooleanProperty();
public Person(String name1, String email1, String profession1) {
name.set(name1);
email.set(email1);
profession.set(profession1);
}
public StringProperty nameProperty() {
return name;
}
public StringProperty emailProperty() {
return email;
}
public BooleanProperty enabledProperty() {
return enabled;
}
public StringProperty professionProperty() {
return profession;
}
}
}
UPDATE :
I think I am now able to reproduce the issue if I set the columns resizable property to false. I think that should be the issue at your side as well.
If you want to set the columns to resizable false, I think you can remove the setting at the time of declaration and add it after the tableView width is computed (in Platform.runLater).. something like..
Platform.runLater(() -> {
double totalWidth = tableView.getColumns().stream().map(tc -> {
if (tc.getStyleableNode() instanceof FormFxTableColumnHeader header) {
return header.resizeCol();
} else {
return tc.getWidth();
}
}).mapToDouble(f -> f.doubleValue()).sum();
totalWidth = totalWidth + 25;
tableView.setPrefWidth(totalWidth);
tableView.setMinWidth(totalWidth);
tableView.setMaxWidth(totalWidth);
profCol.setResizable(false); \\ turn off resizable after the width is computed
enabledCol.setResizable(false);
primaryStage.sizeToScene();
});
JavaFx-14 put this method in the TableColumnHeader, rather than in the Skin. How does one find a TableColumnHeader from a TableColumn and a TableView?
Don't know if you still need this, but if anyone else is interested, this is how I surpassed the problem in java, based on David Goodenough's scala code above.
The class for the TableSkin
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.NestedTableColumnHeader;
import javafx.scene.control.skin.TableColumnHeader;
import javafx.scene.control.skin.TableHeaderRow;
import javafx.scene.control.skin.TableViewSkin;
import java.util.ArrayList;
import java.util.List;
public class CustomTableViewSkin extends TableViewSkin<Track> {
private List<CustomTableColumnHeader> columnHeadersList = new ArrayList<>();
private class CustomTableColumnHeader extends TableColumnHeader {
/**
* Creates a new TableColumnHeader instance to visually represent the given
* {#link TableColumnBase} instance.
*
* #param tc The table column to be visually represented by this instance.
*/
public CustomTableColumnHeader(TableColumnBase tc) {
super(tc);
}
public void resizeColumnToFitContent() {
super.resizeColumnToFitContent(-1);
}
}
public CustomTableViewSkin(TableView<Track> tableView) {
super(tableView);
}
#Override
protected TableHeaderRow createTableHeaderRow() {
return new TableHeaderRow(this) {
#Override
protected NestedTableColumnHeader createRootHeader() {
return new NestedTableColumnHeader(null) {
#Override
protected TableColumnHeader createTableColumnHeader(TableColumnBase col) {
CustomTableColumnHeader columnHeader = new CustomTableColumnHeader(col);
if (columnHeadersList == null) {
columnHeadersList = new ArrayList<>();
}
columnHeadersList.add(columnHeader);
return columnHeader;
}
};
}
};
}
public void resizeColumnToFit() {
if (!columnHeadersList.isEmpty()) {
for (CustomTableColumnHeader columnHeader : columnHeadersList) {
columnHeader.resizeColumnToFitContent();
}
}
}
}
And the class for the TableView
import javafx.scene.control.TableView;
public class CustomTableView extends TableView<Foo> {
private final CustomTableViewSkin thisSkin;
public CustomTableView() {
super();
setSkin(thisSkin = new CustomTableViewSkin(this));
}
public void resizeColumnsToFitContent() {
if (thisSkin != null && getSkin() == thisSkin) {
thisSkin.resizeColumnToFit();
}
}
}
Well this code is Scala not Java, but for the record the code below works:-
skin = new TableViewSkin(this) {
override protected def createTableHeaderRow:TableHeaderRow = {
new TableHeaderRow(this) {
override protected def createRootHeader:NestedTableColumnHeader = {
new NestedTableColumnHeader(null) {
override protected def createTableColumnHeader(col:TableColumnBase[_,_]) = {
val tableColumnHeader = new MyTableColumnHeader(col)
if(col == null || col.getColumns.isEmpty || col == getTableColumn) tableColumnHeader else new NestedTableColumnHeader(col)
}
}
}
}
}
}
private class MyTableColumnHeader(tc:TableColumnBase[_,_]) extends TableColumnHeader(tc) {
def resizeCol():Double = {
resizeColumnToFitContent(-1)
width.value
}
}
and then when I want to use it I use kleopatra's suggestion and:-
val w = columns.map { col =>
// To find the TableColumnHeader we can use column.getStyleableNode as suggested by kleopatra on StackOverflow:-
// you get the header from coumn.getStyleableNode (took a moment, had to check if it's really implemented) – kleopatra Jul 1 at 20:46
col.getStyleableNode() match {
case mtch:MyTableColumnHeader => mtch.resizeCol
case _ => col.width.get
}
}.sum
This is a hack'ish way of getting the TableColumnHeader:
public TableColumnHeader getTableColumnHeader(TableView<?> table, int index) {
return (TableColumnHeader) table.queryAccessibleAttribute(AccessibleAttribute.COLUMN_AT_INDEX, index);
}
Or, as #kleopatra suggested, for a non-hack'ish approach you can do:
public TableColumnHeader getTableColumnHeader(TableView<?> table, int index) {
return (TableColumnHeader) table.getColumns().get(index).getStyleableNode();
}
Make sure that the TableView is part of the scene graph.
However, the resizeColumnToFitContent method is protected and you won't be able to access it.
A relative Java newbie question.
I have a TableView with extractors and a ListChangeListener added to the underlying ObservableList.
If I have a StringProperty column in the data model, the change listener doesn't detect changes if I double-click the cell and then hit ENTER without making any changes. That's good.
However, if I define the column as ObjectProperty<String> and double-click and then hit ENTER, the change listener always detects changes even when none have been made.
Why does that happen? What's the difference between ObjectProperty<String> and StringProperty from a change listener's point of view?
I've read Difference between SimpleStringProperty and StringProperty and JavaFX SimpleObjectProperty<T> vs SimpleTProperty and think I understand the differences. But I don't understand why the change listener is giving different results for TProperty/SimpleTProperty and ObjectProperty<T>.
If it helps, here is a MVCE for my somewhat nonsensical case. I'm actually trying to get a change listener working for BigDecimal and LocalDate columns and have been stuck on it for 5 days. If I can understand why the change listener is giving different results, I might be able to get my code working.
I'm using JavaFX8 (JDK1.8.0_181), NetBeans 8.2 and Scene Builder 8.3.
package test17;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.converter.DefaultStringConverter;
public class Test17 extends Application {
private Parent createContent() {
ObservableList<TestModel> olTestModel = FXCollections.observableArrayList(testmodel -> new Observable[] {
testmodel.strProperty(),
testmodel.strObjectProperty()
});
olTestModel.add(new TestModel("A", "a"));
olTestModel.add(new TestModel("B", "b"));
olTestModel.addListener((ListChangeListener.Change<? extends TestModel > c) -> {
while (c.next()) {
if (c.wasUpdated()) {
System.out.println("===> wasUpdated() triggered");
}
}
});
TableView<TestModel> table = new TableView<>();
TableColumn<TestModel, String> strCol = new TableColumn<>("strCol");
strCol.setCellValueFactory(cellData -> cellData.getValue().strProperty());
strCol.setCellFactory(TextFieldTableCell.forTableColumn(new DefaultStringConverter()));
strCol.setEditable(true);
strCol.setPrefWidth(100);
strCol.setOnEditCommit((CellEditEvent<TestModel, String> t) -> {
((TestModel) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setStr(t.getNewValue());
});
TableColumn<TestModel, String> strObjectCol = new TableColumn<>("strObjectCol");
strObjectCol.setCellValueFactory(cellData -> cellData.getValue().strObjectProperty());
strObjectCol.setCellFactory(TextFieldTableCell.forTableColumn(new DefaultStringConverter()));
strObjectCol.setEditable(true);
strObjectCol.setPrefWidth(100);
strObjectCol.setOnEditCommit((CellEditEvent<TestModel, String> t) -> {
((TestModel) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setStrObject(t.getNewValue());
});
table.getColumns().addAll(strCol, strObjectCol);
table.setItems(olTestModel);
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
BorderPane content = new BorderPane(table);
return content;
}
public class TestModel {
private StringProperty str;
private ObjectProperty<String> strObject;
public TestModel(
String str,
String strObject
) {
this.str = new SimpleStringProperty(str);
this.strObject = new SimpleObjectProperty(strObject);
}
public String getStr() {
return this.str.get();
}
public void setStr(String str) {
this.str.set(str);
}
public StringProperty strProperty() {
return this.str;
}
public String getStrObject() {
return this.strObject.get();
}
public void setStrObject(String strObject) {
this.strObject.set(strObject);
}
public ObjectProperty<String> strObjectProperty() {
return this.strObject;
}
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.setTitle("Test");
stage.setWidth(350);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The difference can be seen by looking at the source code of StringPropertyBase and ObjectPropertyBase—specfically, their set methods.
StringPropertyBase
#Override
public void set(String newValue) {
if (isBound()) {
throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
}
if ((value == null)? newValue != null : !value.equals(newValue)) {
value = newValue;
markInvalid();
}
}
ObjectPropertyBase
#Override
public void set(T newValue) {
if (isBound()) {
throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
}
if (value != newValue) {
value = newValue;
markInvalid();
}
}
Notice the difference in how they check if the new value is equal to the old value? The StringPropertyBase class checks by using Object.equals whereas the ObjectPropertyBase class uses reference equality (==/!=).
I can't answer for certain why this difference exists, but I can hazard a guess: An ObjectProperty can hold anything and therefore there's the potential for Object.equals to be expensive; such as when using a List or Set. When coding StringPropertyBase I guess they decided that potential wasn't there, that the semantics of String equality was more important, or both. There may be more/better reasons for why they did what they did, but as I was not involved in development I'm not aware of them.
Interestingly, if you look at how they handle listeners
(com.sun.javafx.binding.ExpressionHelper) you'll see that they check for equality using Object.equals. This equality check only occurs if there are currently ChangeListeners registered—probably to support lazy evaluation when there are no ChangeListeners.
If the new and old values are equals the ChangeListeners are not notified. This doesn't stop the InvalidationListeners from being notified, however. Thus, your ObservableList will fire an update change because that mechanism is based on InvalidationListeners and not ChangeListeners.
Here's the relevant source code:
ExpressionHelper$Generic.fireValueChangedEvent
#Override
protected void fireValueChangedEvent() {
final InvalidationListener[] curInvalidationList = invalidationListeners;
final int curInvalidationSize = invalidationSize;
final ChangeListener<? super T>[] curChangeList = changeListeners;
final int curChangeSize = changeSize;
try {
locked = true;
for (int i = 0; i < curInvalidationSize; i++) {
try {
curInvalidationList[i].invalidated(observable);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
}
}
if (curChangeSize > 0) {
final T oldValue = currentValue;
currentValue = observable.getValue();
final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
if (changed) {
for (int i = 0; i < curChangeSize; i++) {
try {
curChangeList[i].changed(observable, oldValue, currentValue);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
}
}
}
}
} finally {
locked = false;
}
}
And you can see this behavior in the following code:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class Main {
public static void main(String[] args) {
ObjectProperty<String> property = new SimpleObjectProperty<>("Hello, World!");
property.addListener(obs -> System.out.printf("Property invalidated: %s%n", property.get()));
property.addListener((obs, ov, nv) -> System.out.printf("Property changed: %s -> %s%n", ov, nv));
property.get(); // ensure valid
property.set(new String("Hello, World!")); // must not use interned String
property.set("Goodbye, World!");
}
}
Output:
Property invalidated: Hello, World!
Property invalidated: Goodbye, World!
Property changed: Hello, World! -> Goodbye, World!
I want get a tab through the tab's attribute text, but I cannot find a native method. So I writed a class to implement the purpose.
Question:
- I want to know if there is a native method for that purpose?
- Is there a better implementation?
Thanks!
```
package pre.huangjs.tabpane;
import javafx.collections.ListChangeListener;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import java.util.HashMap;
import java.util.List;
/**
* Created by huangjs on 2018/4/9.
*/
public class TabPaneExpansion {
private TabPane tabPane;
private HashMap<String, Tab> tabsMap;
public TabPane getTabPane() {
return tabPane;
}
public void setTabPane(TabPane tabPane) {
this.tabPane = tabPane;
}
public TabPaneExpansion() {
this.tabPane = new TabPane();
this.tabsMap = new HashMap<>();
initial();
}
public TabPaneExpansion(TabPane tabPane) {
this.tabPane = tabPane;
this.tabsMap = new HashMap<>();
initial();
}
private void initial() {
tabPane.getTabs().addListener(new ListChangeListener<Tab>() {
#Override
public void onChanged(Change<? extends Tab> c) {
while (c.next()) {
// if elements were added into list, the elements's text
// and the elements themselves need to be added into HashMap
if (c.wasAdded()) {
List<? extends Tab> addedTabs = c.getAddedSubList();
for (Tab tab : addedTabs) {
tabsMap.put(tab.getText(), tab);
}
}
// if elements were removed from list, the elements's text
// and the elements themselves need to be removed from HashMap
if(c.wasRemoved()){
List<? extends Tab> removedTabs = c.getRemoved();
for(Tab tab : removedTabs){
tabsMap.remove(tab.getText());
}
}
}
}
});
}
public boolean addTab(Tab tab) {
return this.tabPane.getTabs().add(tab);
}
public boolean addTabs(Tab... tabs) {
return this.tabPane.getTabs().addAll(tabs);
}
public boolean removeTab(String text){
return this.tabPane.getTabs().remove(getTabByText(text));
}
public Tab getTabByText(String text) {
return tabsMap.get(text);
}
}
```
I need to limit interval of the text property of a text field
int maxLength = 64;
int minLength = 0;
txtSeuil.textProperty().addListener((v, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) {
txtSeuil.setText(newValue.replaceAll("[^\\d*{1,2}]", ""));
if (txtSeuil.getText().length() > maxLength || txtSeuil.getText().length() < minLength) {
String s = txtSeuil.getText().substring(0, maxLength);
txtSeuil.setText(s);
}
}
});
the field does accept only numbers but any number, not just the interval values
If I have understood correctly, you're trying to implement a number field that only allows values within the interval [0, 64]. According to this answer, TextFormatter is the recommended way to accomplish such a functionality. Have a look at this MWE which should solve your problem:
public class RestrictedNumberFieldDemo extends Application {
public static void main(String[] args) {
launch();
}
#Override
public void start(Stage primaryStage) {
TextField numField = new TextField();
numField.setTextFormatter(new TextFormatter<Integer>(change -> {
// Deletion should always be possible.
if (change.isDeleted()) {
return change;
}
// How would the text look like after the change?
String txt = change.getControlNewText();
// There shouldn't be leading zeros.
if (txt.matches("0\\d+")) {
return null;
}
// Try parsing and check if the result is in [0, 64].
try {
int n = Integer.parseInt(txt);
return 0 <= n && n <= 64 ? change : null;
} catch (NumberFormatException e) {
return null;
}
}));
primaryStage.setScene(new Scene(numField));
primaryStage.show();
}
}
To solve your problem you can implement custom Filter for TextFormatter. It will allow to enter numbers only and restrict length of string. Here is snippet which show how it can works:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.stage.Stage;
public class Main5 extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
TextField textField = new TextField();
textField.setTextFormatter(new TextFormatter<Integer>(change -> {
if (!change.getText().isEmpty()) {
return change.getText().matches("\\d+") && change.getControlNewText().length() <= 5 ? change : null;
}
return change;
}));
primaryStage.setScene(new Scene(textField));
primaryStage.show();
}
}
Your problem is:
The length check is not done, if the regex matches. But the text can be a sequence of digits and be to long.
You need to do those checks independent of each other.
Also you're setting a new value in some cases which triggers the same checks again. Of course they will result in the String being evaluates as a valid input, but you could prevent checking again by introducing a field in the ChangeListener that stores, whether the listener is currently being executed:
txtSeuil.textProperty().addListener(new ChangeListener<String>() {
private boolean validating = false;
#Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
if (!validating) {
validating = true;
String newText = newValue;
boolean modify = false;
if (!newValue.matches("\\d*")) {
newText = newValue.replaceAll("[^\\d*{1,2}]", "");
modify = true;
}
if (newText.length() > maxLength || newText.length() < minLength) {
newText = newText.substring(0, maxLength);
modify = true;
}
if (modify) {
txtSeuil.setText(newText);
}
validating = false;
}
}
});