I was looking for a universal way (i.e., that can be applied to an arbitrary TableView or TreeTableView) to copy data "as you see it" from a TableView/TreeTableView. I found a couple of posts about how to copy contents from a TableView (here and here), but the important part "as you see it" is the issue with all of them.
All solutions I saw are relying on getting the data associated with each cell (pretty easy to do), and calling .toString() on it. The problem is that when you store one type (let's say a Long) as actual data in a column, and then define a custom cell factory to display it as a String (it's beyond the scope why you would do that, I just want a method that works with such table views):
TableColumn<MyData, Long> timeColumn;
<...>
timeColumn.setCellFactory(param -> new TableCell<MyData, Long>() {
#Override
protected void updateItem(Long item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
super.setText(null);
} else {
super.setText(LocalDate.from(Instant.ofEpochMilli(item)).format(DateTimeFormatter.ISO_DATE));
}
}
}
);
Those methods based on converting the underlying data (which is Long here) to String will obviously not work, because they will copy a number and not a date (which is what the user sees in the table).
Possible (envisioned) solutions:
If I could get my hand on the TableCell object associated with each table cell, I could do TableCell.getText(), and we are done. Unfortunately, TableView does not allow this (have I missed a way to do it?)
I can easily get the CellFactory associated with the column, and therefore create a new TableCell (identical to that one existing in the table view):
TableCell<T, ?> cell = column.getCellFactory().call(column);
Then the problem is there's no way (again, did I miss it?) to force a TableCell to call the updateItem method! I tried to use commitEdit(T newValue), but it's pretty messy: there are checks inside, so you need to make the whole stuff (column, row, table) Editable, and call startEdit first.
2a. So the only solution that works for me, uses the Reflection to call the protected updateItem, which feels kind of dirty hacking:
// For TableView:
T selectedItem = <...>;
// OR for TreeTableView:
TreeItem<T> selectedItem = <...>;
TableCell<T, Object> cell = (TableCell<T, Object>) column.getCellFactory().call(column);
try {
Method update = cell.getClass().getDeclaredMethod("updateItem", Object.class, boolean.class);
update.setAccessible(true);
Object data = column.getCellData(selectedItem);
update.invoke(cell, data, data == null);
} catch (Exception ex) {
logger.warn("Failed to update item: ", ex);
}
if (cell.getText() != null) {
return cell.getText().replaceAll(fieldSeparator, "");
} else {
return "";
}
I would appreciate any comment on it, namely if this can be achieved with less blood. Or may be indicate some problems with my solution which I missed.
Here's the full code in case someone wants to use it (in spite of its ugliness :)
package com.mycompany.util;
import com.google.common.collect.Lists;
import javafx.beans.property.ObjectProperty;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.scene.input.KeyCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class TableViewCopyable {
private static final Logger logger = LoggerFactory.getLogger(TableViewCopyable.class.getName());
protected final String defaultFieldSep;
protected final String defaultLineSep;
protected TableViewCopyable(String defaultFieldSep, String defaultLineSep) {
this.defaultFieldSep = defaultFieldSep;
this.defaultLineSep = defaultLineSep;
}
protected static final <T> void copyToClipboard(List<T> rows, Function<List<T>, String> extractor) {
logger.info("Copied " + rows.size() + " item(s) to clipboard");
Clipboard.getSystemClipboard().setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, extractor.apply(rows)));
}
public static TableViewCopyable with(String fieldSep, String lineSep) {
return new TableViewCopyable(fieldSep, lineSep);
}
public static TableViewCopyable toCsv() {
// When using System.lineSeparator() as line separator, there appears to be an extra line break :-/
return with(",", "\n");
}
public final <T> void makeCopyable(TableView<T> table, Function<List<T>, String> extractor) {
table.setOnKeyPressed(event -> {
if (event.getCode().equals(KeyCode.C) && event.isControlDown() || event.isControlDown() && event.getCode().equals(KeyCode.INSERT)) {
// "Smart" copying: if single selection, copy all by default. Otherwise copy selected by default
boolean selectedOnly = table.getSelectionModel().getSelectionMode().equals(SelectionMode.MULTIPLE);
copyToClipboard(getItemsToCopy(table, selectedOnly), extractor);
}
});
MenuItem copy = new MenuItem("Copy selected");
copy.setOnAction(event -> copyToClipboard(table.getSelectionModel().getSelectedItems(), extractor));
MenuItem copyAll = new MenuItem("Copy all");
copyAll.setOnAction(event -> copyToClipboard(table.getItems(), extractor));
addToContextMenu(table.contextMenuProperty(), copy, copyAll);
}
public final <T> void makeCopyable(TreeTableView<T> table, Function<List<TreeItem<T>>, String> extractor) {
table.setOnKeyPressed(event -> {
if (event.getCode().equals(KeyCode.C) && event.isControlDown() || event.isControlDown() && event.getCode().equals(KeyCode.INSERT)) {
// "Smart" copying: if single selection, copy all by default. Otherwise copy selected by default
boolean selectedOnly = table.getSelectionModel().getSelectionMode().equals(SelectionMode.MULTIPLE);
copyToClipboard(getItemsToCopy(table, selectedOnly), extractor);
}
});
MenuItem copy = new MenuItem("Copy selected");
copy.setOnAction(event -> copyToClipboard(getItemsToCopy(table, true), extractor));
MenuItem copyAll = new MenuItem("Copy all (expanded only)");
copyAll.setOnAction(event -> copyToClipboard(getItemsToCopy(table, false), extractor));
addToContextMenu(table.contextMenuProperty(), copy, copyAll);
}
protected <T> List<TreeItem<T>> getItemsToCopy(TreeTableView<T> table, boolean selectedOnly) {
if (selectedOnly) {
// If multiple selection is allowed, copy only selected by default:
return table.getSelectionModel().getSelectedItems();
} else {
// Otherwise, copy everything
List<TreeItem<T>> list = Lists.newArrayList();
for (int i = 0; i < table.getExpandedItemCount(); i++) {
list.add(table.getTreeItem(i));
}
return list;
}
}
protected <T> List<T> getItemsToCopy(TableView<T> table, boolean selectedOnly) {
if (selectedOnly) {
// If multiple selection is allowed, copy only selected by default:
return table.getSelectionModel().getSelectedItems();
} else {
return table.getItems();
}
}
protected void addToContextMenu(ObjectProperty<ContextMenu> menu, MenuItem... items) {
if (menu.get() == null) {
menu.set(new ContextMenu(items));
} else {
for (MenuItem item : items) {
menu.get().getItems().add(item);
}
}
}
public final <T> void makeCopyable(TableView<T> table, String fieldSeparator) {
makeCopyable(table, csvVisibleColumns(table, fieldSeparator));
}
public final <T> void makeCopyable(TreeTableView<T> table, String fieldSeparator) {
makeCopyable(table, csvVisibleColumns(table, fieldSeparator));
}
public final <T> void makeCopyable(TableView<T> table) {
makeCopyable(table, csvVisibleColumns(table, defaultFieldSep));
}
public final <T> void makeCopyable(TreeTableView<T> table) {
makeCopyable(table, defaultFieldSep);
}
protected <T> String extractDataFromCell(IndexedCell<T> cell, Object data, String fieldSeparator) {
try {
Method update = cell.getClass().getDeclaredMethod("updateItem", Object.class, boolean.class);
update.setAccessible(true);
update.invoke(cell, data, data == null);
} catch (Exception ex) {
logger.warn("Failed to updated item: ", ex);
}
if (cell.getText() != null) {
return cell.getText().replaceAll(fieldSeparator, "");
} else {
return "";
}
}
public final <T> Function<List<T>, String> csvVisibleColumns(TableView<T> table, String fieldSeparator) {
return (List<T> items) -> {
StringBuilder builder = new StringBuilder();
// Write table header
builder.append(table.getVisibleLeafColumns().stream().map(TableColumn::getText).collect(Collectors.joining(fieldSeparator))).append(defaultLineSep);
items.forEach(item -> builder.append(
table.getVisibleLeafColumns()
.stream()
.map(col -> extractDataFromCell(((TableColumn<T, Object>) col).getCellFactory().call((TableColumn<T, Object>) col), col.getCellData(item), fieldSeparator))
.collect(Collectors.joining(defaultFieldSep))
).append(defaultLineSep));
return builder.toString();
};
}
public final <T> Function<List<TreeItem<T>>, String> csvVisibleColumns(TreeTableView<T> table, String fieldSeparator) {
return (List<TreeItem<T>> items) -> {
StringBuilder builder = new StringBuilder();
// Write table header
builder.append(table.getVisibleLeafColumns().stream().map(TreeTableColumn::getText).collect(Collectors.joining(fieldSeparator))).append(defaultLineSep);
items.forEach(item -> builder.append(
table.getVisibleLeafColumns()
.stream()
.map(col -> extractDataFromCell(((TreeTableColumn<T, Object>) col).getCellFactory().call((TreeTableColumn<T, Object>) col), col.getCellData(item), fieldSeparator))
.collect(Collectors.joining(defaultFieldSep))
).append(defaultLineSep));
return builder.toString();
};
}
}
Then the usage is pretty simple:
TableViewCopyable.toCsv().makeCopyable(someTreeTableView);
TableViewCopyable.toCsv().makeCopyable(someTableView);
Thanks!
Related
This question already has answers here:
What is the recommended way to make a numeric TextField in JavaFX?
(24 answers)
Restricting a TextField input to hexadecimal values in Java FX
(3 answers)
Closed 3 years ago.
I have a credit card page in my java fx program. I am trying to make it so that the inputs only allow numbers. At the moment it only gives an error if the fields are empty. But no error occurs if text is included?
I have tried changing it from String to integer, but that doesn't work.
public void thankyoupage(ActionEvent actionEvent) throws IOException {
String cardno = cardtf.getText();
String expdate1 = expirytf1.getText();
String expdate2 = expirytf2.getText();
String cvvnum = cvvtf.getText();
if (cardno.equals("") || expdate1.equals("") ||
expdate2.equals("") || cvvnum.equals("")) {
Alert alert = new Alert(Alert.AlertType.WARNING, "Enter Full Details", ButtonType.OK);
alert.showAndWait();
} else{
Window mainWindow = confirmbut.getScene().getWindow();
Parent newRoot = FXMLLoader.load(getClass().getResource("Thankyou.fxml"));
mainWindow.getScene().setRoot(newRoot);
}
}
Any links or changes would be nice.
You should attach a TextFormatter to your TextField. I have attached a sample on using Decimals - since you are using money, this might make the most sense.
On your text field you simply add the TextFormatter - this will prevent entry of anything other than what you allow.
//For Example
moneyTextField.setTextFormatter(new DecimalTextFormatter(0, 2));
--Below is the control code.
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.function.UnaryOperator;
import javafx.scene.control.TextFormatter;
import javafx.util.StringConverter;
public class DecimalTextFormatter extends TextFormatter<Number> {
private static DecimalFormat format = new DecimalFormat("#.0;-#.0");
public DecimalTextFormatter(int minDecimals, int maxDecimals) {
super(getStringConverter(minDecimals, maxDecimals), 0, getUnaryOperator(maxDecimals, true,-1));
}
public DecimalTextFormatter(int minDecimals, int maxDecimals, boolean allowsNegative) {
super(getStringConverter(minDecimals, maxDecimals), 0, getUnaryOperator(maxDecimals, allowsNegative,-1));
}
public DecimalTextFormatter(int minDecimals, int maxDecimals, boolean allowsNegative , int maxNoOfDigitsBeforeDecimal) {
super(getStringConverter(minDecimals, maxDecimals), 0, getUnaryOperator(maxDecimals, allowsNegative, maxNoOfDigitsBeforeDecimal));
}
private static StringConverter<Number> getStringConverter(int minDecimals, int maxDecimals) {
return new StringConverter<Number>() {
#Override
public String toString(Number object) {
if (object == null) {
return "";
}
String format = "0.";
for (int i = 0; i < maxDecimals; i++) {
if (i < minDecimals) {
format = format + "0";
} else {
format = format + "#";
}
}
format = format + ";-" + format;
DecimalFormat df = new DecimalFormat(format);
String formatted = df.format(object);
return formatted;
}
#Override
public Number fromString(String string) {
try {
if (string == null) {
return null;
}
return format.parse(string);
} catch (ParseException e) {
return null;
}
}
};
}
private static UnaryOperator<javafx.scene.control.TextFormatter.Change> getUnaryOperator(int maxDecimals,
boolean allowsNegative, int noOfDigitsBeforeDecimal) {
return new UnaryOperator<TextFormatter.Change>() {
#Override
public TextFormatter.Change apply(TextFormatter.Change change) {
if (!allowsNegative && change.getControlNewText().startsWith("-")) {
return null;
}
if (change.getControlNewText().isEmpty()) {
return change;
}
ParsePosition parsePosition = new ParsePosition(0);
Object object = format.parse(change.getControlNewText(), parsePosition);
if (change.getCaretPosition() == 1) {
if (change.getControlNewText().equals(".")) {
return change;
}
}
if (object == null || parsePosition.getIndex() < change.getControlNewText().length()) {
return null;
} else {
if(noOfDigitsBeforeDecimal != -1)
{
int signum = new BigDecimal(change.getControlNewText()).signum();
int precision = new BigDecimal(change.getControlNewText()).precision();
int scale = new BigDecimal(change.getControlNewText()).scale();
int val = signum == 0 ? 1 : precision - scale;
if (val > noOfDigitsBeforeDecimal) {
return null;
}
}
int decPos = change.getControlNewText().indexOf(".");
if (decPos > 0) {
int numberOfDecimals = change.getControlNewText().substring(decPos + 1).length();
if (numberOfDecimals > maxDecimals) {
return null;
}
}
return change;
}
}
};
}
}
You have to use regular expressions to validate fields. You can learn more about regular expression here https://regexr.com/
String cardno = cardtf.getText();
if (cardno.equals("") || expdate1.equals("") || expdate2.equals("") || cvvnum.equals("")) {
Alert alert = new Alert(Alert.AlertType.WARNING, "Enter Full Details", ButtonType.OK);
alert.showAndWait();
}else if (cardno.matches("/^[A-Za-z ]+$/")){
Alert alert = new Alert(Alert.AlertType.WARNING, "It Can not contain letters", ButtonType.OK);
alert.showAndWait();
}else{
//Else Part
}
Here is a piece of code that should help you doing the trick by checking at every input if the text contains only numbers an a maximum of one "," as the decimal separator.
There is already a post showing how to do this.
Post
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
public class NumberField extends TextField {
public NumberField () {
initSpellListener();
}
public final void initSpellListener() {
this.textProperty().addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) -> {
if (!newValue.matches("\\d*")) {
this.setText(newValue.replaceAll("[^\\d,]", ""));/*The comma here "[^\\d,]" can be changed with the dot*/
StringBuilder aus = new StringBuilder();
aus.append(this.getText());
boolean firstPointFound = false;
for (int i = 0; i < aus.length(); i++) {
if (aus.charAt(i) == ',') {/*Change the , with . if you want the . to be the decimal separator*/
if (!firstPointFound) {
firstPointFound = true;
} else {
aus.deleteCharAt(i);
}
}
}
newValue = aus.toString();
this.setText(newValue);
} else {
this.setText(newValue);
}
});
}}
[As soon as I find the post I will credit this code.]
if (!newValue.matches("\\d*"))
this part of the code checks with a regex expression if the new string value doesn't contain only numbers, and then with this code
this.setText(newValue.replaceAll("[^\\d,]", ""));
it replaces all the non-digit or comma chars.
Finally the for-loop checks if only exists one comma ad if other are found they are deleted.
To help you with regex writing here is a very useful site : Online regex
Then you can use this object as a normal TextField:
#FMXL
private NumberField nf;
I have recently discovered bindings and they seem great. I have however stumbled upon a binding I want to make that I cannot seem to figure out. I have a textfield which I want to bind to a double property in a bidirectional way. But, I only want the bind to be from the field to the double property if the text in the field can be converted to a double and if the double it converts to falls within some range. In the other direction I want the bind to be bound without restrictions (I also want to be able to do this for ints but this should be easy once the double one is fixed). I believe this has to be done with a low level bind, doesn't it? How can this be done?
I have just started using bindings and am not great with them so go easy on me.
Many thanks.
In JavaFX bindings simply add listeners and react accordingly. Thinking like that you could argue that the listeners are the "low-level" aspect of the API. To do what you want you will have to create your own listeners. I am not aware of anything that does what you want "out of the box".
An example that is not ready for "production use":
public static void bind(TextField field, DoubleProperty property) {
field.textProperty().addListener((observable, oldText, newText) -> {
try {
// If the text can't be converted to a double then an
// exception is thrown. In this case we do nothing.
property.set(Double.parseDouble(newText));
} catch (NumberFormatException ignore) {}
});
property.addListener((observable, oldNumber, newNumber) -> {
field.setText(Double.toString(newNumber.doubleValue()));
});
}
This will do what you want if I understood your requirements correctly. I believe this code opens up the possibility of a memory leak, however. Ideally, you'd want the listeners to not keep the other from being garbage collected. For instance, if property is no longer strongly referenced then field should not keep property from being GC'd. Edit: This code, depending on the implementations of the ObservableValues, could also enter an infinite loop of updates as pointed out in the comments.
Edit: The first "robust" example I gave had some issues and didn't provide a way to unbind the properties from each other. I've changed the example to make it more correct and also provide said unbinding feature. This new example is based on how the developers of JavaFX handled bidirectional binding internally.
A more robust code example of what I gave above. This is heavily "inspired" by the code used by the standard JavaFX internal APIs. Specifically the class com.sun.javafx.binding.BidirectionalBinding.
import javafx.beans.WeakListener;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.lang.ref.WeakReference;
import java.util.Objects;
public class CustomBindings {
// This code is based heavily on how the standard JavaFX API handles bidirectional bindings. Specifically,
// the class 'com.sun.javafx.binding.BidirectionalBinding'.
public static void bindBidirectional(StringProperty stringProperty, DoubleProperty doubleProperty) {
if (stringProperty == null || doubleProperty == null) {
throw new NullPointerException();
}
BidirectionalBinding binding = new BidirectionalBinding(stringProperty, doubleProperty);
stringProperty.addListener(binding);
doubleProperty.addListener(binding);
}
public static void unbindBidirectional(StringProperty stringProperty, DoubleProperty doubleProperty) {
if (stringProperty == null || doubleProperty == null) {
throw new NullPointerException();
}
// The equals(Object) method of BidirectionalBinding was overridden to take into
// account only the properties. This means that the listener will be removed even
// though it isn't the *same* (==) instance.
BidirectionalBinding binding = new BidirectionalBinding(stringProperty, doubleProperty);
stringProperty.removeListener(binding);
doubleProperty.removeListener(binding);
}
private static class BidirectionalBinding implements ChangeListener<Object>, WeakListener {
private final WeakReference<StringProperty> stringRef;
private final WeakReference<DoubleProperty> doubleRef;
// Need to cache it since we can't hold a strong reference
// to the properties. Also, a changing hash code is never a
// good idea and it needs to be "insulated" from the fact
// the properties can be GC'd.
private final int cachedHashCode;
private boolean updating;
private BidirectionalBinding(StringProperty stringProperty, DoubleProperty doubleProperty) {
stringRef = new WeakReference<>(stringProperty);
doubleRef = new WeakReference<>(doubleProperty);
cachedHashCode = Objects.hash(stringProperty, doubleProperty);
}
#Override
public boolean wasGarbageCollected() {
return stringRef.get() == null || doubleRef.get() == null;
}
#Override
public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
if (!updating) {
StringProperty stringProperty = stringRef.get();
DoubleProperty doubleProperty = doubleRef.get();
if (stringProperty == null || doubleProperty == null) {
if (stringProperty != null) {
stringProperty.removeListener(this);
}
if (doubleProperty != null) {
doubleProperty.removeListener(this);
}
} else {
updating = true;
try {
if (observable == stringProperty) {
updateDoubleProperty(doubleProperty, (String) newValue);
} else if (observable == doubleProperty) {
updateStringProperty(stringProperty, (Number) newValue);
} else {
throw new AssertionError("How did we get here?");
}
} finally {
updating = false;
}
}
}
}
private void updateStringProperty(StringProperty property, Number newValue) {
if (newValue != null) {
property.set(Double.toString(newValue.doubleValue()));
} else {
// set the property to a default value such as 0.0?
property.set("0.0");
}
}
private void updateDoubleProperty(DoubleProperty property, String newValue) {
if (newValue != null) {
try {
property.set(Double.parseDouble(newValue));
} catch (NumberFormatException ignore) {
// newValue is not a valid double
}
}
}
#Override
public int hashCode() {
return cachedHashCode;
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
StringProperty stringProperty1 = stringRef.get();
DoubleProperty doubleProperty1 = doubleRef.get();
if (stringProperty1 == null || doubleProperty1 == null) {
return false;
}
if (obj instanceof BidirectionalBinding) {
BidirectionalBinding other = (BidirectionalBinding) obj;
StringProperty stringProperty2 = other.stringRef.get();
DoubleProperty doubleProperty2 = other.doubleRef.get();
if (stringProperty2 == null || doubleProperty2 == null) {
return false;
}
return stringProperty1 == stringProperty2 && doubleProperty1 == doubleProperty2;
}
return false;
}
}
}
When i double-click the right border of a column's header cell - it automatically adjust the width of a column.
How can i do same programmatically?
Finally I found the solution:
TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();
TableHeaderRow headerRow = skin.getTableHeaderRow();
NestedTableColumnHeader rootHeader = headerRow.getRootHeader();
for (TableColumnHeader columnHeader : rootHeader.getColumnHeaders()) {
try {
TableColumn<?, ?> column = (TableColumn<?, ?>) columnHeader.getTableColumn();
if (column != null) {
Method method = skin.getClass().getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
method.setAccessible(true);
method.invoke(skin, column, 30);
}
} catch (Throwable e) {
e = e.getCause();
e.printStackTrace(System.err);
}
}
Javafx still crude. Many simple things need to do through deep ass...
(not only internal) API changed from fx8 to fx9:
TableViewSkin (along with all skin implementations) moved into public scope: now it would be okay to subclass and implement whatever additional functionality is needed
tableViewSkin.getTableHeaderRow() and tableHeaderRow.getRootHeader() changed from protected to package-private scope, the only way to legally access any is via lookup
implementation of resizeToFitContent moved from skin to a package-private utility class TableSkinUtils, no way to access at all
TableColumnHeader got a private doColumnAutoSize(TableColumnBase, int) that calls the utility method, provided the column's prefWidth has its default value 80. On the bright side: due to that suboptimal api we can grab an arbitrary header and auto-size any column
In code (note: this handles all visible leaf columns as returned by the TableView, nested or not - if you want to include hidden columns, you'll need to collect them as well)
/**
* Resizes all visible columns to fit its content. Note that this does nothing if a column's
* prefWidth is != 80.
*
* #param table
*/
public static void doAutoSize(TableView<?> table) {
// good enough to find an arbitrary column header
// due to sub-optimal api
TableColumnHeader header = (TableColumnHeader) table.lookup(".column-header");
if (header != null) {
table.getVisibleLeafColumns().stream().forEach(column -> doColumnAutoSize(header, column));
}
}
public static void doColumnAutoSize(TableColumnHeader columnHeader, TableColumn column) {
// use your preferred reflection utility method
FXUtils.invokeGetMethodValue(TableColumnHeader.class, columnHeader, "doColumnAutoSize",
new Class[] {TableColumnBase.class, Integer.TYPE},
new Object[] {column, -1});
}
Using the above excellent answer posted by Александр Киберман, I created a class to handle this and the issue of getSkin() == null:
import com.sun.javafx.scene.control.skin.NestedTableColumnHeader;
import com.sun.javafx.scene.control.skin.TableColumnHeader;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import java.lang.reflect.Method;
// ---------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------
public class TableViewPlus extends TableView
{
private boolean flSkinPropertyListenerAdded = false;
// ---------------------------------------------------------------------------------------------------------------------
public TableViewPlus()
{
super();
this.setEditable(false);
}
// ---------------------------------------------------------------------------------------------------------------------
public TableViewPlus(final ObservableList toItems)
{
super(toItems);
this.setEditable(false);
}
// ---------------------------------------------------------------------------------------------------------------------
public void resizeColumnsToFit()
{
if (this.getSkin() != null)
{
this.resizeColumnsPlatformCheck();
}
else if (!this.flSkinPropertyListenerAdded)
{
this.flSkinPropertyListenerAdded = true;
// From https://stackoverflow.com/questions/38718926/how-to-get-tableheaderrow-from-tableview-nowadays-in-javafx
// Add listener to detect when the skin has been initialized and therefore this.getSkin() != null.
this.skinProperty().addListener((a, b, newSkin) -> this.resizeColumnsPlatformCheck());
}
}
// ---------------------------------------------------------------------------------------------------------------------
private void resizeColumnsPlatformCheck()
{
if (Platform.isFxApplicationThread())
{
this.resizeAllColumnsUsingReflection();
}
else
{
Platform.runLater(this::resizeAllColumnsUsingReflection);
}
}
// ---------------------------------------------------------------------------------------------------------------------
// From https://stackoverflow.com/questions/38090353/javafx-how-automatically-width-of-tableview-column-depending-on-the-content
// Geesh. . . .
private void resizeAllColumnsUsingReflection()
{
final TableViewSkin<?> loSkin = (TableViewSkin<?>) this.getSkin();
// The skin is not applied till after being rendered. Which is happening with the About dialog.
if (loSkin == null)
{
System.err.println("Skin is null");
return;
}
final TableHeaderRow loHeaderRow = loSkin.getTableHeaderRow();
final NestedTableColumnHeader loRootHeader = loHeaderRow.getRootHeader();
for (final TableColumnHeader loColumnHeader : loRootHeader.getColumnHeaders())
{
try
{
final TableColumn<?, ?> loColumn = (TableColumn<?, ?>) loColumnHeader.getTableColumn();
if (loColumn != null)
{
final Method loMethod = loSkin.getClass().getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
loMethod.setAccessible(true);
loMethod.invoke(loSkin, loColumn, 30);
}
}
catch (final Throwable loErr)
{
loErr.printStackTrace(System.err);
}
}
}
// ---------------------------------------------------------------------------------------------------------------------
}
// ---------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------
table.skinProperty().addListener((a, b, newSkin) -> {
TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();
TableHeaderRow headerRow = skin.getTableHeaderRow();
NestedTableColumnHeader rootHeader = headerRow.getRootHeader();
for (TableColumnHeader columnHeader : rootHeader.getColumnHeaders()) {
try {
TableColumn<?, ?> column = (TableColumn<?, ?>) columnHeader.getTableColumn();
if (column != null) {
Method method = skin.getClass().getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
method.setAccessible(true);
method.invoke(skin, column, 30);
}
} catch (Throwable e) {
e = e.getCause();
e.printStackTrace(System.err);
}
}
});
In order to prevent getSkin() == null;
I'm using Spinner from 8u40b17.
SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 100);
Spinner sp = new Spinner();
sp.setValueFactory(svf);
sp.setEditable(true);
sp.setPrefWidth(80);
I noticed that when I enter some value from keyboard and I increase the upper value the expected number is not the next. Instead of this it's the next default value. How I can fix this?
For example: if I have 5 as default value and I enter 34, then press the upper arrow I expect to get 35 by actually get 6.
I had the same problem with the spinner control. Your bug has been documented here: JDK-8094205
Here is the last comment:
Jonathan Giles added a comment - Dec, 15 2014 12:59 AM
Fixed locally in my repo, will push to the 8u60 repo this week once it
opens. Now the text editor input is committed when increment /
decrement are called (although the value is still not committed when
focus is lost).
Unit tests:
javafx.scene.control.SpinnerTest.test_rt_39655_decrement()
javafx.scene.control.SpinnerTest.test_rt_39655_increment()
The changeset: http://hg.openjdk.java.net/openjfx/8u-dev/rt/rev/89ca7d3f699e
Here is my take on an Autocommit spinner. This one will auto commit anything that the factory will accept.
public class SpinnerAutoCommit<T> extends Spinner<T> {
public SpinnerAutoCommit() {
super();
addListenerKeyChange();
}
public SpinnerAutoCommit(int min, int max, int initialValue) {
super(min, max, initialValue);
addListenerKeyChange();
}
public SpinnerAutoCommit(int min, int max, int initialValue, int amountToStepBy) {
super(min, max, initialValue, amountToStepBy);
addListenerKeyChange();
}
public SpinnerAutoCommit(double min, double max, double initialValue) {
super(min, max, initialValue);
addListenerKeyChange();
}
public SpinnerAutoCommit(double min, double max, double initialValue, double amountToStepBy) {
super(min, max, initialValue, amountToStepBy);
addListenerKeyChange();
}
public SpinnerAutoCommit(ObservableList<T> items) {
super(items);
addListenerKeyChange();
}
public SpinnerAutoCommit(SpinnerValueFactory<T> valueFactory) {
super(valueFactory);
addListenerKeyChange();
}
private void addListenerKeyChange() {
getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
commitEditorText();
});
}
private void commitEditorText() {
if (!isEditable()) return;
String text = getEditor().getText();
SpinnerValueFactory<T> valueFactory = getValueFactory();
if (valueFactory != null) {
StringConverter<T> converter = valueFactory.getConverter();
if (converter != null) {
T value = converter.fromString(text);
valueFactory.setValue(value);
}
}
}
}
By design, the changes in the textfield of the Spinner control are commited only when the user hits ENTER key, via action handler:
getEditor().setOnAction(action -> {
String text = getEditor().getText();
SpinnerValueFactory<T> valueFactory = getValueFactory();
if (valueFactory != null) {
StringConverter<T> converter = valueFactory.getConverter();
if (converter != null) {
T value = converter.fromString(text);
valueFactory.setValue(value);
}
}
});
Note that if the typed value can't be converted, this will throw a NumberFormatException, keeping the wrong value in the textfield.
We can provide our own implementation, listening to other keys, like TAB key, via event filter, and at the same time, and in case of exception, restore the last valid value.
Something like this:
private final Spinner sp = new Spinner();
#Override
public void start(Stage primaryStage) {
SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 100);
sp.setValueFactory(svf);
sp.setEditable(true);
sp.setPrefWidth(80);
// Commit on TAB
sp.addEventFilter(KeyEvent.ANY, e->{
if (sp.isEditable() && e.getCode().equals(KeyCode.TAB)) {
doCommit();
e.consume();
}
});
// Override Commit on ENTER
sp.getEditor().setOnAction(e->{
if(sp.isEditable()) {
doCommit();
e.consume();
}
});
Scene scene = new Scene(new StackPane(sp), 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
/*
Commit new value, checking conversion to integer,
restoring old valid value in case of exception
*/
private void doCommit(){
String text = sp.getEditor().getText();
SpinnerValueFactory<Integer> valueFactory = sp.getValueFactory();
if (valueFactory != null) {
StringConverter<Integer> converter = valueFactory.getConverter();
if (converter != null) {
try{
Integer value = converter.fromString(text);
valueFactory.setValue(value);
} catch(NumberFormatException nfe){
sp.getEditor().setText(converter.toString(valueFactory.getValue()));
}
}
}
}
This solved the problem for me but it relys on Apache Commons Validator to validate entered value in the spinner (org.apache.commons.validator.GenericValidator)
valueSpinner.getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
try {
if (GenericValidator.isInt(newValue)) {
valueSpinner.getValueFactory().setValue(Integer.parseInt(newValue));
}
} catch (NumberFormatException e) {
if (GenericValidator.isInt(oldValue)) {
valueSpinner.getValueFactory().setValue(Integer.parseInt(oldValue));
}
}
});
Edit :-
You can validate the value without using Apache Commons Validator like this example :-
private boolean isInteger(String value) {
if (value == null) {
return false;
}
try {
new Integer(value);
return true;
} catch (NumberFormatException e) {
return false;
}
}
valueSpinner.getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
try {
if (isInteger(newValue)) {
valueSpinner.getValueFactory().setValue(Integer.parseInt(newValue));
}
} catch (NumberFormatException e) {
if (isInteger(oldValue)) {
valueSpinner.getValueFactory().setValue(Integer.parseInt(oldValue));
}
}
});
I am using Caliburn micro(1.3)/MVVM and Silverlight. When I update the itemsource RadGridView, I lose the selected items. I found a blog about implementing a behavior to save the selected items when you are implementing MVVM. I can get the selected items, but I cannot set them back once the itemsource is refreshed. Can someoneshow me how to implement this using caliburn.micro and the RadGridVIew? I think the best way to go is to create a caliburn micro convention, but I can only find a reference for creating a convention for selectedItem, not selectedItems.
Can someone show me how to accomplish this? I tried the following, but it does not work.
private static void SetRadGridSelecteditemsConventions()
{
ConventionManager
.AddElementConvention<DataControl>(DataControl.ItemsSourceProperty, "SelectedItem", "SelectionChanged")
.ApplyBinding = (viewModelType, path, property, element, convention) =>
{
ConventionManager.SetBinding(viewModelType, path, property, element, convention, DataControl.ItemsSourceProperty);
if (ConventionManager.HasBinding(element, DataControl.SelectedItemProperty))
return true;
var index = path.LastIndexOf('.');
index = index == -1 ? 0 : index + 1;
var baseName = path.Substring(index);
foreach (var selectionPath in
from potentialName in ConventionManager.DerivePotentialSelectionNames(baseName)
where viewModelType.GetProperty(potentialName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) != null
select path.Replace(baseName, potentialName))
{
var binding = new Binding(selectionPath) { Mode = BindingMode.TwoWay };
BindingOperations.SetBinding(element, DataControl.SelectedItemProperty, binding);
}
return true;
};
}
Thanks,
Stephane
You should use a behavior for this since the SelectedItems property is readonly.
Telerik has an example for this, only the example is not specific for caliburn.micro.
If you add the following class to your project:
public class MultiSelectBehavior : Behavior<RadGridView>
{
public INotifyCollectionChanged SelectedItems
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(INotifyCollectionChanged), typeof(MultiSelectBehavior), new PropertyMetadata(OnSelectedItemsPropertyChanged));
private static void OnSelectedItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
var collection = args.NewValue as INotifyCollectionChanged;
if (collection != null)
{
collection.CollectionChanged += ((MultiSelectBehavior)target).ContextSelectedItems_CollectionChanged;
}
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItems.CollectionChanged += GridSelectedItems_CollectionChanged;
}
void ContextSelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UnsubscribeFromEvents();
Transfer(SelectedItems as IList, AssociatedObject.SelectedItems);
SubscribeToEvents();
}
void GridSelectedItems_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UnsubscribeFromEvents();
Transfer(AssociatedObject.SelectedItems, SelectedItems as IList);
SubscribeToEvents();
}
private void SubscribeToEvents()
{
AssociatedObject.SelectedItems.CollectionChanged += GridSelectedItems_CollectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged += ContextSelectedItems_CollectionChanged;
}
}
private void UnsubscribeFromEvents()
{
AssociatedObject.SelectedItems.CollectionChanged -= GridSelectedItems_CollectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged -= ContextSelectedItems_CollectionChanged;
}
}
public static void Transfer(IList source, IList target)
{
if (source == null || target == null)
return;
target.Clear();
foreach (var o in source)
{
target.Add(o);
}
}
}
This behavior takes care of the synchronization between collection RadGridView.SelectedItems and MultiSelectBehavior.SelectedItems.
Now we need to have an ObservableCollection in the ViewModel
//Collection holding the selected items
private ObservableCollection<object> selectedGridItems;
public ObservableCollection<object> SelectedGridItems
{
get
{
if (selectedGridItems == null)
selectedGridItems = new ObservableCollection<object>();
return selectedGridItems;
}
set
{
if (selectedGridItems == value) return;
selectedGridItems = value;
NotifyOfPropertyChange(() => SelectedGridItems);
}
}
//Deselect all selected items in the gridview
public void ClearSelectedGridItems()
{
SelectedGridItems.Clear();
}
Last thing is bind the behavior in the view
<telerik:RadGridView x:Name="CustomLogs" AutoGenerateColumns="true" SelectionMode="Extended">
<i:Interaction.Behaviors>
<local:MultiSelectBehavior SelectedItems="{Binding SelectedGridItems}"/>
</i:Interaction.Behaviors>
</telerik:RadGridView>
Thats it, hope it helps you!