2

The problem is the following:

I have a TableView with ComboBoxes where for every TableCell I can select a value from the comboboxes. The problem is, if I have a lot of rows and columns I have to click a lot to select the appropriate value in every comboBox. To select a value in a combobox I have to click four times to select the value. Once to select the cell, once to set the graphics the comboBox, again to open the popup for the combobox where I can select the value and finally to select the value.

I would like to use doubleClick, so I can open fast the comboBox, then I select the value. This would save a click and a lot of time if I have a lot of values to select.

I tried to solve it ,but none of the solutions worked correctly,

I add them here, maybe you can see where I went wrong and correct it.

I tried two similar ways:

  1. ignore startEdit() and add a mouseclick listener to the cell, and pop the comboBox on double click. This has the problem if I click another cell, the previous doesn't set the graphics to null, even if I put the setGrapichs(null) both in cancelEdit and commitEdit. Another problem is that sometimes doesn't commits the value to the model.

  2. The second approach was to handle it in startEdit(), so simply call the .show() there and .hide() it in both commit and cancel edit, depending on action. This gives me a NPE, if I wrap the TableView in a TitledPane and after I collapse/expand it, I try to select a value, after double click it gives NPE:

    Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at com.sun.javafx.scene.control.skin.ComboBoxPopupControl.positionAndShowPopup(ComboBoxPopupControl.java:197)
    at com.sun.javafx.scene.control.skin.ComboBoxPopupControl.show(ComboBoxPopupControl.java:170)
    at com.sun.javafx.scene.control.skin.ComboBoxBaseSkin.handleControlPropertyChanged(ComboBoxBaseSkin.java:127)
    at com.sun.javafx.scene.control.skin.ComboBoxListViewSkin.handleControlPropertyChanged(ComboBoxListViewSkin.java:159)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase.lambda$registerChangeListener$61(BehaviorSkinBase.java:197)
    at com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler$1.changed(MultiplePropertyChangeListenerHandler.java:55)
    at javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:89)
    at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:182)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.property.ReadOnlyBooleanPropertyBase.fireValueChangedEvent(ReadOnlyBooleanPropertyBase.java:72)
    at javafx.beans.property.ReadOnlyBooleanWrapper.fireValueChangedEvent(ReadOnlyBooleanWrapper.java:103)
    at javafx.beans.property.BooleanPropertyBase.markInvalid(BooleanPropertyBase.java:110)
    at javafx.beans.property.BooleanPropertyBase.set(BooleanPropertyBase.java:144)
    at javafx.scene.control.ComboBoxBase.setShowing(ComboBoxBase.java:185)
    at javafx.scene.control.ComboBoxBase.show(ComboBoxBase.java:391)
    at stackoverflow.combo.ComboTableCell.startEdit(ComboTableCell.java:47)
    at javafx.scene.control.TableCell.updateEditing(TableCell.java:556)
    at javafx.scene.control.TableCell.lambda$new$26(TableCell.java:142)
    at javafx.beans.WeakInvalidationListener.invalidated(WeakInvalidationListener.java:83)
    at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:349)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
    at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
    at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
    at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
    at javafx.scene.control.TableView.setEditingCell(TableView.java:1145)
    at javafx.scene.control.TableView.edit(TableView.java:1459)
    at com.sun.javafx.scene.control.behavior.TableCellBehavior.edit(TableCellBehavior.java:108)
    at com.sun.javafx.scene.control.behavior.TableCellBehavior.edit(TableCellBehavior.java:38)
    at com.sun.javafx.scene.control.behavior.CellBehaviorBase.handleClicks(CellBehaviorBase.java:271)
    at com.sun.javafx.scene.control.behavior.TableCellBehaviorBase.simpleSelect(TableCellBehaviorBase.java:218)
    at com.sun.javafx.scene.control.behavior.TableCellBehaviorBase.doSelect(TableCellBehaviorBase.java:148)
    at com.sun.javafx.scene.control.behavior.CellBehaviorBase.mouseReleased(CellBehaviorBase.java:159)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:96)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:89)
    at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.Scene$MouseHandler.process(Scene.java:3757)
    at javafx.scene.Scene$MouseHandler.access$1500(Scene.java:3485)
    at javafx.scene.Scene.impl_processMouseEvent(Scene.java:1762)
    at javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2494)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:381)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$354(GlassViewEventHandler.java:417)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:416)
    at com.sun.glass.ui.View.handleMouseEvent(View.java:555)
    at com.sun.glass.ui.View.notifyMouse(View.java:937)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)
    

Here is the code where you can check:

TableCell:

public class ComboTableCell<T,S> extends TableCell<T,S> {

    private ComboBox<S> combo;

    public ComboTableCell(Collection<S> items) {
        combo = new ComboBox<>();
        combo.setItems(FXCollections.observableArrayList(items));
        combo.prefWidthProperty().bind(widthProperty());
        combo.valueProperty().addListener((observable, oldValue, newValue) -> commitEdit(newValue));
//      1. Solution with mouse event
//      this.setOnMouseClicked(event -> {
//          if(event.getClickCount() == 2){
//              combo.getSelectionModel().select(getItem());
//              setText(null);
//              setGraphic(combo);
//              if(!combo.isShowing()){
//                  combo.show();
//              }
//          }
//      });
    }

//  2. Solution with startEdit
    @Override
    public void startEdit() {
        combo.getSelectionModel().select(getItem());
        super.startEdit();
        setText(null);
        setGraphic(combo);
        if(!combo.isShowing()){
            combo.show();
        }
    }

    @Override
    protected void updateItem(S item, boolean empty) {
        super.updateItem(item, empty);
        if(empty){
            setText(null);
            setGraphic(null);
            return;
        }
        setText(getItem().toString());
        setGraphic(null);
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText(getItem().toString());
        setGraphic(null);
        if(combo.isShowing()){
            combo.hide();
        }
    }

    @Override
    public void commitEdit(S newValue) {
        super.commitEdit(newValue);
        setGraphic(null);
        setText(getItem().toString());
        if(combo.isShowing()){
            combo.hide();
        }
        setGraphic(null);
        setText(getItem().toString());
    }
}

Controller:

public class Controller implements Initializable {

    @FXML
    private TableView<Model> table;
    @FXML
    private TableColumn<Model,String> col;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        table.setEditable(true);

        col.setCellValueFactory(data -> data.getValue().text);
        col.setCellFactory(factory -> new ComboTableCell<>(Arrays.asList("a","b","c")));

        table.setItems(FXCollections.observableArrayList(Arrays.asList(new Model("a"),new Model("b"))));
    }

     static class Model{

        private StringProperty text;

        public Model(String text) {
            this.text = new SimpleStringProperty(text);
        }

        public String getText() {
            return text.get();
        }

        public StringProperty textProperty() {
            return text;
        }
    }

}

Fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TitledPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="stackoverflow.combo.Controller">
    <TitledPane text="Table">
        <TableView fx:id="table">
            <columns>
                <TableColumn fx:id="col" prefWidth="200"/>
            </columns>
        </TableView>
    </TitledPane>
</AnchorPane>

I would prefer any solution which provides me the expected result, you can suggest me even a different solution, which uses less workarounds or a fix for one of my suggested "solutions".

JDK version 1.8.0_121

Sunflame
  • 2,993
  • 4
  • 24
  • 48
  • the currently uncommented code worksforme - fx11. Wouldn't have expected, though, because there's so much tweaking off the "normal" places .. – kleopatra Jan 21 '19 at 15:23
  • 1
    another note: extending ComboBoxTableCell, overriding startEdit and open the popup after calling super seems to work as well. So might be a version issue, maybe some bug fixed (there are plenty around cells and edits ;) – kleopatra Jan 21 '19 at 15:38
  • I'm using jfx8 so maybe it was fixed later, but i'll check your suggestion tomorrow. – Sunflame Jan 21 '19 at 20:48
  • Hmm... I've tried with jfx11 and indeed it works(for me the second approach) without exception, but unfortunately I cannot use it. – Sunflame Jan 22 '19 at 09:07
  • 1
    as to the NPE: it's a bug in ComboBoxPopupControl (the skin super of ComboBoxListViewSkin) https://bugs.openjdk.java.net/browse/JDK-8196827 - fixed in fx11. If you are allowed to extend internal classes (as skins in fx8 still are) then a quick fix is to subclass the skin, override show to check for null scene before calling super. – kleopatra Apr 08 '19 at 12:27

1 Answers1

0

I encountered the same issue with Method (2), with mine being in the condition below when clicked. The comboBox appears, but just won't expand. Further click will not trigger anything.

enter image description here

In the end, I implemented a focus listener on the comboBox which will show the popup when it has focus. I called requestFocus at the end of startEdit.

// In the creation of comboBox, add focus listener
combo.focusProperty().addListener((observable, oldValue, isFocused) -> if (isFocused) combo.show());

@Override
public void startEdit() {
    combo.getSelectionModel().select(getItem());
    super.startEdit();
    setText(null);
    setGraphic(combo);
    // Creating a JavaFX task to make a small delay and then request focus.
    // Code below is in Kotlin and TornadoFX, but you get the point.
    runAsync {
        Thread.sleep(50)
    } ui {
        combo.requestFocus()
    }
} 

The delay above is required for the comboBox to draw its layout correctly. Or else, you will end up with the following condition randomly, depending on whether the layout already updates or not.

enter image description here

With that small delay, you will get it correct all the time.

enter image description here

P.S. If you are thinking, why not just directly call comboBox.show() after a small delay and remove the focusListener? Well, in my test, the comboBox just fail to show randomly (similar to 1st image) when you create new row or call table.refresh(). I guess it has something to do with virtualFlow and multiple instances of tableCell being created for the same instance. See here.

tingyik90
  • 1,641
  • 14
  • 23
  • Thanks for the effort, but since this is a so hacky solution (not only your but the fast opening combobox idea) I decided not to use this, but always display the comboBox (setGraphics(combo)) so the user can directly change the value, doesn't need to click 2-3x on the cell. Something like this was implemented: https://stackoverflow.com/questions/55471652/javafx-combobox-textwrap?noredirect=1#comment97661910_55471652. – Sunflame Apr 08 '19 at 08:37
  • Also related to the answer, personally I don't really like `Thread.sleep`s since they aren't reliable – Sunflame Apr 08 '19 at 08:38
  • Sure I understand. Coming from Android background, JavaFX is really poor, especially TableView. I'm building an engineering software and TableView happens to be the core UI. There's so much hacking to make it usable, so any extra hack doesn't makes a difference to me ;-) Anyway, using a delay before triggering another UI change is quite common in Android, hence the idea. I'm publishing this answer for my own reference. – tingyik90 Apr 08 '19 at 10:07
  • Also, mind elaborating why `Thread.sleep` is not reliable? There were cases where it doesn't sleep? – tingyik90 Apr 08 '19 at 10:08
  • the general rule is to never-ever (as in reeeaaally never!) sleep the ui thread - while it might appear to be working, it prevents the normal flow of events, tends to sum up, is hard to test and hard to debug .. at the end you opt for a bunch of unpredictable problems in future. Simply don't, it's not worth the effort in the long run. Just saying, though, I'm well aware of how buggy nearly everything around TableView is if we start to exercise it a bit below the obvious .. Seeing you mention refresh - dont-dont-dont: if you seem to need it there's most probably something wrong in your setup – kleopatra Apr 08 '19 at 11:13
  • But I'm not sleeping in the UI thread? I'm running sleep on a background thread, then calling the UI thread after the delay. – tingyik90 Apr 08 '19 at 17:34
  • @kleopatra, also, I've seen a lot of your answer on SO and learnt a lot from you. Just wanted to say thank you. As for the `refresh()` part, I really hope to avoid it. The only part where this happen is when copy pasting from Excel. The large amount of setValue just doesn't trigger the `updateItem()` sometimes. – tingyik90 Apr 08 '19 at 17:41
  • oops .. my bad: didn't recognize the short-hand ;) As to refresh: there _are_ use-cases when it is needed (that's why it was added), one (where it was not intended for) is f.i. when modifying the items list while in editing mode. But I would go a very very long way to verify that my context is set up correctly before resorting to it. _large amount of setValue_ doesn't smell like a valid reason ... can't tell from afar :) – kleopatra Apr 10 '19 at 08:58