Prevent column width resizing - javafx

How can I prevent column width resizing? I have a table, and I'm wondering if I can simply disable the option to drag to resize width of the columns. Is there any way to do this, or will I have to manually set the max and min width of each column just so that it can't be manipulated by the user? The problem with this is that the text in the title/header of my table is smaller than what's in the row underneath it. Setting the width to the column width forces it smaller, and then you can't see the info in the cell underneath.
Also, if there's a way to disable rearranging the columns, that'd be nice too. I basically want what I have set to not be changed at all. If this can't be done, I might just replace each table with an image of itself, so that it can't be manipulated at all.

Prevent Resizing Columns
From here, we can know that setResizable(boolean) allows you to choose whether the user can resize a column. Setting the max and min width to the same value does prevents the user from resizing the column, but not a preferred method. Also, the user will see the resizing cursor but not the default cursor when attempting to resize the column.
Prevent Reordering Columns
JavaFX 9
For preventing the user from reordering the columns, there isn't a straight-forward solution until JavaFX 9, which introduces setReorderable(boolean), isReorderable(), reorderableProperty methods, and the reorderable field in the TableColumnBase class. Here is a snippet of the source code:
package javafx.scene.control;
//some imports and JavaDoc comments
#IDProperty("id")
public abstract class TableColumnBase<S,T> implements EventTarget, Styleable {
//some code
// --- Reorderable
/**
* A boolean property to toggle on and off the 'reorderability' of this column
* (with drag and drop - reordering by modifying the appropriate <code>columns</code>
* list is always allowed). When this property is true, this column can be reordered by
* users simply by dragging and dropping the columns into their desired positions.
* When this property is false, this ability to drag and drop columns is not available.
*
* #since 9
*/
private BooleanProperty reorderable;
public final BooleanProperty reorderableProperty() {
if (reorderable == null) {
reorderable = new SimpleBooleanProperty(this, "reorderable", true);
}
return reorderable;
}
public final void setReorderable(boolean value) {
reorderableProperty().set(value);
}
public final boolean isReorderable() {
return reorderable == null ? true : reorderable.get();
}
//some code
}
If your application bases on JavaFX 9, then you are lucky. Simply invoke setReorderable(false) on your desired table column and there you go.
JavaFX 8
If your application bases on JavaFX 8 or older versions, you can use com.sun.javafx.scene.control.skin.TableHeaderRow, which has isReordering, setReordering, reorderingProperty methods, and reorderingProperty field. Here is a snippet of the source code:
package com.sun.javafx.scene.control.skin;
//some imports and JavaDoc comments
public class TableHeaderRow extends StackPane {
//some code
private BooleanProperty reorderingProperty = new BooleanPropertyBase() {
#Override protected void invalidated() {
TableColumnHeader r = getReorderingRegion();
if (r != null) {
double dragHeaderHeight = r.getNestedColumnHeader() != null ?
r.getNestedColumnHeader().getHeight() :
getReorderingRegion().getHeight();
dragHeader.resize(dragHeader.getWidth(), dragHeaderHeight);
dragHeader.setTranslateY(getHeight() - dragHeaderHeight);
}
dragHeader.setVisible(isReordering());
}
#Override
public Object getBean() {
return TableHeaderRow.this;
}
#Override
public String getName() {
return "reordering";
}
};
public final void setReordering(boolean value) { reorderingProperty().set(value); }
public final boolean isReordering() { return reorderingProperty.get(); }
public final BooleanProperty reorderingProperty() { return reorderingProperty; }
//some code
}
The methods and fields work the same as the one in TableColumnBase in JavaFX 9, just with different names.
You want to obtain the TableHeaderRow object as a children of the skin of the TableView:
TableView<MyType> table = new TableView<MyType>();
//some code
//DISPLAY THE TABLE OR GETSKIN WILL RETURN NULL
com.sun.javafx.scene.control.skin.TableHeaderRow header = null;
for (Node node : table.getSkin().getChildren())
if (node instanceof com.sun.javafx.scene.control.skin.TableHeaderRow)
header = (com.sun.javafx.scene.control.skin.TableHeaderRow) node;
if (header == null); //Table not rendered
header.setReorderable(false);
You must have the TableView rendered before accessing the skin, because the TableViewSkin obtained from TableView.getSkin() is a visual representation of user interface controls. As the official JavaFx JavaDoc says, Skin is a
Base class for defining the visual representation of user interface
controls by defining a scene graph of nodes to represent the skin. A
user interface control is abstracted behind the Skinnable interface.
Therefore, the Skin will be null if the TableView is not rendered because there is nothing visual to represent.
Note that the second method cannot be used in Java 9 or later due to the modules in Java API block your access towards any sun packages.
EDIT:
In JavaFX 8 or older, there is a method called impl_setReorderable(boolean) which is deprecated, but works flawlessly, pretty much same as setReorderable(boolean) in JavaFX 9.

Related

Using setRowFactory to style rows doesn't work on visible rows (JavaFX 11)

I have a TableView which is updated from an ObservableList. It has two columns. When a file is loaded, the list is populated and the table updates, (initially just the first column is populated). After validation of the items in the list the second column is populated with a success or failure flag. Using the setRowFactory I update the background style of the row to either green for success or red for failure. Some items don't get validated and are styled with "". The table has about a dozen rows visible out of a couple of thousand rows total. The problem I have is that the visible rows don't get their background style updated until they're scrolled out of view and then back in again.
I've been able to overcome this by using the table's refresh() method, but that causes another problem. The first column is editable to allow the data to be corrected before re-validation. If the refresh() method is used then it breaks the ability to edit a cell. The textfield still appears, but is disabled, (no focus border and no ability to highlight or edit its content).
If I leave out the refresh() method editing works just fine. Include the refresh() and the table displays correctly without the need for scrolling, but editing is broken.
So I can either have editable cells or properly displayed rows, but not both. Apart from this problem the code works fine. I've read countless examples and TableView issues, and associated solutions, and nothing I've tried has fixed the problem. In my efforts I can see that the overriden updateItem method is only ever called when the row is redrawn after becoming visible again. My thinking is that I need another mechanism to style the rows on the validationResponse change but this is where I get stuck.
So my question is how to have the visible table rows get their style updated without scrolling while not breaking cell editing? Thanks!!
Edit:
Reproducible code example follows. Click the first button to populate the table with initial data. Click the second button to simulate validation. The second column will update with the validation response, but the styling doesn't take effect until the rows are scrolled out of view and then back in to view. At this point first column is editable. If you uncomment the tblGCode.refresh() line and re-run the test the styling is applied immediately without scrolling, but editing a cell in the first column no longer works.
Main class:
public class TableViewTest extends Application {
private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList(
item -> new Observable[]{item.validatedProperty(), item.errorDescriptionProperty()});
private final TableView tblGCode = new TableView();
#Override
public void start(Stage stage) {
TableColumn<GCodeItem, String> colGCode = new TableColumn<>("GCode");
colGCode.setCellValueFactory(new PropertyValueFactory<>("gcode"));
TableColumn<GCodeItem, String> colStatus = new TableColumn<>("Status");
colStatus.setCellValueFactory(new PropertyValueFactory<>("validationResponse"));
// Set first column to be editable
tblGCode.setEditable(true);
colGCode.setEditable(true);
colGCode.setCellFactory(TextFieldTableCell.forTableColumn());
colGCode.setOnEditCommit((TableColumn.CellEditEvent<GCodeItem, String> t) -> {
((GCodeItem) t.getTableView().getItems().get(t.getTablePosition().getRow())).setGcode(t.getNewValue());
});
// Set row factory
tblGCode.setRowFactory(tbl -> new TableRow<GCodeItem>() {
private final Tooltip tip = new Tooltip();
{
tip.setShowDelay(new Duration(250));
}
#Override
protected void updateItem(GCodeItem item, boolean empty) {
super.updateItem(item, empty);
if(item == null || empty) {
setStyle("");
setTooltip(null);
} else {
if(item.isValidated()) {
if(item.hasError()) {
setStyle("-fx-background-color: #ffcccc"); // red
tip.setText(item.getErrorDescription());
setTooltip(tip);
} else {
setStyle("-fx-background-color: #ccffdd"); // green
setTooltip(null);
}
} else {
setStyle("");
setTooltip(null);
}
}
//tblGCode.refresh(); // this works to give desired styling, but breaks editing
}
});
tblGCode.getColumns().setAll(colGCode, colStatus);
tblGCode.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
// buttons to simulate issue
Button btnPopulate = new Button("1. Populate Table");
btnPopulate.setOnAction(eh -> populateTable());
Button btnValidate = new Button("2. Validate Table");
btnValidate.setOnAction(eh -> simulateValidation());
var scene = new Scene(new VBox(tblGCode, btnPopulate, btnValidate), 640, 320);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
private void populateTable() {
// simulates updating of ObservableList with first couple of dozen lines of a file
gcodeItems.add(new GCodeItem("(1001)"));
gcodeItems.add(new GCodeItem("(T4 D=0.25 CR=0 - ZMIN=-0.4824 - flat end mill)"));
gcodeItems.add(new GCodeItem("G90 G94"));
gcodeItems.add(new GCodeItem("G17"));
gcodeItems.add(new GCodeItem("G20"));
gcodeItems.add(new GCodeItem("G28 G91 Z0"));
gcodeItems.add(new GCodeItem("G90"));
gcodeItems.add(new GCodeItem(""));
gcodeItems.add(new GCodeItem("(Face1)"));
gcodeItems.add(new GCodeItem("T4 M6"));
gcodeItems.add(new GCodeItem("S5000 M3"));
gcodeItems.add(new GCodeItem("G54"));
gcodeItems.add(new GCodeItem("M8"));
gcodeItems.add(new GCodeItem("G0 X1.3842 Y-1.1452"));
gcodeItems.add(new GCodeItem("Z0.6"));
gcodeItems.add(new GCodeItem("Z0.2"));
gcodeItems.add(new GCodeItem("G1 Z0.015 F20"));
gcodeItems.add(new GCodeItem("G18 G3 X1.3592 Z-0.01 I-0.025 K0"));
gcodeItems.add(new GCodeItem("G1 X1.2492"));
gcodeItems.add(new GCodeItem("X-1.2492 F40"));
gcodeItems.add(new GCodeItem("X-1.25"));
gcodeItems.add(new GCodeItem("G17 G2 X-1.25 Y-0.9178 I0 J0.1137"));
gcodeItems.add(new GCodeItem("G1 X1.25"));
gcodeItems.add(new GCodeItem("G3 X1.25 Y-0.6904 I0 J0.1137"));
// Add list to table
tblGCode.setItems(gcodeItems);
}
private void simulateValidation() {
// sets validationResponse on certain rows (not every row is validated)
gcodeItems.get(2).setValidationResponse("ok");
gcodeItems.get(3).setValidationResponse("ok");
gcodeItems.get(4).setValidationResponse("ok");
gcodeItems.get(5).setValidationResponse("ok");
gcodeItems.get(6).setValidationResponse("ok");
gcodeItems.get(9).setValidationResponse("error:20");
gcodeItems.get(10).setValidationResponse("ok");
gcodeItems.get(11).setValidationResponse("ok");
gcodeItems.get(12).setValidationResponse("ok");
gcodeItems.get(13).setValidationResponse("ok");
gcodeItems.get(14).setValidationResponse("ok");
gcodeItems.get(15).setValidationResponse("ok");
gcodeItems.get(16).setValidationResponse("ok");
gcodeItems.get(17).setValidationResponse("ok");
gcodeItems.get(18).setValidationResponse("ok");
gcodeItems.get(19).setValidationResponse("ok");
gcodeItems.get(20).setValidationResponse("ok");
gcodeItems.get(21).setValidationResponse("ok");
gcodeItems.get(22).setValidationResponse("ok");
gcodeItems.get(23).setValidationResponse("ok");
}
}
GCodeItem model:
public class GCodeItem {
private final SimpleStringProperty gcode;
private final SimpleStringProperty validationResponse;
private ReadOnlyBooleanWrapper validated;
private ReadOnlyBooleanWrapper hasError;
private ReadOnlyIntegerWrapper errorNumber;
private ReadOnlyStringWrapper errorDescription;
public GCodeItem(String gcode) {
this.gcode = new SimpleStringProperty(gcode);
this.validationResponse = new SimpleStringProperty("");
this.validated = new ReadOnlyBooleanWrapper();
this.hasError = new ReadOnlyBooleanWrapper();
this.errorNumber = new ReadOnlyIntegerWrapper();
this.errorDescription = new ReadOnlyStringWrapper();
validated.bind(Bindings.createBooleanBinding(
() -> ! "".equals(getValidationResponse()),
validationResponse
));
hasError.bind(Bindings.createBooleanBinding(
() -> ! ("ok".equals(getValidationResponse()) ||
"".equals(getValidationResponse())),
validationResponse
));
errorNumber.bind(Bindings.createIntegerBinding(
() -> {
String vResp = getValidationResponse();
if ("ok".equals(vResp)) {
return 0;
} else {
// should handle potential exceptions here...
if(vResp.contains(":")) {
int en = Integer.parseInt(vResp.split(":")[1]);
return en ;
} else {
return 0;
}
}
}, validationResponse
));
errorDescription.bind(Bindings.createStringBinding(
() -> {
int en = getErrorNumber() ;
return GrblDictionary.getErrorDescription(en);
}, errorNumber
));
}
public final String getGcode() {
return gcode.get();
}
public final void setGcode(String value) {
gcode.set(value);
}
public SimpleStringProperty gcodeProperty() {
return this.gcode;
}
public final String getValidationResponse() {
return validationResponse.get();
}
public final void setValidationResponse(String value) {
validationResponse.set(value);
}
public SimpleStringProperty validationResponseProperty() {
return this.validationResponse;
}
public Boolean isValidated() {
return validatedProperty().get();
}
public ReadOnlyBooleanProperty validatedProperty() {
return validated.getReadOnlyProperty();
}
// ugly method name to conform to method naming pattern:
public final boolean isHasError() {
return hasErrorProperty().get();
}
// better method name:
public final boolean hasError() {
return isHasError();
}
public ReadOnlyBooleanProperty hasErrorProperty() {
return hasError.getReadOnlyProperty();
}
public final int getErrorNumber() {
return errorNumberProperty().get();
}
public ReadOnlyIntegerProperty errorNumberProperty() {
return errorNumber.getReadOnlyProperty() ;
}
public final String getErrorDescription() {
return errorDescriptionProperty().get();
}
public ReadOnlyStringProperty errorDescriptionProperty() {
return errorDescription.getReadOnlyProperty();
}
}
Supporting dictionary class (abridged):
public class GrblDictionary {
private static final Map<Integer, String> ERRORS = Map.ofEntries(
entry(1, "G-code words consist of a letter and a value. Letter was not found."),
entry(2, "Numeric value format is not valid or missing an expected value."),
entry(17, "Laser mode requires PWM outentry."),
entry(20, "Unsupported or invalid g-code command found in block."),
entry(21, "More than one g-code command from same modal group found in block."),
entry(22, "Feed rate has not yet been set or is undefined.")
);
public static String getErrorDescription(int errorNumber) {
return ERRORS.containsKey(errorNumber) ? ERRORS.get(errorNumber) : "Unrecognized error number.";
}
}
Edit #2:
If I replace the TableView.setRowFactory code with TableColumn.setCellFactory as shown below I get the desired effect and editing still works. Is this a sensible solution, or should I really be using setRowFactory and getting the list changes recognised correctly by setRowFactory? In my testing it only ever seemed like the overriden updateItem method was being called when rows scrolled in to view.
colStatus.setCellFactory(tc -> new TableCell<GCodeItem, String>() {
private final Tooltip tip = new Tooltip();
{
tip.setShowDelay(new Duration(250));
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
TableRow<GCodeItem> row = this.getTableRow();
GCodeItem rowItem = row.getItem();
if(item == null || empty) {
row.setStyle("");
row.setTooltip(null);
} else {
if(rowItem.isValidated()) {
if(rowItem.hasError()) {
row.setStyle("-fx-background-color: #ffcccc"); // red
tip.setText(rowItem.getErrorDescription());
row.setTooltip(tip);
} else {
row.setStyle("-fx-background-color: #ccffdd"); // green
row.setTooltip(null);
}
} else {
row.setStyle("");
row.setTooltip(null);
}
setText(item);
}
}
});
Edit #3:
Many thanks to kleopatra and James_D I now have a solution. Overriding isItemChanged() in the row factory has solved my issue.
The place to install conditional row styling is a custom TableRow - nowhere else. As always, contained nodes - like tableCells here - must not interfere with their parent's state, never-ever!.
The base problem with such styling in a tableRow is that row.updateItem(...) is not called when we might expect it, in particular, not after an update of a property. There are two options to solve (apart from making sure that the table is notified at all on updates of properties not shown in columns by using an extractor as already suggested by James)
A quick option is to unconditionally force an update always, by overriding isItemChanged:
#Override
protected boolean isItemChanged(GCodeItem oldItem,
GCodeItem newItem) {
return true;
}
Another option is to update the styling in both updateItem(...) and updateIndex(...) (the latter is called always when anything chances in the data)
#Override
protected void updateIndex(int i) {
super.updateIndex(i);
doUpdateItem(getItem());
}
#Override
protected void updateItem(CustomItem item, boolean empty) {
super.updateItem(item, empty);
doUpdateItem(item);
}
protected void doUpdateItem(CustomItem item) {
// actually do the update and styling
}
Choosing between both depends on context and requirements. Have seen contexts where the one or other didn't work properly, without a clean indication when/why that happened (too lazy to really dig ;)
Aside - a couple of comments to the question which did improve considerably over time but still is not quite a [MCVE]:
the data item is both too complex (for basic styling, there's no need for several direct/indirect intertwined conditions) and not complete enough to really demonstrate the requirements (like update after editing the value that drives the error condition)
the data item exposes properties (good thing!) - so use those (vs. PropertyValueFactory, bad thing!)
with a writable property a custom edit commit handler is not needed
TableColumn is editable by default, making col.setEditable(true) a no-op. If only some columns should editable, the others must be set to false
The basic issue is that the table is not forcing updates on the table row when the relevant properties change. Using the "extractor" as you do with
private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList(
item -> new Observable[]{item.validatedProperty(), item.errorDescriptionProperty()});
should work, but it seems the table does not force row updates when the underlying data list fires updated type changes. (I'd consider this a bug; it's possible the JavaFX team simply doesn't consider this a supported feature.)
One approach here is to have the TableRow register a listener with the current item's validationResponseProperty() (or any other desired property), and update the row when it changes. A little care is needed here, because the current item that the row displays can change (e.g. when scrolling or when the data in the list change), so you need to observe the itemProperty() and ensure the listener is registered with the property in the correct item. This looks like:
// Set row factory
tblGCode.setRowFactory(tbl -> new TableRow<GCodeItem>() {
private final Tooltip tip = new Tooltip();
private final ChangeListener<String> listener = (obs, oldValidationResponse, newValidationResponse) ->
updateStyleAndTooltip();
{
tip.setShowDelay(new Duration(250));
itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.validationResponseProperty().removeListener(listener);
}
if (newItem != null) {
newItem.validationResponseProperty().addListener(listener);
}
updateStyleAndTooltip();
});
}
#Override
protected void updateItem(GCodeItem item, boolean empty) {
super.updateItem(item, empty);
updateStyleAndTooltip();
}
private void updateStyleAndTooltip() {
GCodeItem item = getItem();
if(item == null || isEmpty()) {
setStyle("");
setTooltip(null);
} else {
if(item.isValidated()) {
if(item.hasError()) {
setStyle("-fx-background-color: #ffcccc"); // red
tip.setText(item.getErrorDescription());
setTooltip(tip);
} else {
setStyle("-fx-background-color: #ccffdd"); // green
setTooltip(null);
}
} else {
setStyle("");
setTooltip(null);
}
}
}
});
Note now you no longer need the list created with the extractor:
private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList();
and indeed this would work without the dependent properties being implemented as JavaFX (bound) properties (as long as they are kept consistent with the other data); though I still consider the version you currently have to be the better implementation.
BTW, as a brief aside, your style will work better if you use -fx-background instead of -fx-background-color. By default, the background color (-fx-background-color) of a row is set equal to -fx-background. However, the color of the text is made dependent on -fx-background: if -fx-background is light, then a dark text is used, and vice-versa. By default, selecting a row changes -fx-background, which results in a change in text color, so in your implementation you'll notice the text is hard to read in a selected (validated or error) row. In short, modifying -fx-background will play better with selection than modifying -fx-background-color.

How do I keep my Editor from scrolling below keyboard when moving the cursor?

I've got an Editor which is tall enough to accommodate multiple lines of input. The editor is at the bottom of the screen if it matters. Once multiple lines are entered, if you move the cursor up a line, the entire view shifts downward, so the line you just left is now obscured by the keyboard. I'd like it to not do this unless the line the cursor is on is either off screen or close to being off screen. Sort of like how the built in Android message app works. Here's what I mean in pictures.
Default state, everything looks good. We have 3 lines of input
I move the cursor up one line: notice that the entire view has shifted down one line and so "line 3" is now obscured. I don't want this behavior (it among other things hides some UI elements).
This is the Android messaging app. This is the behavior I want: when you move the cursor to that next line, the view doesn't just shift downward. If you have enough lines to scroll past the visible area, Android just shifts the text and not the entire view to accommodate it.
In essence, I just want the contents of the editor to shift (when appropriate) and not the entire window.
I managed to figure it out. Firstly, in MainActivity.cs, after the LoadApplication call, I added this:
Xamarin.Forms.Application.Current.On<Xamarin.Forms.PlatformConfiguration.Android>().UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize);
Using the default or .Pan didn't work at all. Note that just doing this didn't fix the issue. I also had to take the class that's described here and use it. Without that class, things didn't work correctly. Also the "alternative method" described on that page didn't work (which was basically just changing the soft input mode adjust).
In the event that the link I mentioned dies, here's the class. I did slightly modify it so that by default it is not enabled. You just set Disabled = false on it to enable it. Also you call the static method Init to initialize the class.
public class AndroidBug5497WorkaroundForXamarinAndroid
{
private readonly View _childOfContent;
private readonly FrameLayout.LayoutParams _frameLayoutParams;
private int _usableHeightPrevious;
public static AndroidBug5497WorkaroundForXamarinAndroid Instance { get; private set; }
private bool _disabled = true;
public bool Disabled
{
get => _disabled;
set
{
if (_disabled != value)
{
_disabled = value;
if (_disabled)
_childOfContent.ViewTreeObserver.GlobalLayout -= PossiblyResizeChildOfContent;
else
_childOfContent.ViewTreeObserver.GlobalLayout += PossiblyResizeChildOfContent;
}
}
}
public static void Init(Activity activity)
{
if (Instance != null)
return;
Instance = new AndroidBug5497WorkaroundForXamarinAndroid(activity);
}
private AndroidBug5497WorkaroundForXamarinAndroid(Activity activity)
{
FrameLayout content = (FrameLayout)activity.FindViewById(Android.Resource.Id.Content);
_childOfContent = content.GetChildAt(0);
_frameLayoutParams = (FrameLayout.LayoutParams)_childOfContent.LayoutParameters;
}
private void PossiblyResizeChildOfContent(object sender, EventArgs e)
{
int usableHeightNow = ComputeUsableHeight();
if (usableHeightNow != _usableHeightPrevious)
{
var usableHeightSansKeyboard = _childOfContent.RootView.Height;
var heightDifference = usableHeightSansKeyboard - usableHeightNow;
_frameLayoutParams.Height = usableHeightSansKeyboard - heightDifference;
_childOfContent.RequestLayout();
_usableHeightPrevious = usableHeightNow;
}
}
private int ComputeUsableHeight()
{
Rect r = new Rect();
_childOfContent.GetWindowVisibleDisplayFrame(r);
return (r.Bottom - r.Top);
}

JavaFX - using setRowFactory to highlight new rows

I am writing a JavaFX app where a series of messages appear in a TableView. When a new message appears, its row in the table should be highlighted, meaning its background color should be orange or something. Once the user clicks it, the background color should clear, acknowledging the message was read. Should be simple.
I've done enough research to realize that I need to use a rowFactory to set or clear a row's background. But I'm struggling with the mechanics of setRowFactory(). The documentation on Oracle is over my head, and every example I pull up online seems radically different than the last one.
Here's what I have:
public class Message {
private boolean readOnce;
private int date;
private String msg;
public Message(int date, String msg, String msg2){
this.readOnce = false;
this.date = date;
this.msg = msg;
}
public boolean isReadOnce() {
return readOnce;
}
public void setReadOnce(){
readOnce = true;
}
// ...and more standard getters & setters here...
}
The TableView is set up in the main controller:
#FXML TableView<Message> messageTable;
#FXML TableColumn<Message, Integer> Col1;
#FXML TableColumn<Message, String> Col2;
ObservableList<Message> tableItems;
// ...
// Setting up the Table:
PropertyValueFactory<Message, Integer> dateProperty = new PropertyValueFactory<Message, Integer>("date");
PropertyValueFactory<Message, String> msgProperty = new PropertyValueFactory<Message, String>("msg");
Col1.setCellValueFactory( dateProperty );
Col2.setCellValueFactory( msgProperty );
messageTable.setItems( tableItems );
// If we click an item in the table: messageTable.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
if (newSelection != null) {
System.out.println("Troubleshoot: You clicked: "+newSelection.getMsg());
newSelection.setReadOnce(true);
}
});
And if I want to add a new message to the table, I just add it into the observable list:
public void addMsg(int num, String msg){
tableItems.add(new Message(num, msg));
}
So far, pretty easy. But I'm all thumbs when it comes to the rowFactory:
messageTable.setRowFactory(messageTable -> {
TableRow<Message> row = new TableRow<>();
ObjectProperty<Message> opMsg = row.itemProperty();
Message tmpMsg = opMsg.get();
if(!tmpMsg.isReadOnce()){
row.getStyleClass().add("highlight-message"); // defined in CSS
} else {
row.getStyleClass().add("clear-message"); // defined in CSS
}
return row;
});
To be very honest, I have no idea what I'm doing here. I understand that the rowFactory takes in the entire table and regenerates each row one-by-one. What I don't understand is how does the RowFactory code examine each Message in the table and how can I access them? Originally I thought these line might allow me to see the Message within the row:
TableRow<Message> row = new TableRow<>();
ObjectProperty<Message> opMsg = row.itemProperty();
Message tmpMsg = opMsg.get();
But when I debug the code, tmpMsg == NULL. So that's a big fat dead end.
Anyone see what I'm doing wrong? I've been researching this for about a week, getting absolutely no-where. Any help anyone can offer is wildly appreciated.
Many thanks,
-RAO
TableRows are created by TableView to fill it's viewport and contain TableCells. At the time they are created the item property still contains the default value null. You could register a listener to that property but usually I prefer overriding the updateItem method of a cell.
Also using PseudoClass is simpler than using style classes. New items can be assigned to a row; this could result in the same style class being added multiple times and even both style classes could be added to the same cell. PseudoClasses however can be switched on/of without the need to take care of removing other classes.
final PseudoClass highlightMessage = PseudoClass.getPseudoClass("highlight-message");
messageTable.setRowFactory(messageTable -> new TableRow<Message>() {
{
selectedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
Message item = getItem();
if (item != null) {
item.setReadOnce();
pseudoClassStateChanged(highlightMessage, false);
}
}
});
}
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
pseudoClassStateChanged(highlightMessage, item != null && !item.isReadOnce());
}
});
In a CSS stylesheet you could use rules like this:
.table-row-cell:filled {
/* style for non-highlighted rows */
}
.table-row-cell:filled:highlight-message {
/* style for highlighted rows */
}
Note that this does not allow you to programmatically alter the read state. It updates the state on selecting a cell. You could add a BooleanProperty to Message or use a ObservableSet to store the highlighted messages and update the state of cells from a listener if you need to programmatically update the readOnce property. In the latter case you do not need to store a readOnce property in the Message itself...

How can I listen to INTERNAL changes in a tableview?

I have a TableView whose items contain checkboxes. As soon as 2 checkboxes are selected, I need to "unhide" a button.
I have no idea how to check that. Do you have an approach?
The items don't know each other.
The TableView-Controller holds the TableView and the TableColumns.
As far as I know you cannot use bindings here, since you cannot bind yourself to multiple properties. I'm glad for every kind of help. :)
EDIT: To clarify myself: tableView.getItems().addListener() won't work since this can only listen to modifications to the list and not to the outer elements. It can notice if "add()" or "remove" was called, but that's basically it as far as I know.
PS: Busy waiting in a seperate thread is no solution of course.
Assuming you have a TableView<Item> for some Item class with a BooleanProperty:
public class Item {
private final BooleanProperty checked = new SimpleBooleanProperty();
public BooleanProperty checkedProperty() {
return checked ;
}
public final boolean isChecked() {
return checkedProperty().get();
}
public final void setChecked(boolean checked) {
checkedProperty().set(checked);
}
// other properties, etc...
}
and your checkboxes are bound to this property, then you can create your items list using an extractor:
ObservableList<Item> items = FXCollections.observableArrayList(item ->
new Observable[] { item.checkedProperty() });
table.setItems(items);
This ensures that the list fires update notifications when the checkedProperty changes on any of its elements.
So now you can just do normal binding stuff like:
IntegerBinding numberChecked = Bindings.createIntegerBinding(() ->
(int) items.stream().filter(Item::isChecked).count(),
items);
button.visibleProperty().bind(numberChecked.greaterThanOrEqualTo(2));
If you want to be super-efficient:
int requiredNumberChecked = 2 ;
button.visibleProperty().bind(Bindings.createBooleanBinding(() ->
items.stream()
.filter(Item::isSelected)
.skip(requiredNumberChecked-1)
.findAny().isPresent(),
items));
(the binding will return true as soon as it finds two checked items, instead of scanning the entire list).

ChoiceBox inside a TableView's cell only through custom Callback

I have been using a custom cell factory to display a ChoiceBox inside a TableView's TableColumn's cell, i.e.
someColumn.setCellFactory(new DayCellFactory());
with DayCellFactory implementing Callback (with the call and updateItem methods), and the ChoiceBox's (and thus the cell's) value being bound to the value of a property. The ChoiceBox shows the potential values of the enum Day.
class DayCellFactory implements Callback<TableColumn<DayPeriod, Day>, TableCell<DayPeriod, Day>> {
private final static ObservableList<Day> list = FXCollections.observableArrayList();
static {
for (Day day : Day.values()) {
list.add(day);
}
}
#Override
public TableCell<DayPeriod, Day> call (TableColumn<DayPeriod, Day> column) {
return new TableCell<DayPeriod, Day>() {
#Override
public void updateItem (Day item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
ObservableValue<Day> obsVal = getTableColumn().getCellObservableValue(getIndex());
ObjectProperty<Day> objProp = (ObjectProperty<Day>) obsVal;
ChoiceBox<Day> choice = new ChoiceBox<>(list);
choice.valueProperty().bindBidirectional(objProp);
setGraphic(choice);
}
}
};
}
}
I thought I could reduce the code by simply using
someColumn.setCellFactory(ChoiceBoxTableCell.forTableColumn(list));
(with the list defined and filled as required) but the resulting cell looks (and behaves) like a simple label showing the property's toString() value - yes, I did click there, but no ChoiceBox appears. And removing this line makes no difference. Removing the cell value factory results in no value being displayed.
I guess that there is no binding taking place with this simple approach, but this can probably be taken care of as well. It just seems strange that there is no ChoiceBox appearing.

Resources