various
This commit is contained in:
@@ -10,6 +10,7 @@ group = "net.eksb"
|
||||
|
||||
dependencies {
|
||||
api("com.miglayout:miglayout-swing:11.4.2")
|
||||
api("org.swinglabs:swingx:1.6.1")
|
||||
}
|
||||
|
||||
java {
|
||||
|
||||
196
src/main/kotlin/net/eksb/kswingutil/BoundsPopupMenuListener.kt
Normal file
196
src/main/kotlin/net/eksb/kswingutil/BoundsPopupMenuListener.kt
Normal file
@@ -0,0 +1,196 @@
|
||||
package net.eksb.kswingutil
|
||||
|
||||
/*
|
||||
This is based on https://tips4java.wordpress.com/2010/11/28/combo-box-popup/, which says:
|
||||
We assume no responsibility for the code. You are free to use and/or modify and/or distribute any or all code posted on
|
||||
the Java Tips Weblog without restriction. A credit in the code comments would be nice, but not in any way mandatory.
|
||||
*/
|
||||
|
||||
import javax.swing.event.PopupMenuListener
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.JComboBox
|
||||
import javax.swing.plaf.basic.BasicComboPopup
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.JList
|
||||
import javax.swing.JScrollBar
|
||||
|
||||
/**
|
||||
* This class will change the bounds of the JComboBox popup menu to support
|
||||
* different functionality. It will support the following features:
|
||||
* - a horizontal scrollbar can be displayed when necessary
|
||||
* - the popup can be wider than the combo box
|
||||
* - the popup can be displayed above the combo box
|
||||
*
|
||||
* Class will only work for a JComboBox that uses a BasicComboPop.
|
||||
*/
|
||||
class BoundsPopupMenuListener(
|
||||
/**
|
||||
* True add horizontal scrollBar to scrollPane
|
||||
* false remove the horizontal scrollBar
|
||||
*
|
||||
* For some reason the default implementation of the popup removes the
|
||||
* horizontal scrollBar from the popup scroll pane which can result in
|
||||
* the truncation of the rendered items in the popop. Adding a scrollBar
|
||||
* back to the scrollPane will allow horizontal scrolling if necessary.
|
||||
*/
|
||||
var isScrollBarRequired:Boolean = true,
|
||||
/**
|
||||
* True adjust the width as required.
|
||||
*
|
||||
* Change the width of the popup to be the greater of the width of the
|
||||
* combo box or the preferred width of the popup. Normally the popup width
|
||||
* is always the same size as the combo box width.
|
||||
*/
|
||||
var isPopupWider:Boolean = false,
|
||||
/**
|
||||
* Limit the popup width to the value specified
|
||||
* (minimum size will be the width of the combo box)
|
||||
*/
|
||||
var maximumWidth:Int = -1,
|
||||
/**
|
||||
* true display popup above the combo box,
|
||||
*
|
||||
* false display popup below the combo box.
|
||||
* Change the location of the popup relative to the combo box.
|
||||
*/
|
||||
var isPopupAbove:Boolean = false
|
||||
):PopupMenuListener {
|
||||
private var scrollPane:JScrollPane? = null
|
||||
|
||||
/**
|
||||
* Alter the bounds of the popup just before it is made visible.
|
||||
*/
|
||||
override fun popupMenuWillBecomeVisible(e:PopupMenuEvent) {
|
||||
val comboBox = e.source as JComboBox<*>
|
||||
if (comboBox.itemCount == 0) return
|
||||
val child:Any = comboBox.accessibleContext.getAccessibleChild(0)
|
||||
if (child is BasicComboPopup) {
|
||||
SwingUtilities.invokeLater { customizePopup(child) }
|
||||
}
|
||||
}
|
||||
|
||||
protected fun customizePopup(popup:BasicComboPopup) {
|
||||
scrollPane = getScrollPane(popup)
|
||||
if (isPopupWider) popupWider(popup)
|
||||
checkHorizontalScrollBar(popup)
|
||||
|
||||
// For some reason in JDK7 the popup will not display at its preferred
|
||||
// width unless its location has been changed from its default
|
||||
// (ie. for normal "pop down" shift the popup and reset)
|
||||
val comboBox = popup.invoker
|
||||
if ( comboBox.isVisible ) {
|
||||
val location = comboBox.locationOnScreen
|
||||
if (isPopupAbove) {
|
||||
val height = popup.preferredSize.height
|
||||
popup.setLocation(location.x, location.y - height)
|
||||
} else {
|
||||
val height = comboBox.preferredSize.height
|
||||
popup.setLocation(location.x, location.y + height - 1)
|
||||
popup.setLocation(location.x, location.y + height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Adjust the width of the scrollpane used by the popup
|
||||
*/
|
||||
protected fun popupWider(popup:BasicComboPopup) {
|
||||
val list:JList<*> = popup.list
|
||||
|
||||
// Determine the maximimum width to use:
|
||||
// a) determine the popup preferred width
|
||||
// b) limit width to the maximum if specified
|
||||
// c) ensure width is not less than the scroll pane width
|
||||
var popupWidth = (list.preferredSize.width + 5 // make sure horizontal scrollbar doesn't appear
|
||||
+ getScrollBarWidth(popup, scrollPane))
|
||||
if (maximumWidth != -1) {
|
||||
popupWidth = Math.min(popupWidth, maximumWidth)
|
||||
}
|
||||
val scrollPaneSize = scrollPane!!.preferredSize
|
||||
popupWidth = Math.max(popupWidth, scrollPaneSize.width)
|
||||
|
||||
// Adjust the width
|
||||
scrollPaneSize.width = popupWidth
|
||||
scrollPane!!.preferredSize = scrollPaneSize
|
||||
scrollPane!!.maximumSize = scrollPaneSize
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is called every time:
|
||||
* - to make sure the viewport is returned to its default position
|
||||
* - to remove the horizontal scrollbar when it is not wanted
|
||||
*/
|
||||
private fun checkHorizontalScrollBar(popup:BasicComboPopup) { // Reset the viewport to the left
|
||||
val viewport = scrollPane!!.viewport
|
||||
val p = viewport.viewPosition
|
||||
p.x = 0
|
||||
viewport.viewPosition = p
|
||||
|
||||
// Remove the scrollbar so it is never painted
|
||||
if (!isScrollBarRequired) {
|
||||
scrollPane!!.horizontalScrollBar = null
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure a horizontal scrollbar exists in the scrollpane
|
||||
var horizontal = scrollPane!!.horizontalScrollBar
|
||||
if (horizontal == null) {
|
||||
horizontal = JScrollBar(JScrollBar.HORIZONTAL)
|
||||
scrollPane!!.horizontalScrollBar = horizontal
|
||||
scrollPane!!.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
|
||||
// Potentially increase height of scroll pane to display the scrollbar
|
||||
if (horizontalScrollBarWillBeVisible(popup, scrollPane)) {
|
||||
val scrollPaneSize = scrollPane!!.preferredSize
|
||||
scrollPaneSize.height += horizontal.preferredSize.height
|
||||
scrollPane!!.preferredSize = scrollPaneSize
|
||||
scrollPane!!.maximumSize = scrollPaneSize
|
||||
scrollPane!!.revalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the scroll pane used by the popup so its bounds can be adjusted
|
||||
*/
|
||||
protected fun getScrollPane(popup:BasicComboPopup):JScrollPane {
|
||||
val list:JList<*> = popup.list
|
||||
val c = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, list)
|
||||
return c as JScrollPane
|
||||
}
|
||||
|
||||
/*
|
||||
* I can't find any property on the scrollBar to determine if it will be
|
||||
* displayed or not so use brute force to determine this.
|
||||
*/
|
||||
protected fun getScrollBarWidth(popup:BasicComboPopup, scrollPane:JScrollPane?):Int {
|
||||
var scrollBarWidth = 0
|
||||
val comboBox = popup.invoker as JComboBox<*>
|
||||
if (comboBox.itemCount > comboBox.maximumRowCount) {
|
||||
val vertical = scrollPane!!.verticalScrollBar
|
||||
scrollBarWidth = vertical.preferredSize.width
|
||||
}
|
||||
return scrollBarWidth
|
||||
}
|
||||
|
||||
/*
|
||||
* I can't find any property on the scrollBar to determine if it will be
|
||||
* displayed or not so use brute force to determine this.
|
||||
*/
|
||||
protected fun horizontalScrollBarWillBeVisible(popup:BasicComboPopup, scrollPane:JScrollPane?):Boolean {
|
||||
val list:JList<*> = popup.list
|
||||
val scrollBarWidth = getScrollBarWidth(popup, scrollPane)
|
||||
val popupWidth = list.preferredSize.width + scrollBarWidth
|
||||
return popupWidth > scrollPane!!.preferredSize.width
|
||||
}
|
||||
|
||||
override fun popupMenuCanceled(e:PopupMenuEvent) {}
|
||||
override fun popupMenuWillBecomeInvisible(
|
||||
e:PopupMenuEvent) { // In its normal state the scrollpane does not have a scrollbar
|
||||
if (scrollPane != null) {
|
||||
scrollPane!!.horizontalScrollBar = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package net.eksb.kswingutil.ext
|
||||
|
||||
import net.eksb.kswingutil.property.MutableProperty
|
||||
import net.eksb.kswingutil.property.Property
|
||||
import net.eksb.kswingutil.ext.bind
|
||||
import org.jdesktop.swingx.autocomplete.AutoCompleteDecorator
|
||||
import org.jdesktop.swingx.autocomplete.ObjectToStringConverter
|
||||
import java.awt.Component
|
||||
import java.awt.Container
|
||||
import java.awt.Font
|
||||
@@ -18,13 +19,36 @@ import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JList
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.JSeparator
|
||||
import javax.swing.JTabbedPane
|
||||
import javax.swing.JToolBar
|
||||
|
||||
fun Container.separator(constraints:Any?=null, alignment:Int, block:JSeparator.()->Unit ={}):JSeparator =
|
||||
add(constraints, JSeparator(alignment),block)
|
||||
|
||||
fun Container.panel(constraints:Any?=null, block:JPanel.()->Unit ={}):JPanel = add(constraints, JPanel(),block)
|
||||
|
||||
fun Container.scrollPane(constraints:Any?=null, block:JScrollPane.()->Component):JScrollPane {
|
||||
val scrollPane = JScrollPane()
|
||||
scrollPane.viewport.view = block(scrollPane)
|
||||
scrollPane.fastScroll()
|
||||
add(scrollPane,constraints)
|
||||
return scrollPane
|
||||
}
|
||||
|
||||
fun JScrollPane.fastScroll() {
|
||||
verticalScrollBar.unitIncrement = 12
|
||||
horizontalScrollBar.unitIncrement = 12
|
||||
}
|
||||
|
||||
fun Container.tabbedPane(constraints:Any?=null, block:JTabbedPane.()->Unit = {}):JTabbedPane = add(constraints,JTabbedPane(),block)
|
||||
fun jtabbedPane(block:JTabbedPane.()->Unit = {}):JTabbedPane = JTabbedPane().also{block(it)}
|
||||
|
||||
fun JTabbedPane.tab( title:String, op:()->Component ) { addTab(title,op()) }
|
||||
|
||||
fun Container.toolbar(constraints:Any?=null, block:JToolBar.()->Unit = {}):JToolBar = add(constraints,JToolBar(),block)
|
||||
|
||||
fun Container.horizontalGlue():Component = add(Box.createHorizontalGlue())
|
||||
|
||||
fun JComponent.emptyBorder(size:Int) {
|
||||
@@ -50,6 +74,32 @@ fun <T> JComboBox<T>.model(values:Array<T>) {
|
||||
model = DefaultComboBoxModel<T>(values)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an autocomplete decorator to this [JComboBox].
|
||||
*
|
||||
* If [autoCompleteStrings] is not null, it is used to determine which rows match.
|
||||
* The first item in the sequence is the preferred string, subsequent items are also allowed matches.
|
||||
*
|
||||
* If [autoCompleteStrings] is null the string representation ([Any.toString])]
|
||||
* of the combobox item is used.
|
||||
*/
|
||||
fun <E> JComboBox<E>.autoComplete( autoCompleteStrings:((E)->Sequence<String>)? =null ) {
|
||||
if ( autoCompleteStrings != null ) {
|
||||
AutoCompleteDecorator.decorate(this,
|
||||
object:ObjectToStringConverter() {
|
||||
override fun getPreferredStringForItem(item:Any?):String {
|
||||
return autoCompleteStrings(item as E).firstOrNull() ?: ""
|
||||
}
|
||||
override fun getPossibleStringsForItem(item:Any?):Array<String> {
|
||||
return autoCompleteStrings(item as E).toList().toTypedArray()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
AutoCompleteDecorator.decorate(this)
|
||||
}
|
||||
}
|
||||
|
||||
typealias ListCellRendererLambda<T> = DefaultListCellRenderer.(
|
||||
list:JList<T>,
|
||||
value:T,
|
||||
|
||||
82
src/main/kotlin/net/eksb/kswingutil/ext/WindowExt.kt
Normal file
82
src/main/kotlin/net/eksb/kswingutil/ext/WindowExt.kt
Normal file
@@ -0,0 +1,82 @@
|
||||
package net.eksb.kswingutil.ext
|
||||
|
||||
import java.awt.Frame
|
||||
import java.awt.GraphicsConfiguration
|
||||
import java.awt.GraphicsDevice
|
||||
import java.awt.GraphicsEnvironment
|
||||
import java.awt.MouseInfo
|
||||
import java.awt.Rectangle
|
||||
import java.awt.Window
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
/** Get the bounds of the screen that the mouse is currently on. */
|
||||
fun getMouseScreenBounds():Rectangle = getMouseWindowGraphicsConfiguration().bounds
|
||||
/** Get the location of the mouse. */
|
||||
fun getMouseLocation():Pair<Int,Int> = MouseInfo.getPointerInfo().location.run { x to y }
|
||||
|
||||
/** Get the GraphicsConfiguration of the screen the mouse is currently on. */
|
||||
fun getMouseWindowGraphicsConfiguration():GraphicsConfiguration {
|
||||
val (mouseX:Int,mouseY:Int) = getMouseLocation()
|
||||
return GraphicsEnvironment
|
||||
.getLocalGraphicsEnvironment()
|
||||
.screenDevices
|
||||
.map(GraphicsDevice::getDefaultConfiguration) // correct?
|
||||
.find { graphicsConfiguration ->
|
||||
graphicsConfiguration.bounds.contains( mouseX, mouseY )
|
||||
}
|
||||
?: throw Exception("Could not find screen with mouse.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Place this window at the mouse
|
||||
*
|
||||
* @param direction Direction of the window from the mouse.
|
||||
* SwingConstants.NORTH_EAST, NORTH_WEST, SOUTH_EAST, or SOUTH_WEST.
|
||||
* This is where the window is relative to the mouse, not which corner of the window the mouse is.
|
||||
*/
|
||||
fun Window.placeWindowAtMouse( direction:Int ) {
|
||||
|
||||
// find the bounds of the screen the mouse is on
|
||||
val (mouseX:Int,mouseY:Int) = getMouseLocation()
|
||||
val mouseScreenBounds = getMouseScreenBounds()
|
||||
|
||||
// get target position based on mouse position, size, and direction
|
||||
var (x:Int,y:Int) =
|
||||
when(direction) {
|
||||
SwingConstants.NORTH_EAST -> mouseX to mouseY - height
|
||||
SwingConstants.NORTH_WEST -> mouseX - width to mouseY - height
|
||||
SwingConstants.SOUTH_EAST -> mouseX to mouseY
|
||||
SwingConstants.SOUTH_WEST -> mouseX - width to mouseY
|
||||
else -> throw Exception("direction must be NORTH_EAST, NORTH_WEST, SOUTH_EAST, or SOUTH_WEST")
|
||||
}
|
||||
|
||||
// adjust to fit on screen
|
||||
if ( x < mouseScreenBounds.x ) {
|
||||
x = mouseScreenBounds.x
|
||||
}
|
||||
if ( y < mouseScreenBounds.y ) {
|
||||
y = mouseScreenBounds.y
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
setLocation(x,y)
|
||||
requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Center this window over the specified frame.
|
||||
*
|
||||
* [Window.setLocationRelativeTo] does *NOT* work on multi-monitor setups.
|
||||
*/
|
||||
fun Window.centerOver(owner:Frame) {
|
||||
val ownerBounds = owner.bounds
|
||||
val centerX = ownerBounds.x + ownerBounds.width/2
|
||||
val centerY = ownerBounds.y + ownerBounds.height/2
|
||||
|
||||
val x = centerX - width/2
|
||||
val y = centerY - height/2
|
||||
|
||||
setLocation(x,y)
|
||||
}
|
||||
154
src/main/kotlin/net/eksb/kswingutil/table/GoodTable.kt
Normal file
154
src/main/kotlin/net/eksb/kswingutil/table/GoodTable.kt
Normal file
@@ -0,0 +1,154 @@
|
||||
package net.eksb.kswingutil.table
|
||||
|
||||
import net.eksb.kswingutil.ext.emptyBorder
|
||||
import net.eksb.kswingutil.ext.add
|
||||
import java.awt.Color
|
||||
import java.awt.Component
|
||||
import java.awt.Container
|
||||
import java.awt.Font
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JTable
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.event.ChangeEvent
|
||||
import javax.swing.event.ListSelectionEvent
|
||||
import javax.swing.event.TableColumnModelEvent
|
||||
import javax.swing.event.TableColumnModelListener
|
||||
import javax.swing.event.TableModelEvent
|
||||
import javax.swing.table.TableCellRenderer
|
||||
import javax.swing.table.TableColumnModel
|
||||
import javax.swing.table.TableModel
|
||||
|
||||
/**
|
||||
* A [JTable] that supports auto-resized columns
|
||||
* and custom row and cell rendering.
|
||||
*/
|
||||
class GoodTable: JTable() {
|
||||
init {
|
||||
tableHeader.font = tableHeader.font.deriveFont(Font.BOLD)
|
||||
rowHeight = tableRowHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to false to stop GoodTable from setting row background colors.
|
||||
* Do this if you plan on setting them in your renders.
|
||||
*/
|
||||
var setBackgroundColor:Boolean = true
|
||||
|
||||
/** Fill weights for [resizeColumnWidths]. */
|
||||
var fillWeights:Map<Int,Float>? = null
|
||||
|
||||
/**
|
||||
* Fixed widths for [reszieColumnWidths].
|
||||
* [colResizeListener] sets these when the user resizes a column,
|
||||
* so it stays that width.
|
||||
*/
|
||||
val fixedWidths:MutableMap<Int,Int> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Listener to update [fixedWidths].
|
||||
*/
|
||||
private val colResizeListener = object:TableColumnModelListener {
|
||||
override fun columnAdded(e:TableColumnModelEvent) {}
|
||||
override fun columnMarginChanged(e:ChangeEvent) {
|
||||
val resizingCol = this@GoodTable.tableHeader.resizingColumn
|
||||
if ( resizingCol != null ) {
|
||||
val ci = columnModel.columns.asSequence().indexOf(resizingCol)
|
||||
fixedWidths[ci] = resizingCol.width
|
||||
}
|
||||
}
|
||||
override fun columnMoved(e:TableColumnModelEvent) {}
|
||||
override fun columnRemoved(e:TableColumnModelEvent) {}
|
||||
override fun columnSelectionChanged(e:ListSelectionEvent) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* When setting a model, register [colResizeListener].
|
||||
*/
|
||||
override fun setColumnModel(columnModel:TableColumnModel) {
|
||||
this.columnModel?.removeColumnModelListener(colResizeListener)
|
||||
super.setColumnModel(columnModel)
|
||||
columnModel.addColumnModelListener(colResizeListener)
|
||||
}
|
||||
|
||||
override fun setModel(dataModel:TableModel) {
|
||||
super.setModel(dataModel)
|
||||
|
||||
// resize columns to fit content width
|
||||
autoResizeMode = AUTO_RESIZE_OFF // because in a scroll pane
|
||||
model.addTableModelListener { event ->
|
||||
// resize if rows or cols are added or removed
|
||||
// do later so table can re-render and re-sort first, otherwise row indexes are wrong
|
||||
if ( event.type in listOf(TableModelEvent.INSERT,TableModelEvent.DELETE)
|
||||
|| event.column == TableModelEvent.ALL_COLUMNS // this happens on fireTableDataChanged
|
||||
) {
|
||||
SwingUtilities.invokeLater {
|
||||
resizeColumnWidths(this,true,fillWeights,fixedWidths)
|
||||
}
|
||||
}
|
||||
}
|
||||
SwingUtilities.invokeLater {
|
||||
if ( fillWeights != null ) {
|
||||
// resize when parent size changes
|
||||
parent?.addComponentListener( object:ComponentAdapter() {
|
||||
override fun componentResized(e:ComponentEvent?) {
|
||||
SwingUtilities.invokeLater {
|
||||
resizeColumnWidths(this@GoodTable,true,fillWeights,fixedWidths)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
resizeColumnWidths(this,true,fillWeights,fixedWidths)
|
||||
}
|
||||
}
|
||||
|
||||
override fun prepareRenderer(renderer:TableCellRenderer, row:Int, column:Int):Component {
|
||||
super.prepareRenderer(renderer, row, column)
|
||||
val component = super.prepareRenderer(renderer, row, column)
|
||||
val selected = selectionModel.isSelectedIndex(row)
|
||||
|
||||
if ( setBackgroundColor ) {
|
||||
component.background =
|
||||
when {
|
||||
selected -> tableColorSelected
|
||||
row % 2 == 0 -> tableColorEven
|
||||
else -> tableColorOdd
|
||||
}
|
||||
// If we set the background we have to set the foreground in case somebody's theme
|
||||
// defaulted to white text on black background and we just made the background white.
|
||||
if ( component.foreground == null ) component.foreground = Color.BLACK
|
||||
}
|
||||
|
||||
val jcomp = ( component as? JComponent)
|
||||
if ( jcomp != null ) {
|
||||
if ( jcomp.border == null ) {
|
||||
jcomp.emptyBorder(tableCellPadding)
|
||||
}
|
||||
}
|
||||
|
||||
preparer?.invoke( component , row, column, selected )
|
||||
return component
|
||||
}
|
||||
|
||||
var preparer:((Component,Int,Int,Boolean)->Unit)? = null
|
||||
|
||||
companion object {
|
||||
val tableColorEven = Color.WHITE
|
||||
val tableColorOdd = Color(240,240,240)
|
||||
val tableColorSelected = Color(218,230,242)
|
||||
val tableRowHeight = 22
|
||||
val tableCellPadding = 3
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Container.goodTable(constraints:Any?=null, block:GoodTable.()->Unit ={}):GoodTable = add(constraints, GoodTable(), block)
|
||||
|
||||
/** Builder to add a [GoodTableColumn] to a [GoodTableModel]. */
|
||||
inline fun <T:Any,reified C> GoodTableModel<T>.column(name:String, identifier:Any?=null, op:GoodTableColumn<T, C>.()->Unit ): GoodTableColumn<T,C> {
|
||||
val tableColumn = GoodTableColumn<T, C>(name, identifier, C::class.java)
|
||||
op(tableColumn)
|
||||
addColumn(tableColumn)
|
||||
return tableColumn
|
||||
}
|
||||
498
src/main/kotlin/net/eksb/kswingutil/table/GoodTableModel.kt
Normal file
498
src/main/kotlin/net/eksb/kswingutil/table/GoodTableModel.kt
Normal file
@@ -0,0 +1,498 @@
|
||||
package net.eksb.kswingutil.table
|
||||
|
||||
import net.eksb.kswingutil.BoundsPopupMenuListener
|
||||
import net.eksb.kswingutil.ext.autoComplete
|
||||
import net.eksb.kswingutil.ext.renderer
|
||||
import net.eksb.kswingutil.property.Property
|
||||
import java.awt.Component
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.ComboBoxModel
|
||||
import javax.swing.DefaultCellEditor
|
||||
import javax.swing.DefaultRowSorter
|
||||
import javax.swing.JComboBox
|
||||
import javax.swing.JTable
|
||||
import javax.swing.table.AbstractTableModel
|
||||
|
||||
import org.jdesktop.swingx.autocomplete.ComboBoxCellEditor
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.table.DefaultTableColumnModel
|
||||
import javax.swing.table.TableColumn
|
||||
import javax.swing.table.TableColumnModel
|
||||
|
||||
/** Lambda to get a cell value (C) for the row value (T). */
|
||||
typealias CellValueCalc<T,C> = (T)->C
|
||||
|
||||
/** Lambda to get a cell value (C) for the row value (T), at the specified index. */
|
||||
typealias CellValueCalcIndexed<T,C> = (T,Int)->C
|
||||
|
||||
/** Lambda to determine if the cell is editable for the row value (R). */
|
||||
typealias IsEditableCalc<T> = (T)->Boolean
|
||||
|
||||
/**
|
||||
* Callback for when cell's value is changed.
|
||||
* Arguments are row value, cell value, row index, cell index.
|
||||
*/
|
||||
typealias CellValueChangeHandler<T,C> = (T,C,Int,Int)->Unit
|
||||
|
||||
/**
|
||||
* Callback when a cell is clicked.
|
||||
* Arguments are mouse event, row index, column index, row value, cell value.
|
||||
*/
|
||||
typealias CellClickHandler<T,C> = (MouseEvent,Int,Int,T,C)->Unit
|
||||
|
||||
/**
|
||||
* Callback when a header cell is clicked.
|
||||
* Arguments are mouse event and column index.
|
||||
*/
|
||||
typealias HeaderClickHandler = (MouseEvent,Int)->Unit
|
||||
|
||||
/**
|
||||
* A table model that lets you add columns that know how to get their values
|
||||
* based on the row's value.
|
||||
*
|
||||
* @property T row value type.
|
||||
*/
|
||||
class GoodTableModel<T:Any>:AbstractTableModel() {
|
||||
val cols:MutableList<GoodTableColumn<T, *>> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Generate a [TableColumnModel] for the specified [JTable].
|
||||
*/
|
||||
fun columnModel(table:JTable):TableColumnModel {
|
||||
return DefaultTableColumnModel()
|
||||
.apply {
|
||||
colsInTable(table).forEach { col -> addColumn(col) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the columns that are in the specified table, according to [GoodTableColumn.onlyInTable].
|
||||
*/
|
||||
private fun colsInTable(table:JTable):List<GoodTableColumn<T,*>> = cols
|
||||
.filter { col -> col.tables?.contains(table) ?: true }
|
||||
|
||||
/** The rows. If you modify this directly, you are responsible for calling [fire*] */
|
||||
val rows:MutableList<T> = mutableListOf()
|
||||
|
||||
override fun isCellEditable(rowIndex:Int, columnIndex:Int):Boolean {
|
||||
// determine if the cell is editable by calling the [GoodTableColumn.isEditableCalc].
|
||||
val isEditableClac = cols[columnIndex].isEditableCalc
|
||||
return if ( isEditableClac != null ) {
|
||||
val rowValue = getRowValue(rowIndex)
|
||||
if ( rowValue != null ) {
|
||||
isEditableClac.invoke(rowValue)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun <T:Any,C> getColumn(columnIndex:Int):GoodTableColumn<T,C> = cols[columnIndex] as GoodTableColumn<T,C>
|
||||
fun <T:Any,C> getColumn(columnName:String):GoodTableColumn<T,C> = getColumn( findColumn(columnName) )
|
||||
|
||||
override fun getColumnCount():Int = cols.size
|
||||
|
||||
override fun getColumnClass(columnIndex:Int):Class<*> = cols[columnIndex].cls
|
||||
|
||||
override fun findColumn(columnName:String):Int = cols.indexOfFirst { col -> col.name == columnName }
|
||||
|
||||
override fun getColumnName(column:Int):String = cols[column].name
|
||||
|
||||
override fun getRowCount():Int = rows.size
|
||||
|
||||
/**
|
||||
* Get the value for the specified model row index,
|
||||
* which is NOT necessarily the table row index.
|
||||
* If you have a table row index, use [getTableRowValue].
|
||||
*/
|
||||
fun getRowValue(rowIndex:Int):T? = if ( rowIndex < rows.size ) rows[rowIndex] else null
|
||||
|
||||
/**
|
||||
* Get the value for the specified table row index,
|
||||
* which is NOT necessarily the model row index.
|
||||
* If you have a model row index, use [getRowValue].
|
||||
*/
|
||||
fun getTableRowValue(table:JTable,rowIndex:Int):T? = getRowValue(
|
||||
table.convertRowIndexToModel(rowIndex)
|
||||
)
|
||||
|
||||
override fun getValueAt(rowIndex:Int, columnIndex:Int):Any? {
|
||||
val rowValue = getRowValue(rowIndex) ?: throw Exception("Null value for row ${rowIndex}")
|
||||
val cellValueCalc = cols[columnIndex].cellValueCalc
|
||||
?: throw Exception("No selector for column ${columnIndex}" )
|
||||
return cellValueCalc.invoke(rowValue,rowIndex)
|
||||
}
|
||||
|
||||
override fun setValueAt(aValue:Any?, rowIndex:Int, columnIndex:Int) {
|
||||
cols[columnIndex].setValueAt( getRowValue(rowIndex)!!, aValue, rowIndex, columnIndex )
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a column.
|
||||
*/
|
||||
fun addColumn( tableColumn:GoodTableColumn<T,*> ) {
|
||||
val columnIndex = cols.size
|
||||
|
||||
cols.add(tableColumn)
|
||||
|
||||
tableColumn.modelIndex = columnIndex
|
||||
tableColumn.headerValue = tableColumn.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the table for this model.
|
||||
*
|
||||
* @param table the table.
|
||||
*/
|
||||
fun setup( table:JTable ) {
|
||||
// Propagate table clicks to [GoodTableColumn.clickHandler]s.
|
||||
table.onCellClick { mouseEvent, tableRowIndex, tableColIndex ->
|
||||
val colIndex = table.convertColumnIndexToModel(tableColIndex)
|
||||
val rowIndex = table.convertRowIndexToModel(tableRowIndex)
|
||||
val col = table.columnModel.getColumn(tableColIndex) as GoodTableColumn<T,*>
|
||||
val rowValue = getRowValue(rowIndex)!!
|
||||
col.click( mouseEvent, rowIndex, colIndex, rowValue )
|
||||
}
|
||||
table.onHeaderClick { mouseEvent, tableColIndex ->
|
||||
val colIndex = table.convertColumnIndexToModel(tableColIndex)
|
||||
val col = table.columnModel.getColumn(tableColIndex) as GoodTableColumn<T,*>
|
||||
col.headerClick( mouseEvent, colIndex )
|
||||
}
|
||||
|
||||
val colsInTable = colsInTable(table)
|
||||
|
||||
// Set [GoodTableColumn.sorter]s if the table has a RowSorter
|
||||
val rowSorter = ( table.rowSorter as? DefaultRowSorter<*,*> )
|
||||
if ( rowSorter != null ) {
|
||||
colsInTable.forEachIndexed { i, col ->
|
||||
rowSorter.setSortable( i, col.sortable )
|
||||
if ( col.sorter != null ) {
|
||||
rowSorter.setComparator( i, col.sorter )
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ( colsInTable.any { it.sorter != null } ) {
|
||||
// At least one column has a sorter set, but the table does not have a DefaultRowSorter.
|
||||
// this is fine if you are providing your own RowSorter that looks at [GoodTableColumn.sorter]
|
||||
}
|
||||
}
|
||||
|
||||
// show/hide columns
|
||||
colsInTable.forEach { col ->
|
||||
col.visible?.addListener { _, _ -> propagateColumnVisibility(table) }
|
||||
}
|
||||
propagateColumnVisibility(table)
|
||||
|
||||
if ( table is GoodTable ) {
|
||||
val fillWeights = colsInTable
|
||||
.asSequence()
|
||||
.mapIndexedNotNull { i, col ->
|
||||
col.fillWeight?.let { i to it }
|
||||
}
|
||||
.toMap()
|
||||
val fixedWidths = colsInTable
|
||||
.asSequence()
|
||||
.mapIndexedNotNull { i, col ->
|
||||
col.fixedWidth?.let { i to it }
|
||||
}
|
||||
.toMap()
|
||||
if ( fillWeights.isNotEmpty() || fixedWidths.isNotEmpty() ) {
|
||||
table.fillWeights = fillWeights
|
||||
table.fixedWidths.putAll( fixedWidths )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Remove columns from the JTable (they stay in the model)
|
||||
* according to [GoodTableColumn.visible].
|
||||
*/
|
||||
private fun propagateColumnVisibility(table:JTable) {
|
||||
// index of next column
|
||||
var i = 0
|
||||
|
||||
colsInTable(table).forEach { col ->
|
||||
val show = col.visible?.value ?: true
|
||||
|
||||
if ( show ) {
|
||||
val tableCol = table.columnModel.columns.asSequence().firstOrNull { it == col }
|
||||
if ( tableCol != null ) {
|
||||
// already there
|
||||
} else {
|
||||
// add - have to add to the end and then move
|
||||
table.addColumn(col)
|
||||
table.moveColumn( table.columnCount-1 ,i )
|
||||
}
|
||||
i++
|
||||
} else {
|
||||
// remove if it is there, noop if it is not
|
||||
table.removeColumn(col)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fireAllTableRowsUpdated() {
|
||||
fireTableRowsUpdated(0,rowCount-1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a GoodTableModel to a JTable.
|
||||
*
|
||||
* I thought about creating a method
|
||||
*
|
||||
* JTable.goodTableModel(op:GoodTableModel.()->Unit)
|
||||
*
|
||||
* to build the model inline in the table, but that lets you access the
|
||||
* table before the model is completely build, which could lead to bugs.
|
||||
*
|
||||
* @see [GoodTableModel.setup]
|
||||
*/
|
||||
fun <T:Any> JTable.goodTableModel( hmodel:GoodTableModel<T> ) {
|
||||
model = hmodel
|
||||
columnModel = hmodel.columnModel(this)
|
||||
hmodel.setup(this)
|
||||
hmodel.fireTableDataChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Column for [GoodTableModel].
|
||||
*
|
||||
* @param name Column name. Used in the header by default.
|
||||
* @param identifier Value for [TableColumn.identifier].
|
||||
* @param cls The column type, to be returned by [TableModel.getColumnClass]
|
||||
*/
|
||||
class GoodTableColumn<T:Any,C>(val name:String, identifier:Any? =null, val cls:Class<*>):TableColumn() {
|
||||
|
||||
init {
|
||||
setIdentifier(identifier)
|
||||
}
|
||||
|
||||
/** Calculates a cell's value based on the row value. */
|
||||
var cellValueCalc:CellValueCalcIndexed<T,C>? = null
|
||||
|
||||
/** Calculates if the cell is editable based on the row value. */
|
||||
var isEditableCalc:IsEditableCalc<T>? = null
|
||||
|
||||
/** Callback for when a cell's value changes. */
|
||||
var changeHandler:CellValueChangeHandler<T,C>? = null
|
||||
|
||||
/** Callback for when a cell is clicked. */
|
||||
var clickHandler:CellClickHandler<T,C>? = null
|
||||
|
||||
/** Callback for when a header cell is clicked. */
|
||||
var headerClickHandler:HeaderClickHandler? = null
|
||||
|
||||
/**
|
||||
* Fill weights for [resizeColumnWidths].
|
||||
* Automatically set by [GoodTableModel.setup] if table is a [GoodTable].
|
||||
*/
|
||||
var fillWeight:Float? = null
|
||||
|
||||
/**
|
||||
* Fixed widths for [resizeColumnWidths].
|
||||
* Automatically set by [GoodTableModel.setup] if table is a [GoodTable].
|
||||
*/
|
||||
var fixedWidth:Int? = null
|
||||
|
||||
/**
|
||||
* Is the column sortable.
|
||||
*
|
||||
* If [JTable.rowSorter] is not null,
|
||||
* [GoodTableModel.setup] will set the column sortable according to [sortable].
|
||||
* So if you want to use this, set [JTable.autoCreateRowSorter] to true
|
||||
* or create a sorter by some other method before calling [GoodTableModel.setup].
|
||||
*/
|
||||
var sortable:Boolean = true
|
||||
|
||||
/** Sorter to use if the table has a row sorter.
|
||||
*
|
||||
* If [sorter] and [JTable.rowSorter] are not null,
|
||||
* [GoodTableModel.setup] will set this comparator.
|
||||
* So if you want to use this, set [JTable.autoCreateRowSorter] to true
|
||||
* or create a sorter by some other method before calling [GoodTableModel.setup].
|
||||
*/
|
||||
var sorter:Comparator<C>? = null
|
||||
|
||||
/**
|
||||
* Observable indicating if the column is visible. Always visible if null.
|
||||
*
|
||||
* [GoodTableModel.setup] sets up listeners to this property to show/hide the column.
|
||||
*/
|
||||
var visible:Property<Boolean>? = null
|
||||
|
||||
/** If not null, only put in these tables' ColumnModel. */
|
||||
var tables:Set<JTable>? = null
|
||||
|
||||
/** Set the function that calculates a cell's value based on the row value. */
|
||||
fun cellValue( calc:CellValueCalcIndexed<T,C>) {
|
||||
this.cellValueCalc = calc
|
||||
}
|
||||
|
||||
/** Set the function that calculates a cell's value based on the row value. */
|
||||
fun cellValue( calc:CellValueCalc<T,C>) = cellValue { value, index -> calc(value) }
|
||||
|
||||
/** Sets the function that determines if a cell is editable based on the row value. */
|
||||
fun isEditable( calc:IsEditableCalc<T>) {
|
||||
this.isEditableCalc = calc
|
||||
}
|
||||
|
||||
/** Call [changeHandler] for the new value. */
|
||||
internal fun setValueAt( rowValue:T, newValue:Any?, rowIndex:Int, columnIndex:Int ) {
|
||||
changeHandler?.invoke( rowValue, newValue as C, rowIndex, columnIndex )
|
||||
}
|
||||
|
||||
/** Register a callback for when a cell's value changes. */
|
||||
fun onChange( changeHandler:CellValueChangeHandler<T,C> ) {
|
||||
this.changeHandler = changeHandler
|
||||
}
|
||||
|
||||
/** Register a callback for when a cell is clicked. */
|
||||
fun onClick( clickHandler:CellClickHandler<T,C> ) {
|
||||
this.clickHandler = clickHandler
|
||||
}
|
||||
|
||||
internal fun click( mouseEvent:MouseEvent, row:Int, col:Int, rowValue:T ) {
|
||||
clickHandler?.let { clickHandler ->
|
||||
cellValueCalc?.let { cellValueCalc ->
|
||||
val cellValue = cellValueCalc.invoke(rowValue,row)
|
||||
clickHandler.invoke( mouseEvent, row, col, rowValue, cellValue )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Register a callback for when a header cell is clicked. */
|
||||
fun onHeaderClick( handler:HeaderClickHandler ) {
|
||||
this.headerClickHandler = handler
|
||||
}
|
||||
|
||||
internal fun headerClick( mouseEvent:MouseEvent, col:Int ) {
|
||||
headerClickHandler?.invoke( mouseEvent, col )
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a renderer for this cell.
|
||||
*
|
||||
* The row index passed to [op] is the table row index, not the model index.
|
||||
*/
|
||||
inline fun cellRenderer( crossinline op:DefaultTableCellRenderer.(table:JTable, rowValue:T, value:C, isSelected:Boolean, hasFocus:Boolean, row:Int, column:Int)->Component) {
|
||||
setCellRenderer( object:DefaultTableCellRenderer() {
|
||||
override fun getTableCellRendererComponent(table:JTable, value:Any?, isSelected:Boolean, hasFocus:Boolean, row:Int, column:Int):Component {
|
||||
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
|
||||
val rowValue:T = ( table.model as GoodTableModel<T>).getTableRowValue(table,row)!!
|
||||
return op(table,rowValue,value as C,isSelected,hasFocus,row,column)
|
||||
}
|
||||
} )
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the supplied JComboBox for the editor. The callback is called each time a cell edit starts.
|
||||
*
|
||||
* The row index passed to [op] is the table row index, not the model index.
|
||||
*/
|
||||
inline fun comboboxEditor( combobox:JComboBox<C>, crossinline op:JComboBox<C>.(table:JTable,rowValue:T,value:C,isSelected:Boolean,row:Int,column:Int)->Unit ) {
|
||||
setCellEditor( object:DefaultCellEditor(combobox) {
|
||||
override fun getTableCellEditorComponent(table:JTable, value:Any?, isSelected:Boolean, row:Int, column:Int):Component {
|
||||
super.getTableCellEditorComponent(table, value, isSelected, row, column)
|
||||
val rowValue:T = ( table.model as GoodTableModel<T>).getTableRowValue(table,row)!!
|
||||
op(combobox,table,rowValue,value as C,isSelected,row,column)
|
||||
return combobox
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a JComboBox with the supplied values for the editor.
|
||||
*
|
||||
* @param values Combobox options
|
||||
* @param autoComplete If true, use auto-completing input. Without this, a value will be selected
|
||||
* based on a single keystroke. This allows you to type until you get the desired match, then hit enter.
|
||||
* @param autoSizePopup If true, make the popup big enough to fit the widest value;
|
||||
* the popup may be larger than the combobox.
|
||||
* @param renderText Function to generate the text to show in the option for the given value
|
||||
*/
|
||||
fun comboboxEditor( values:Array<C>,
|
||||
autoComplete:Boolean = false,
|
||||
autoCompleteStrings:((C)->Sequence<String>)? = null,
|
||||
autoSizePopup:Boolean = true,
|
||||
renderText:(value:C)->String? ) {
|
||||
val combobox = JComboBox(values)
|
||||
|
||||
combobox.prototypeDisplayValue = values
|
||||
.asSequence()
|
||||
.map { value ->
|
||||
val text = renderText(value)
|
||||
val length = text?.length ?: 0
|
||||
Triple(value,text,length)
|
||||
}
|
||||
.maxByOrNull { (value,text,length) -> length }
|
||||
?.first
|
||||
|
||||
if( autoSizePopup ) {
|
||||
combobox.addPopupMenuListener(BoundsPopupMenuListener(isPopupWider = true))
|
||||
}
|
||||
combobox.renderer { list, value, index, isSelected, cellHasFocus ->
|
||||
text = renderText.invoke(value) ?: " "
|
||||
this
|
||||
}
|
||||
if ( autoComplete ) {
|
||||
combobox.autoComplete(autoCompleteStrings)
|
||||
cellEditor = ComboBoxCellEditor(combobox)
|
||||
} else {
|
||||
cellEditor = DefaultCellEditor(combobox)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a JComboBox with the supplied [model].
|
||||
*
|
||||
* @param values Combobox options
|
||||
* @param autoComplete If true, use auto-completing input. Without this, a value will be selected
|
||||
* based on a single keystroke. This allows you to type until you get the desired match, then hit enter.
|
||||
* @param autoSizePopup If true, make the popup big enough to fit the widest value;
|
||||
* the popup may be larger than the combobox.
|
||||
* @param renderText Function to generate the text to show in the option for the given value
|
||||
* @param onEdit Callback before the combobox is shown with the row value and cell value.
|
||||
* This allows making changes to [model] first.
|
||||
*/
|
||||
fun comboboxEditor( model:ComboBoxModel<C>,
|
||||
autoComplete:Boolean = false,
|
||||
autoCompleteStrings:((C)->Sequence<String>)? = null,
|
||||
autoSizePopup:Boolean = true,
|
||||
renderText:(value:C)->String?,
|
||||
onEdit:((T,C)->Unit)? = null,
|
||||
) {
|
||||
val combobox = JComboBox(model)
|
||||
|
||||
combobox.prototypeDisplayValue = (0 until model.size )
|
||||
.asSequence()
|
||||
.map { index -> model.getElementAt(index) }
|
||||
.map { value ->
|
||||
val text = renderText(value)
|
||||
val length = text?.length ?: 0
|
||||
Triple(value,text,length)
|
||||
}
|
||||
.maxByOrNull { (value,text,length) -> length }
|
||||
?.first
|
||||
|
||||
if( autoSizePopup ) {
|
||||
combobox.addPopupMenuListener( BoundsPopupMenuListener(isPopupWider=true) )
|
||||
}
|
||||
combobox.renderer { list, value, index, isSelected, cellHasFocus ->
|
||||
text = renderText.invoke(value) ?: " "
|
||||
this
|
||||
}
|
||||
if ( autoComplete ) {
|
||||
combobox.autoComplete(autoCompleteStrings)
|
||||
}
|
||||
cellEditor = object:ComboBoxCellEditor(combobox) {
|
||||
override fun getTableCellEditorComponent(table:JTable, value:Any?, isSelected:Boolean, row:Int, column:Int):Component {
|
||||
val rowValue:T = (table.model as GoodTableModel<T>).getTableRowValue(table,row)!!
|
||||
onEdit?.invoke(rowValue,value as C)
|
||||
return super.getTableCellEditorComponent(table, value, isSelected, row, column)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/main/kotlin/net/eksb/kswingutil/table/TableUtil.kt
Normal file
194
src/main/kotlin/net/eksb/kswingutil/table/TableUtil.kt
Normal file
@@ -0,0 +1,194 @@
|
||||
package net.eksb.kswingutil.table
|
||||
|
||||
import net.eksb.kswingutil.ext.onClick
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JTable
|
||||
import javax.swing.ListSelectionModel
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
fun setupTableSizes(table: JTable) {
|
||||
val font = table.font
|
||||
val metrics = table.getFontMetrics(font)
|
||||
val desiredHeight = (metrics.height.toFloat() * 0.4f).toInt() + metrics.height
|
||||
if (desiredHeight > table.rowHeight) {
|
||||
table.rowHeight = desiredHeight
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the table's columns to fit the cell values (looking at up to the first 10000 rows).
|
||||
*
|
||||
* @param table JTable whose columns to resize
|
||||
* @param considerHeaderWidth If true, also resize to fit the header content.
|
||||
* @param fillWeights Map of model column index to weight; if not null and not empty,
|
||||
* after resizing, if there is space left, increase the size of the columns in the map
|
||||
* according to their weights. Columns are not sized proportional to their weights,
|
||||
* extra space is allocated to columns according to their weights.
|
||||
* @param fixedWidths Map of model column index to fixed width for the column. Do not resize these columns.
|
||||
*/
|
||||
fun resizeColumnWidths(table: JTable, considerHeaderWidth:Boolean = false,
|
||||
fillWeights:Map<Int,Float>? = null,
|
||||
fixedWidths:Map<Int,Int>? = null
|
||||
) {
|
||||
val columnModel = table.columnModel
|
||||
val font = table.font
|
||||
val metrics = table.getFontMetrics(font)
|
||||
val colNames = Array<String>(table.columnCount) { table.getColumnName(it) }
|
||||
val widths = IntArray(table.columnCount) { ci ->
|
||||
val mci = table.convertColumnIndexToModel(ci)
|
||||
fixedWidths?.get(mci) ?: max(metrics.stringWidth(" ${colNames[ci]} W"), 15)
|
||||
}
|
||||
val ww = metrics.stringWidth("W")
|
||||
val rowCnt = min(table.rowCount, 10000)
|
||||
if ( considerHeaderWidth ) {
|
||||
for (ci in 0 until table.columnCount) {
|
||||
val mci = table.convertColumnIndexToModel(ci)
|
||||
if ( fixedWidths == null || ! fixedWidths.containsKey(mci) ) {
|
||||
val column = table.tableHeader.columnModel.getColumn(ci)
|
||||
val renderer = column.headerRenderer ?: table.tableHeader.defaultRenderer
|
||||
val comp = renderer.getTableCellRendererComponent(table,column.headerValue,true,true,0,ci)
|
||||
val width = min(300, comp.preferredSize.width + ww)
|
||||
if (width > widths[ci]) widths[ci] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
for (row in 0 until rowCnt) {
|
||||
for (ci in 0 until table.columnCount) {
|
||||
val mci = table.convertColumnIndexToModel(ci)
|
||||
if ( fixedWidths == null || ! fixedWidths.containsKey(mci) ) {
|
||||
//val colName = colNames[ci]
|
||||
val renderer = table.getCellRenderer(row, ci)
|
||||
val comp = table.prepareRenderer(renderer, row, ci)
|
||||
val width = min(300, comp.preferredSize.width + ww)
|
||||
if (width > widths[ci]) widths[ci] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( fillWeights != null && fillWeights.isNotEmpty() ) {
|
||||
val left = table.parent.width - widths.sum()
|
||||
// fill weights for columns in [fixedWidths] are not applicable
|
||||
val applicableFillWeights =
|
||||
if ( fixedWidths == null ) {
|
||||
fillWeights
|
||||
} else {
|
||||
fillWeights
|
||||
.filter { (mci,prio) ->
|
||||
! fixedWidths.containsKey(mci)
|
||||
}
|
||||
}
|
||||
if ( left > 0 ) {
|
||||
val prioTotal = applicableFillWeights.values.sum()
|
||||
if ( prioTotal > 0 ) {
|
||||
applicableFillWeights.asSequence()
|
||||
.drop(1) // because later we use the first to fill available space
|
||||
.forEach { (mci,prio) ->
|
||||
val ci = table.convertColumnIndexToView(mci)
|
||||
if ( ci >= 0 ) {
|
||||
widths[ci] = widths[ci] + ( (prio/prioTotal) * left ).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
val firstMci = applicableFillWeights.keys.first()
|
||||
val firstCi = table.convertColumnIndexToView(firstMci)
|
||||
if ( firstCi >= 0 ) { // it
|
||||
widths[firstCi] = widths[firstCi] + table.parent.width - widths.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//table.preferredSize = Dimension(widths.sum(), table.preferredSize.height)
|
||||
for (ci in 0 until table.columnCount) {
|
||||
columnModel.getColumn(ci).preferredWidth = widths[ci]
|
||||
if ( fixedWidths != null && fixedWidths.containsKey(table.convertColumnIndexToModel(ci)) ) {
|
||||
columnModel.getColumn(ci).minWidth = widths[ci]
|
||||
columnModel.getColumn(ci).maxWidth = widths[ci]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected indicies.
|
||||
* Because [ListSelectionModel.getSelectedIndices] is not in Java 8.
|
||||
*/
|
||||
fun ListSelectionModel.getSelectedIndexes():Set<Int> {
|
||||
val min = minSelectionIndex
|
||||
val max = maxSelectionIndex
|
||||
return if ( min >= 0 && max >= 0 ) {
|
||||
(min..max)
|
||||
.asSequence()
|
||||
.filter { index -> isSelectedIndex(index) }
|
||||
.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize two tables' sorting and selection.
|
||||
*
|
||||
* Sharing [RowSorter] and [ListSelectionModel] between tables does not work,
|
||||
* for reasons that are not clear to me. This method detects changes to one
|
||||
* and copies the state to the other.
|
||||
*/
|
||||
fun syncSelectionModelAndRowSorter( table1:JTable, table2:JTable ) {
|
||||
var changing = false
|
||||
fun copySelection(a:JTable,b:JTable) {
|
||||
b.selectionModel.clearSelection()
|
||||
a.selectionModel.getSelectedIndexes().forEach { index ->
|
||||
b.selectionModel.addSelectionInterval(index,index)
|
||||
}
|
||||
}
|
||||
fun copySort(a:JTable,b:JTable) {
|
||||
b.rowSorter.sortKeys = a.rowSorter.sortKeys
|
||||
}
|
||||
fun setup(a:JTable,b:JTable) {
|
||||
a.selectionModel.addListSelectionListener { event ->
|
||||
if ( ! event.valueIsAdjusting ) {
|
||||
if ( ! changing ) {
|
||||
try {
|
||||
changing = true
|
||||
copySelection(a,b)
|
||||
} finally {
|
||||
changing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
a.rowSorter.addRowSorterListener { event ->
|
||||
if ( ! changing ) {
|
||||
try {
|
||||
changing = true
|
||||
copySort(a,b)
|
||||
} finally {
|
||||
changing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setup(table1,table2)
|
||||
setup(table2,table1)
|
||||
}
|
||||
|
||||
fun JTable.onCellClick( op:(MouseEvent,Int,Int)->Unit ) {
|
||||
onClick { event ->
|
||||
val point = event.point
|
||||
val rowIndex = rowAtPoint(point)
|
||||
val colIndex = columnAtPoint(point)
|
||||
if ( rowIndex >= 0 && colIndex >= 0 ) {
|
||||
op(event,rowIndex,colIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun JTable.onHeaderClick( op:(MouseEvent,Int)->Unit ) {
|
||||
tableHeader.addMouseListener( object:MouseAdapter() {
|
||||
override fun mouseClicked(mouseEvent:MouseEvent) {
|
||||
val colIndex = columnAtPoint( mouseEvent.point )
|
||||
op(mouseEvent,colIndex)
|
||||
}
|
||||
} )
|
||||
}
|
||||
Reference in New Issue
Block a user