various
This commit is contained in:
@@ -10,6 +10,7 @@ group = "net.eksb"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api("com.miglayout:miglayout-swing:11.4.2")
|
api("com.miglayout:miglayout-swing:11.4.2")
|
||||||
|
api("org.swinglabs:swingx:1.6.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
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.MutableProperty
|
||||||
import net.eksb.kswingutil.property.Property
|
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.Component
|
||||||
import java.awt.Container
|
import java.awt.Container
|
||||||
import java.awt.Font
|
import java.awt.Font
|
||||||
@@ -18,13 +19,36 @@ import javax.swing.JComponent
|
|||||||
import javax.swing.JLabel
|
import javax.swing.JLabel
|
||||||
import javax.swing.JList
|
import javax.swing.JList
|
||||||
import javax.swing.JPanel
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.JScrollPane
|
||||||
import javax.swing.JSeparator
|
import javax.swing.JSeparator
|
||||||
|
import javax.swing.JTabbedPane
|
||||||
|
import javax.swing.JToolBar
|
||||||
|
|
||||||
fun Container.separator(constraints:Any?=null, alignment:Int, block:JSeparator.()->Unit ={}):JSeparator =
|
fun Container.separator(constraints:Any?=null, alignment:Int, block:JSeparator.()->Unit ={}):JSeparator =
|
||||||
add(constraints, JSeparator(alignment),block)
|
add(constraints, JSeparator(alignment),block)
|
||||||
|
|
||||||
fun Container.panel(constraints:Any?=null, block:JPanel.()->Unit ={}):JPanel = add(constraints, JPanel(),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 Container.horizontalGlue():Component = add(Box.createHorizontalGlue())
|
||||||
|
|
||||||
fun JComponent.emptyBorder(size:Int) {
|
fun JComponent.emptyBorder(size:Int) {
|
||||||
@@ -50,6 +74,32 @@ fun <T> JComboBox<T>.model(values:Array<T>) {
|
|||||||
model = DefaultComboBoxModel<T>(values)
|
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.(
|
typealias ListCellRendererLambda<T> = DefaultListCellRenderer.(
|
||||||
list:JList<T>,
|
list:JList<T>,
|
||||||
value: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