All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.dankito.utils.javafx.ui.dialogs.JavaFXDialogService.kt Maven / Gradle / Ivy

package net.dankito.utils.javafx.ui.dialogs

import javafx.scene.control.*
import javafx.scene.layout.GridPane
import javafx.scene.layout.Priority
import javafx.scene.layout.Region
import javafx.scene.layout.VBox
import javafx.stage.Screen
import javafx.stage.Stage
import net.dankito.utils.javafx.util.FXUtils
import net.dankito.utils.localization.Localization
import net.dankito.utils.ui.dialogs.ConfirmationDialogButton
import net.dankito.utils.ui.dialogs.ConfirmationDialogConfig
import net.dankito.utils.ui.dialogs.IDialogService
import net.dankito.utils.ui.dialogs.InputType
import org.slf4j.LoggerFactory
import java.awt.Desktop
import java.io.PrintWriter
import java.io.StringWriter
import java.net.URI
import java.net.URLEncoder
import kotlin.concurrent.thread


class JavaFXDialogService(private val localizationProperty: Localization, private val showSendBugReportButton: Boolean = false,
                          private val emailAddressForBugReports: String? = null, private val subjectForBugReports: String? = null) : IDialogService {

    companion object {
        private val log = LoggerFactory.getLogger(JavaFXDialogService::class.java)
    }


    override fun getLocalization(): Localization {
        return localizationProperty
    }


    override fun showLittleInfoMessage(infoMessage: CharSequence) {
        // nothing to do for JavaFX, was only introduced for showing Android Toasts
    }

    override fun showInfoMessage(infoMessage: CharSequence, alertTitle: CharSequence?) {
        showInfoMessage(infoMessage, alertTitle, null)
    }

    fun showInfoMessage(infoMessage: CharSequence, alertTitle: CharSequence?, owner: Stage?) {
        FXUtils.runOnUiThread { showInfoMessageOnUiThread(infoMessage, alertTitle, owner) }
    }

    private fun showInfoMessageOnUiThread(infoMessage: CharSequence, alertTitle: CharSequence?, owner: Stage?) {
        val alert = createDialog(Alert.AlertType.INFORMATION, infoMessage, alertTitle, owner, ButtonType.OK)

        alert.showAndWait()
    }


    override fun showConfirmationDialog(message: CharSequence, alertTitle: CharSequence?, config: ConfirmationDialogConfig, optionSelected: (ConfirmationDialogButton) -> Unit) {
        showConfirmationDialog(message, alertTitle, config, null, optionSelected)
    }

    fun showConfirmationDialog(message: CharSequence, alertTitle: CharSequence?, config: ConfirmationDialogConfig = ConfirmationDialogConfig(), owner: Stage?, optionSelected: (ConfirmationDialogButton) -> Unit) {
        FXUtils.runOnUiThread { showConfirmationDialogOnUiThread(message, alertTitle, config, owner, optionSelected) }
    }

    private fun showConfirmationDialogOnUiThread(message: CharSequence, alertTitle: CharSequence?, config: ConfirmationDialogConfig, owner: Stage?, optionSelected: (ConfirmationDialogButton) -> Unit) {
        val buttons = ArrayList()
        if(config.showNoButton) {
            buttons.add(ButtonType.NO)
        }
        if(config.showThirdButton) {
            buttons.add(ButtonType.CANCEL) // TODO: test
        }
        buttons.add(ButtonType.YES)

        val alert = createDialog(Alert.AlertType.CONFIRMATION, message, alertTitle, owner, *buttons.toTypedArray())

        when(alert.showAndWait().get()) {
            ButtonType.NO -> return optionSelected(ConfirmationDialogButton.No)
            else -> return optionSelected(ConfirmationDialogButton.Confirm)
        }
    }


    override fun showErrorMessage(errorMessage: CharSequence, alertTitle: CharSequence?, exception: Exception?) {
        showErrorMessage(errorMessage, alertTitle, exception, null)
    }

    fun showErrorMessage(errorMessage: CharSequence, alertTitle: CharSequence?, exception: Exception?, owner: Stage?) {
        FXUtils.runOnUiThread { showErrorMessageOnUiThread(errorMessage, alertTitle, exception, owner) }
    }

    private fun showErrorMessageOnUiThread(errorMessage: CharSequence, alertTitle: CharSequence?, exception: Exception?, owner: Stage?) {
        val alert = createDialog(Alert.AlertType.ERROR, errorMessage, alertTitle, owner, ButtonType.OK)
        alert.isResizable = true

        if(exception != null) {
            createExpandableException(alert, exception)
        }

        val clickedButton = alert.showAndWait().get()

        if(clickedButton?.buttonData == ButtonBar.ButtonData.OTHER && exception != null) {
            sendBugReport(exception, errorMessage)
        }
    }

    private fun createExpandableException(alert: Alert, exception: Exception) {
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        exception.printStackTrace(pw)
        val exceptionText = sw.toString()

        val label = Label(localizationProperty.getLocalizedString("dialog.service.error.stacktrace.label"))

        val textArea = TextArea(exceptionText)
        textArea.isEditable = false
        textArea.isWrapText = true

        textArea.maxWidth = FXUtils.SizeMaxValue
        textArea.maxHeight = FXUtils.SizeMaxValue
        GridPane.setVgrow(textArea, Priority.ALWAYS)
        GridPane.setHgrow(textArea, Priority.ALWAYS)

        val expContent = GridPane()
        expContent.maxWidth = FXUtils.SizeMaxValue
        expContent.add(label, 0, 0)
        expContent.add(textArea, 0, 1)

        if(showSendBugReportButton) {
            val sendBugReportButton = ButtonType(localizationProperty.getLocalizedString("alert.message.send.bug.report"), ButtonBar.ButtonData.OTHER)
            alert.buttonTypes.add(sendBugReportButton)
        }

        // Set expandable Exception into the dialog pane.
        alert.dialogPane.expandableContent = expContent
    }

    private fun sendBugReport(exception: Exception, errorMessage: CharSequence) {
        if(emailAddressForBugReports != null) {
            thread { // get off UI thread
                try {
                    val mailUri = createMailUriForBugReport(errorMessage, exception)

                    Desktop.getDesktop().mail(URI.create(mailUri))
                } catch(e: Exception) { log.error("Could not send bug report", e) }
            }
        }
    }

    private fun createMailUriForBugReport(errorMessage: CharSequence, exception: Exception): String {
        var bugReport = errorMessage.toString() + "\r\n\r\n"

        val sw = StringWriter()
        val pw = PrintWriter(sw)
        exception.printStackTrace(pw)
        val stackTrace = sw.toString()
        pw.close()

        bugReport += "Stack trace:\r\n\r\n$stackTrace"

        var mailUri = "mailto:$emailAddressForBugReports?"

        subjectForBugReports?.let {
            mailUri += "subject=${URLEncoder.encode(it, "utf-8").replace("+", "%20")}&" // use %20, not URLEncoder's + as Thunderbird understands only %20
        }

        val encodedBugReport = URLEncoder.encode(bugReport, "utf-8").replace("+", "%20")
        mailUri += "body=$encodedBugReport"

        return mailUri
    }


    private fun createDialog(alertType: Alert.AlertType, message: CharSequence, alertTitle: CharSequence?, owner: Stage?, vararg buttons: ButtonType): Alert {
        val alert = Alert(alertType)

        (alertTitle as? String)?.let { alert.title = it }

        owner?.let { alert.initOwner(it) }

        (message as? String)?.let { setAlertContent(alert, it) }
        alert.headerText = null

        alert.buttonTypes.setAll(*buttons)

        return alert
    }

    private fun setAlertContent(alert: Alert, content: String) {
        var maxWidth = Screen.getPrimary().visualBounds.width

        if(alert.owner != null) {
            FXUtils.getScreenWindowLeftUpperCornerIsIn(alert.owner)?.let { ownersScreen ->
                maxWidth = ownersScreen.visualBounds.width
            }
        }

        maxWidth *= 0.6 // set max width to 60 % of Screen width

        val contentLabel = Label(content)
        contentLabel.isWrapText = true
        contentLabel.prefHeight = Region.USE_COMPUTED_SIZE
        contentLabel.maxHeight = FXUtils.SizeMaxValue
        contentLabel.maxWidth = maxWidth

        val contentPane = VBox(contentLabel)
        contentPane.prefHeight = Region.USE_COMPUTED_SIZE
        contentPane.maxHeight = FXUtils.SizeMaxValue
        VBox.setVgrow(contentLabel, Priority.ALWAYS)

        alert.dialogPane.prefHeight = Region.USE_COMPUTED_SIZE
        alert.dialogPane.maxHeight = FXUtils.SizeMaxValue
        alert.dialogPane.maxWidth = maxWidth
        alert.dialogPane.content = contentPane
    }


    override fun askForTextInput(questionText: CharSequence, alertTitleText: CharSequence?, defaultValue: CharSequence?, type: InputType, callback: (Boolean, String?) -> Unit) {
        FXUtils.runOnUiThread { askForTextInputOnUiThread(questionText, alertTitleText, defaultValue, callback) }
    }

    private fun askForTextInputOnUiThread(questionText: CharSequence, alertTitleText: CharSequence?, defaultValue: CharSequence?, callback: (Boolean, String?) -> Unit) {
        val dialog = TextInputDialog(defaultValue as? String)
        dialog.headerText = null
        dialog.title = alertTitleText as? String
        dialog.contentText = questionText as? String

        val result = dialog.showAndWait()

        val enteredText = if(result.isPresent) result.get() else null
        callback(result.isPresent, enteredText)
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy