
tri.util.ui.FxUtils.kt Maven / Gradle / Ivy
The newest version!
/*-
* #%L
* tri.promptfx:promptfx
* %%
* Copyright (C) 2023 - 2025 Johns Hopkins University Applied Physics Laboratory
* %%
* Licensed 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.
* #L%
*/
package tri.util.ui
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import javafx.beans.binding.BooleanExpression
import javafx.beans.property.Property
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.beans.value.ObservableValue
import javafx.beans.value.WritableValue
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.embed.swing.SwingFXUtils
import javafx.event.EventTarget
import javafx.scene.control.*
import javafx.scene.image.Image
import javafx.scene.input.DataFormat
import javafx.scene.input.TransferMode
import javafx.scene.paint.Color
import javafx.scene.text.Text
import javafx.scene.text.TextFlow
import javafx.stage.Modality
import javafx.stage.StageStyle
import tornadofx.*
import tri.ai.prompt.AiPrompt
import tri.ai.prompt.AiPromptLibrary
import tri.promptfx.PromptFxConfig
import tri.promptfx.api.ImagesView
import tri.promptfx.promptFxFileChooser
import tri.util.loggerFor
import tri.util.warning
import java.io.File
import java.io.IOException
import javax.imageio.ImageIO
//region FILE I/O
/** Configures a [TextInputControl] to accept dropped files and set its text to the content of the first file. */
fun TextInputControl.enableDroppingFileContent() {
// enable dropping file content
setOnDragOver { it.acceptTransferModes(*TransferMode.COPY_OR_MOVE) }
setOnDragDropped {
if (it.dragboard.hasFiles()) {
textProperty().set(it.dragboard.files.first().readText())
}
it.isDropCompleted = true
it.consume()
}
}
//endregion
//region IMAGES
fun UIComponent.saveToFile(image: Image) {
promptFxFileChooser(
dirKey = PromptFxConfig.DIR_KEY_IMAGE,
title = "Save to File",
filters = arrayOf(PromptFxConfig.FF_PNG, PromptFxConfig.FF_ALL),
mode = FileChooserMode.Save
) {
it.firstOrNull()?.let {
writeImageToFile(image, it)
information("Image saved to file: ${it.name}", owner = primaryStage)
}
}
}
/** Writes an [Image] to a [File]. */
fun writeImageToFile(image: Image, file: File): Boolean = try {
file.outputStream().use { os ->
ImageIO.write(SwingFXUtils.fromFXImage(image, null), file.extension, os)
}
true
} catch (x: IOException) {
loggerFor().warning("Error saving image to file: $file", x)
false
}
/** Copies an image to a clipboard. */
fun UIComponent.copyToClipboard(image: Image) {
// the original image doesn't seem to copy to clipboard properly, so cycle it through [BufferedImage]
val image2 = SwingFXUtils.fromFXImage(image, null)
val fxImage = SwingFXUtils.toFXImage(image2, null)
clipboard.put(DataFormat.IMAGE, fxImage)
}
//endregion
//region Text and TextFlow UTILS
fun TextFlow.plainText() = children.joinToString("") {
(it as? Text)?.text ?:
(it as? Hyperlink)?.text ?: ""
}
//endregion
//region ICONS
fun icon(icon: FontAwesomeIcon) = FontAwesomeIconView(icon)
val FontAwesomeIcon.graphic
get() = icon(this)
val FontAwesomeIconView.gray
get() = apply {
fill = Color.GRAY
}
val FontAwesomeIconView.navy
get() = apply {
fill = Color.NAVY
}
val FontAwesomeIconView.burgundy
get() = apply {
fill = Color(128.0/255, 0.0, 32.0/255, 1.0)
}
val FontAwesomeIconView.forestGreen
get() = apply {
fill = Color(34.0/255, 139.0/255, 34.0/255, 1.0)
}
//endregion
//region UI BUILDERS
/**
* Creates a [menubutton] to select a template
*/
fun EventTarget.templatemenubutton(template: SimpleStringProperty, promptFilter: (Map.Entry) -> Boolean = { true }) =
listmenubutton(
items = { AiPromptLibrary.INSTANCE.prompts.filter(promptFilter).keys.sorted() },
action = { template.set(AiPromptLibrary.lookupPrompt(it).template) }
)
/**
* Creates a [menubutton] with the provided items and action.
* The list is dynamically updated each time the button is shown.
*/
fun EventTarget.listmenubutton(items: () -> Collection, action: (String) -> Unit) =
menubutton("", FontAwesomeIconView(FontAwesomeIcon.LIST)) {
setOnShowing {
this.items.clear()
items().forEach { key ->
item(key) {
action { action(key) }
}
}
}
}
/** Slider with editable label. */
fun EventTarget.sliderwitheditablelabel(range: IntRange, property: SimpleIntegerProperty) {
slider(range, property)
val tokenLabel = label(property.asString())
tokenLabel.apply {
setOnMouseClicked {
val textField = TextField(property.value.toString()).apply {
prefColumnCount = 1
setOnAction {
property.value = text.toIntOrNull()?.coerceIn(range) ?: property.value
replaceWith(tokenLabel)
}
focusedProperty().addListener { _, _, focused ->
if (!focused) {
property.value = text.toIntOrNull()?.coerceIn(range) ?: property.value
replaceWith(tokenLabel)
}
}
}
replaceWith(textField)
textField.requestFocus()
textField.selectAll()
}
}
}
//endregion
//region DIALOGS
/**
* Shows a dialog with the given [Image].
* Click to close dialog.
* Context menu provides a copy option.
*/
fun UIComponent.showImageDialog(image: Image) {
val d = dialog(
modality = Modality.APPLICATION_MODAL,
stageStyle = StageStyle.UNDECORATED,
owner = primaryStage
) {
imageview(image) {
onLeftClick { close() }
contextmenu {
item("Copy Image to Clipboard") {
action { copyToClipboard(image) }
}
}
}
padding = insets(0)
form.padding = insets(1)
form.background = Color.WHITE.asBackground()
}
// center dialog on window (dialog method doesn't do this because it adds content after centering on owner)
d?.owner?.let {
d.x = it.x + (it.width / 2) - (d.scene.width / 2)
d.y = it.y + (it.height / 2) - (d.scene.height / 2)
}
}
//endregion
//region PROPERTY BINDINGS
/**
* Create an observable list backed by a mutable property and a [List] or [ObservableList] property therein.
* @param obj the property to listen to
* @param op a function to extract the list from the property
*/
fun createListBinding(obj: ObservableValue, op: (X?) -> List): ObservableList =
createListBinding(obj, op) { _, it -> it }
/**
* Create an observable list backed by a mutable property and a [List] or [ObservableList] property therein.
* @param obj the property to listen to
* @param op a function to extract the list from the property
* @param transform a function to transform the list elements
*/
fun createListBinding(obj: ObservableValue, op: (X?) -> List, transform: (X, Y) -> Z): ObservableList {
var listeningList = op(obj.value) as? ObservableList
val resultList = observableListOf(listeningList?.map { transform(obj.value, it) } ?: listOf())
val listener = ListChangeListener { resultList.setAll(it.list.map { transform(obj.value, it) }) }
listeningList?.addListener(listener)
obj.onChange {
listeningList?.removeListener(listener)
if (it == null) {
resultList.setAll()
listeningList = null
} else {
val nueList = op(it)
resultList.setAll(nueList.map { transform(obj.value, it) })
listeningList = nueList as? ObservableList
listeningList?.addListener(listener)
}
}
return resultList
}
fun booleanListBindingOr(list: ObservableList, defaultValue: Boolean = false, itemToBooleanExpr: T.() -> BooleanExpression): BooleanExpression {
val facade = SimpleBooleanProperty()
fun rebind() {
if (list.isEmpty()) {
facade.unbind()
facade.value = defaultValue
} else {
facade.cleanBind(list.map(itemToBooleanExpr).reduce { a, b -> a.or(b) })
}
}
list.onChange { rebind() }
rebind()
return facade
}
//endregion
//region ListView BINDINGS
/** Binds a single selected value of a [ListView] to an existing [Property]. */
fun ListView.bindSelectionBidirectional(property: T) where T : WritableValue, T : Property {
selectionModel.selectionMode = SelectionMode.SINGLE
selectionModel.selectedItemProperty().onChange { property.value = it }
property.onChange {
if (it == null)
selectionModel.clearSelection()
else
selectionModel.select(it)
}
}
/** Binds multiple selected values of a [ListView] to an existing [ObservableList]. */
fun ListView.bindSelectionBidirectional(property: ObservableList) {
selectionModel.selectionMode = SelectionMode.MULTIPLE
var isUpdating = false
selectionModel.selectedItems.onChange {
isUpdating = true
property.setAll(it.list.toList())
isUpdating = false
}
property.onChange {
if (!isUpdating) {
val indices = it.list.map { items.indexOf(it) }.toIntArray()
if (indices.isEmpty())
selectionModel.clearSelection()
else
selectionModel.selectIndices(indices[0], *indices.drop(1).toIntArray())
}
}
}
//endregion
© 2015 - 2025 Weber Informatics LLC | Privacy Policy