blob: ea825cf9206310d8d65ea44db0824e44e415fe30 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jorphan.gui
import org.apiguardian.api.API
import java.awt.Container
import java.awt.FlowLayout
import java.awt.event.ActionEvent
import javax.swing.AbstractAction
import javax.swing.Box
import javax.swing.JCheckBox
import javax.swing.JComboBox
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JPopupMenu
import javax.swing.SwingUtilities
import javax.swing.event.ChangeEvent
/**
* A Checkbox that can be converted to an editable field and back.
* @since 5.6
*/
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public open class JEditableCheckBox(
label: String,
private val configuration: Configuration
) : JPanel() {
public companion object {
public const val CHECKBOX_CARD: String = "checkbox"
public const val EDITABLE_CARD: String = "editable"
public const val VALUE_PROPERTY: String = "value"
}
/**
* The representation of the state.
*/
public sealed interface Value {
public companion object {
@JvmStatic
public fun of(value: kotlin.Boolean): Boolean =
if (value) Boolean.TRUE else Boolean.FALSE
}
/**
* The value is a checkbox.
*/
public sealed interface Boolean : Value {
public val value: kotlin.Boolean
public object TRUE : Boolean {
override val value: kotlin.Boolean
get() = true
}
public object FALSE : Boolean {
override val value: kotlin.Boolean
get() = false
}
}
/**
* The value is a free text.
*/
public data class Text(val value: String) : Value
}
/**
* Supplies the parameters to [JEditableCheckBox].
*/
public data class Configuration(
/** Menu item title to "start editing" the checkbox value. */
val startEditing: String = "Use Expression",
/** The title to be used for "true" value in the checkbox. */
val trueValue: String = "true",
/** The title to be used for "false" value in the checkbox. */
val falseValue: String = "false",
/** Extra values to be added for the combobox. */
val extraValues: List<String> = listOf(),
)
private val cards = CardLayoutWithSizeOfCurrentVisibleElement()
private val useExpressionAction = object : AbstractAction(configuration.startEditing) {
override fun actionPerformed(e: ActionEvent?) {
cards.next(this@JEditableCheckBox)
comboBox.requestFocusInWindow()
fireValueChanged()
}
}
private val checkbox: JCheckBox = JCheckBox(label).apply {
val cb = this
componentPopupMenu = JPopupMenu().apply {
add(useExpressionAction)
}
addItemListener {
fireValueChanged()
}
}
private val comboBox: JComboBox<String> = JComboBox<String>().apply {
isEditable = true
configuration.extraValues.forEach {
addItem(it)
}
addItem(configuration.trueValue)
addItem(configuration.falseValue)
addActionListener {
val jComboBox = it.source as JComboBox<*>
SwingUtilities.invokeLater {
if (jComboBox.isPopupVisible) {
fireValueChanged()
return@invokeLater
}
when (val value = jComboBox.selectedItem as String) {
configuration.trueValue, configuration.falseValue -> {
checkbox.isSelected = value == configuration.trueValue
cards.show(this@JEditableCheckBox, CHECKBOX_CARD)
checkbox.requestFocusInWindow()
fireValueChanged()
}
}
}
}
// TODO: trigger value changed when the text is changed
}
private val textFieldLabel = JLabel(label).apply {
labelFor = comboBox
}
@Transient
private var changeEvent: ChangeEvent? = null
init {
layout = cards
add(
// A dummy container ensures popup menu appears on top of the checkbox
Container().apply {
layout = FlowLayout(FlowLayout.LEADING, 0, 0)
add(checkbox)
},
CHECKBOX_CARD
)
add(
Container().apply {
// FlowLayout adds horizontal gap before the first element, so we set zero gaps
// and add the gap between the components manually.
layout = FlowLayout(FlowLayout.LEADING, 0, 0)
add(comboBox)
add(Box.createHorizontalStrut(5))
add(textFieldLabel)
},
EDITABLE_CARD
)
}
private var oldValue = value
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
checkbox.isEnabled = enabled
comboBox.isEnabled = enabled
useExpressionAction.isEnabled = enabled
}
private fun fireValueChanged() {
val newValue = value
if (value != oldValue) {
firePropertyChange(VALUE_PROPERTY, oldValue, newValue)
oldValue = newValue
}
}
public var value: Value
get() = when (components.indexOfFirst { it.isVisible }) {
0 -> if (checkbox.isSelected) Value.Boolean.TRUE else Value.Boolean.FALSE
else -> Value.Text(comboBox.selectedItem as String)
}
set(value) {
when (value) {
is Value.Boolean -> {
comboBox.selectedItem = ""
checkbox.isSelected = value.value
cards.show(this, CHECKBOX_CARD)
}
is Value.Text -> {
checkbox.isSelected = false
comboBox.selectedItem = value.value
cards.show(this, EDITABLE_CARD)
}
}
fireValueChanged()
}
@get:JvmSynthetic
public var booleanValue: Boolean
@Deprecated(message = "write-only property", level = DeprecationLevel.HIDDEN)
get() = TODO()
set(value) {
this.value = Value.of(value)
}
@get:JvmSynthetic
public var stringValue: String
@Deprecated(message = "write-only property", level = DeprecationLevel.HIDDEN)
get() = TODO()
set(value) {
this.value = Value.Text(value)
}
public fun makeSmall() {
JFactory.small(checkbox)
// We do not make combobox small as the expression migh be hard to read
}
}