edvin / tornadofx

Lightweight JavaFX Framework for Kotlin
Apache License 2.0
3.68k stars 272 forks source link

TableView column resizer extension function #48

Closed thomasnield closed 8 years ago

thomasnield commented 8 years ago

One thing that frustrated me for awhile is having a way to fit TableView columns to the header and cell contents. Then I found a little gem in a Stack Overflow post.

I was able to come up with this class to expose that resizeColumnToFitContent()

public final class TableColumnResizer<T> extends TableViewSkin<T> {

    public TableColumnResizer(TableView<T> tableView) {
        super(tableView);
    }

    @Override
    public void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
      super.resizeColumnToFitContent(tc,maxRows);
    }
}

Then I can apply that TableViewSkin to the skinProperty(), and call a resizeOp function at any time to re-fit the columns.

var resizeOp: (() -> Unit) by singleAssign()

//layout built here

tableView.apply {
    val resizerSkin = TableColumnResizer<FTRecord>(this);
    skinProperty().set(resizerSkin)
    resizeOp = { columns.forEach { resizerSkin.resizeColumnToFitContent(it, 100) } }
}

It would be awesome if we could apply a resizeColumns() extension function to the TableView. The problem is I can't think of an efficient way at the top of my head without extending TableView or doing some lazy operation that stores state elsewhere. It's really too bad extension properties can't be fields.

edvin commented 8 years ago

We could always use reflection to call the method in the skin. It's not ideal, but it will work. If they ever remove the method, we can switch to another implementation but keep the interface. We can even choose different strategies in our extension function based on what's available on the skin. I'll give it a go :)

edvin commented 8 years ago

I've come up with the following implementation. Can you check if it works for you?

fun <T> TableView<T>.resizeColumnsToFitContent(resizeColumns: List<TableColumn<T, *>> = columns, maxRows: Int = 20) {
    val resizer = skin.javaClass.getDeclaredMethod("resizeColumnToFitContent", TableColumn::class.java, Int::class.java)
    resizer.isAccessible = true
    resizeColumns.forEach { resizer.invoke(skin, it, maxRows) }
}

It supports optionally specifying only some columns, as well as the max number of rows to evaluate. Defaults are "all columns" and "20 rows".

I'm committing it so it's easier for you to test.

thomasnield commented 8 years ago

I like this idea a lot. However I am getting an error...

 val tbl = tableview<Person> {
                items = persons
                column("ID",Person::id)
                column("Name", Person::name)
                column("Birthday", Person::birthday)
                column("Age",Person::age)
            }
            tbl.resizeColumnsToFitContent()
java.lang.NullPointerException
    at tornadofx.MenuView.resizeColumnsToFitContent(MenuTest.kt:41)
    at tornadofx.MenuView.resizeColumnsToFitContent$default(MenuTest.kt:40)
    at tornadofx.MenuView.<init>(MenuTest.kt:36)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
    at java.lang.Class.newInstance(Class.java:442)
    at tornadofx.FXKt.find(FX.kt:87)
    at tornadofx.App.start(App.kt:20)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$163(LauncherImpl.java:863)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$176(PlatformImpl.java:326)
    at com.sun.javafx.application.PlatformImpl.lambda$null$174(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$175(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$149(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)
edvin commented 8 years ago

That's probably because the skin isn't initialized yet. Try wrapping it in Platform.runLater { }. If that works, I can make sure it is automatically wrapped if the skin isn't available yet. Let me know!

edvin commented 8 years ago

I commited a fix for this :)

thomasnield commented 8 years ago

Okay awesome, ill play with it again when I get back to my tablet.

thomasnield commented 8 years ago

Brilliant! Works perfectly. Thanks Edvin, that was pretty clever.