improve eros error dialog, chat: url parsing

This commit is contained in:
Bixilon 2021-07-30 14:59:44 +02:00
parent 5d0d194ed2
commit 407019dadb
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
11 changed files with 145 additions and 37 deletions

View File

@ -43,7 +43,7 @@ class BaseComponent : ChatComponent {
}
}
constructor(parent: TextComponent? = null, legacy: String = "") {
constructor(parent: TextComponent? = null, legacy: String = "", restrictedMode: Boolean = false) {
val currentText = StringBuilder()
var currentColor = parent?.color
val currentFormatting: MutableSet<ChatFormattingCode> = parent?.formatting?.toMutableSet() ?: mutableSetOf()
@ -54,15 +54,34 @@ class BaseComponent : ChatComponent {
fun push() {
if (currentText.isNotEmpty()) {
parts += TextComponent(message = currentText.toString(), color = currentColor, formatting = currentFormatting.toMutableSet())
if (currentText.isEmpty()) {
return
}
val spaceSplit = currentText.split(' ')
for ((index, split) in spaceSplit.withIndex()) {
var clickEvent: ClickEvent? = null
for (protocol in URLProtocols.VALUES) {
if (!split.startsWith(protocol.prefix)) {
continue
}
if (protocol.restricted && restrictedMode) {
break
}
clickEvent = ClickEvent(ClickEvent.ClickEventActions.OPEN_URL, split)
break
}
parts += TextComponent(message = split, color = currentColor, formatting = currentFormatting.toMutableSet(), clickEvent = clickEvent)
if (index != spaceSplit.size - 1) {
parts += TextComponent(message = " ", color = currentColor, formatting = currentFormatting.toMutableSet())
}
}
currentFormatting.clear()
currentColor = null
currentText.clear()
}
currentFormatting.clear()
}
while (char != CharacterIterator.DONE) {
// ToDo: Parse urls with click event (and respect restrictedMode)
if (char != ProtocolDefinition.TEXT_COMPONENT_SPECIAL_PREFIX_CHAR) {
currentText.append(char)
char = iterator.next()
@ -94,7 +113,7 @@ class BaseComponent : ChatComponent {
push()
}
constructor(translator: Translator? = null, parent: TextComponent? = null, json: Map<String, Any>) {
constructor(translator: Translator? = null, parent: TextComponent? = null, json: Map<String, Any>, restrictedMode: Boolean = false) {
val currentParent: TextComponent?
var currentText = ""
json["text"]?.nullCast<String>()?.let {
@ -121,10 +140,10 @@ class BaseComponent : ChatComponent {
formatting.addOrRemove(PreChatFormattingCodes.STRIKETHROUGH, json["strikethrough"]?.toBoolean())
formatting.addOrRemove(PreChatFormattingCodes.OBFUSCATED, json["obfuscated"]?.toBoolean())
val clickEvent = json["clickEvent"]?.compoundCast()?.let { click -> ClickEvent(click) }
val clickEvent = json["clickEvent"]?.compoundCast()?.let { click -> ClickEvent(click, restrictedMode) }
val hoverEvent = json["hoverEvent"]?.compoundCast()?.let { hover -> HoverEvent(hover) }
val textComponent = MultiChatComponent(
val textComponent = TextComponent(
message = currentText,
color = color,
formatting = formatting,

View File

@ -76,7 +76,7 @@ interface ChatComponent {
val EMPTY = ChatComponent.of("")
@JvmOverloads
fun of(raw: Any? = null, translator: Translator? = null, parent: TextComponent? = null, ignoreJson: Boolean = false): ChatComponent {
fun of(raw: Any? = null, translator: Translator? = null, parent: TextComponent? = null, ignoreJson: Boolean = false, restrictedMode: Boolean = false): ChatComponent {
// ToDo: Remove gson, replace with maps
if (raw == null) {
return BaseComponent()
@ -85,13 +85,13 @@ interface ChatComponent {
return raw
}
if (raw is Map<*, *>) {
return BaseComponent(translator, parent, raw.unsafeCast())
return BaseComponent(translator, parent, raw.unsafeCast(), restrictedMode)
}
val string = when (raw) {
is List<*> -> {
val component = BaseComponent()
for (part in raw) {
component += of(part, translator, parent)
component += of(part, translator, parent, restrictedMode = restrictedMode)
}
return component
}
@ -99,7 +99,7 @@ interface ChatComponent {
}
if (!ignoreJson && string.startsWith('{')) {
try {
return BaseComponent(translator, parent, JSONSerializer.MAP_ADAPTER.fromJson(string)!!)
return BaseComponent(translator, parent, JSONSerializer.MAP_ADAPTER.fromJson(string)!!, restrictedMode)
} catch (ignored: JsonEncodingException) {
}
}

View File

@ -13,6 +13,10 @@
package de.bixilon.minosoft.data.text
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.data.text.events.ClickEvent
import de.bixilon.minosoft.data.text.events.HoverEvent
import de.bixilon.minosoft.gui.eros.dialog.ErosErrorReport.Companion.report
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.hyperlink
import de.bixilon.minosoft.gui.rendering.RenderConstants
import de.bixilon.minosoft.gui.rendering.RenderWindow
import de.bixilon.minosoft.gui.rendering.font.Font
@ -38,6 +42,8 @@ open class TextComponent(
message: Any? = "",
var color: RGBColor? = null,
var formatting: MutableSet<ChatFormattingCode> = mutableSetOf(),
var clickEvent: ClickEvent? = null,
var hoverEvent: HoverEvent? = null,
) : ChatComponent {
override var message: String = message?.toString() ?: "null"
@ -160,6 +166,26 @@ open class TextComponent(
}
}
nodes.add(text)
clickEvent?.let { event ->
when (event.action) {
ClickEvent.ClickEventActions.OPEN_URL -> text.hyperlink(event.value.toString())
else -> {
NotImplementedError("Unknown action ${event.action}").report()
return@let
}
}
}
hoverEvent?.let {
when (it.action) {
HoverEvent.HoverEventActions.SHOW_TEXT -> text.accessibleText = it.value.toString() // ToDo
else -> {
NotImplementedError("Unknown action ${it.action}").report()
return@let
}
}
}
return nodes
}

View File

@ -1,6 +1,6 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
* Copyright (C) 2021 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
@ -10,15 +10,20 @@
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.text
import de.bixilon.minosoft.data.text.events.ClickEvent
import de.bixilon.minosoft.data.text.events.HoverEvent
import de.bixilon.minosoft.util.KUtil
import de.bixilon.minosoft.util.enum.ValuesEnum
class MultiChatComponent(
message: String = "",
color: RGBColor? = null,
formatting: MutableSet<ChatFormattingCode> = mutableSetOf(),
var clickEvent: ClickEvent? = null,
var hoverEvent: HoverEvent? = null,
) : TextComponent(message, color, formatting)
enum class URLProtocols(val prefix: String, val restricted: Boolean = false) {
HTTP("http://"),
HTTPS("https://"),
FILE("file:", true),
;
companion object : ValuesEnum<URLProtocols> {
override val VALUES: Array<URLProtocols> = values()
override val NAME_MAP: Map<String, URLProtocols> = KUtil.getEnumValues(VALUES)
}
}

View File

@ -13,15 +13,23 @@
package de.bixilon.minosoft.data.text.events
import de.bixilon.minosoft.util.KUtil
import de.bixilon.minosoft.util.Util
import de.bixilon.minosoft.util.enum.ValuesEnum
class ClickEvent {
val action: ClickEventActions
val value: Any
constructor(json: Map<String, Any>) {
constructor(json: Map<String, Any>, restrictedMode: Boolean = false) {
action = ClickEventActions[json["action"].toString().lowercase()]
this.value = json["value"]!!
if (!restrictedMode) {
return
}
if (action == ClickEventActions.OPEN_URL) {
Util.checkURL(value.toString())
}
}
constructor(action: ClickEventActions, value: Any) {

View File

@ -13,19 +13,29 @@
package de.bixilon.minosoft.gui.eros.dialog
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.gui.eros.controller.JavaFXWindowController
import de.bixilon.minosoft.gui.eros.crash.ErosCrashReport.Companion.crash
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.ctext
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.text
import de.bixilon.minosoft.terminal.RunConfiguration
import de.bixilon.minosoft.util.KUtil.asResourceLocation
import de.bixilon.minosoft.util.KUtil.realName
import de.bixilon.minosoft.util.KUtil.toStackTrace
import javafx.application.Platform
import javafx.fxml.FXML
import javafx.scene.control.Button
import javafx.scene.control.TextArea
import javafx.scene.text.TextFlow
class ErosErrorReport : JavaFXWindowController() {
@FXML private lateinit var headerFX: TextFlow
@FXML private lateinit var descriptionFX: TextFlow
@FXML private lateinit var detailsFX: TextArea
@FXML private lateinit var ignoreFX: Button
@FXML private lateinit var fatalCrashFX: Button
var exception: Throwable? = null
set(value) {
@ -45,9 +55,23 @@ class ErosErrorReport : JavaFXWindowController() {
exception?.crash()
}
override fun init() {
super.init()
headerFX.text = HEADER
descriptionFX.text = DESCRIPTION
ignoreFX.ctext = IGNORE
fatalCrashFX.ctext = FATAL_CRASH
}
companion object {
private val LAYOUT = "minosoft:eros/dialog/error.fxml".asResourceLocation()
private val TITLE = { exception: Throwable? -> Minosoft.LANGUAGE_MANAGER.translate("minosoft:error.title".asResourceLocation(), null, exception?.let { it::class.java.realName }) }
private val HEADER = "minosoft:error.header".asResourceLocation()
private val DESCRIPTION = "minosoft:error.description".asResourceLocation()
private val IGNORE = "minosoft:error.ignore".asResourceLocation()
private val FATAL_CRASH = "minosoft:error.fatal_crash".asResourceLocation()
fun Throwable?.report() {
if (RunConfiguration.DISABLE_EROS) {
@ -55,7 +79,7 @@ class ErosErrorReport : JavaFXWindowController() {
}
Platform.runLater {
val controller = JavaFXUtil.openModal<ErosErrorReport>("", LAYOUT)
val controller = JavaFXUtil.openModal<ErosErrorReport>(TITLE(this), LAYOUT)
controller.exception = this
controller.stage.show()
}

View File

@ -22,6 +22,7 @@ import de.bixilon.minosoft.gui.eros.controller.JavaFXWindowController
import de.bixilon.minosoft.gui.eros.modding.invoker.JavaFXEventInvoker
import de.bixilon.minosoft.gui.eros.util.JavaFXAccountUtil.avatar
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.clickable
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.ctext
import de.bixilon.minosoft.modding.event.events.account.AccountSelectEvent
import de.bixilon.minosoft.modding.event.master.GlobalEventMaster
@ -90,6 +91,10 @@ class MainErosController : JavaFXWindowController() {
ErosMainActivities.ABOUT to aboutIconFX,
)
for (icon in iconMap) {
icon.value.clickable()
}
highlightIcon(playIconFX)
playIconFX.setOnMouseClicked {
@ -104,14 +109,19 @@ class MainErosController : JavaFXWindowController() {
aboutIconFX.setOnMouseClicked {
activity = ErosMainActivities.ABOUT
}
exitIconFX.setOnMouseClicked {
exitIconFX.apply {
clickable()
setOnMouseClicked {
ShutdownManager.shutdown(reason = ShutdownReasons.REQUESTED_BY_USER)
}
}
GlobalEventMaster.registerEvent(JavaFXEventInvoker.of<AccountSelectEvent> {
accountImageFX.image = it.account?.avatar
accountNameFX.ctext = it.account?.username ?: NO_ACCOUNT_SELECTED
})
accountImageFX.clickable()
accountNameFX.clickable()
activity = ErosMainActivities.PlAY
}

View File

@ -19,10 +19,11 @@ import de.bixilon.minosoft.gui.eros.controller.EmbeddedJavaFXController
import de.bixilon.minosoft.gui.eros.controller.JavaFXController
import de.bixilon.minosoft.gui.eros.controller.JavaFXWindowController
import de.bixilon.minosoft.util.KUtil.setValue
import de.bixilon.minosoft.util.KUtil.unsafeCast
import javafx.application.HostServices
import javafx.css.StyleableProperty
import javafx.fxml.FXMLLoader
import javafx.scene.Parent
import javafx.scene.Scene
import javafx.scene.*
import javafx.scene.control.Labeled
import javafx.scene.control.TextField
import javafx.scene.image.Image
@ -92,4 +93,15 @@ object JavaFXUtil {
set(value) {
this.text = Minosoft.LANGUAGE_MANAGER.translate(value).message
}
fun Text.hyperlink(link: String) {
this.setOnMouseClicked { HOST_SERVICES.showDocument(link) }
this.accessibleRole = AccessibleRole.HYPERLINK
this.styleClass.setAll("hyperlink")
this.clickable()
}
fun Node.clickable() {
this.cursorProperty().unsafeCast<StyleableProperty<Cursor>>().applyStyle(null, Cursor.HAND)
}
}

View File

@ -255,7 +255,7 @@ open class InByteBuffer {
}
open fun readChatComponent(): ChatComponent {
return ChatComponent.of(readString())
return ChatComponent.of(readString(), restrictedMode = true)
}
fun readChatComponentArray(length: Int = readVarInt()): Array<ChatComponent> {

View File

@ -17,12 +17,10 @@
<RowConstraints maxHeight="Infinity" vgrow="ALWAYS"/>
<RowConstraints maxHeight="Infinity" vgrow="NEVER"/>
</rowConstraints>
<Text strokeWidth="0.0" text="An error occurred">
<font>
<Font name="System Bold" size="19.0"/>
</font>
</Text>
<TextFlow GridPane.rowIndex="1">
<TextFlow fx:id="headerFX" style="-fx-font-size: 30; -fx-font-weight: bold;">
<Text text="An error occurred"/>
</TextFlow>
<TextFlow fx:id="descriptionFX" GridPane.rowIndex="1">
<Label text="An error in minosoft occurred. You can continue like before, but the behavior might not be the expected one. If this error persists, feel free to open an issue here: " wrapText="true"/>
<Hyperlink onAction="#openURL" text="https://gitlab.bixilon.de/bixilon/minosoft/-/issues/" wrapText="true"/>
<GridPane.margin>
@ -51,8 +49,8 @@
<padding>
<Insets top="5.0"/>
</padding>
<Button maxWidth="Infinity" onAction="#ignore" text="Ignore" GridPane.columnIndex="2"/>
<Button maxWidth="Infinity" onAction="#fatalCrash" text="Fatal crash"/>
<Button fx:id="ignoreFX" maxWidth="Infinity" onAction="#ignore" text="Ignore" GridPane.columnIndex="2"/>
<Button fx:id="fatalCrashFX" maxWidth="Infinity" onAction="#fatalCrash" text="Fatal crash"/>
</GridPane>
</GridPane>
<padding>

View File

@ -98,3 +98,9 @@ minosoft:connection.kick.close_button=Close
minosoft:connection.login_kick.title=Kicked from server
minosoft:connection.login_kick.header=You got kicked
minosoft:connection.login_kick.description=You got kicked while logging in from %1$s (connected with: %2$s)
minosoft:error.title=%1$s - Minosoft
minosoft:error.header=An error occurred!
minosoft:error.description=An error in minosoft occurred. You can continue like before, but the behavior might not be the expected one. If this error persists, feel free to open an issue here: https://gitlab.bixilon.de/bixilon/minosoft/-/issues/
minosoft:error.ignore=Ignore
minosoft:error.fatal_crash=Fatal crash