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() val currentText = StringBuilder()
var currentColor = parent?.color var currentColor = parent?.color
val currentFormatting: MutableSet<ChatFormattingCode> = parent?.formatting?.toMutableSet() ?: mutableSetOf() val currentFormatting: MutableSet<ChatFormattingCode> = parent?.formatting?.toMutableSet() ?: mutableSetOf()
@ -54,15 +54,34 @@ class BaseComponent : ChatComponent {
fun push() { fun push() {
if (currentText.isNotEmpty()) { if (currentText.isEmpty()) {
parts += TextComponent(message = currentText.toString(), color = currentColor, formatting = currentFormatting.toMutableSet()) return
currentColor = null }
currentText.clear() 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() currentFormatting.clear()
currentColor = null
currentText.clear()
} }
while (char != CharacterIterator.DONE) { while (char != CharacterIterator.DONE) {
// ToDo: Parse urls with click event (and respect restrictedMode)
if (char != ProtocolDefinition.TEXT_COMPONENT_SPECIAL_PREFIX_CHAR) { if (char != ProtocolDefinition.TEXT_COMPONENT_SPECIAL_PREFIX_CHAR) {
currentText.append(char) currentText.append(char)
char = iterator.next() char = iterator.next()
@ -94,7 +113,7 @@ class BaseComponent : ChatComponent {
push() 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? val currentParent: TextComponent?
var currentText = "" var currentText = ""
json["text"]?.nullCast<String>()?.let { json["text"]?.nullCast<String>()?.let {
@ -121,10 +140,10 @@ class BaseComponent : ChatComponent {
formatting.addOrRemove(PreChatFormattingCodes.STRIKETHROUGH, json["strikethrough"]?.toBoolean()) formatting.addOrRemove(PreChatFormattingCodes.STRIKETHROUGH, json["strikethrough"]?.toBoolean())
formatting.addOrRemove(PreChatFormattingCodes.OBFUSCATED, json["obfuscated"]?.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 hoverEvent = json["hoverEvent"]?.compoundCast()?.let { hover -> HoverEvent(hover) }
val textComponent = MultiChatComponent( val textComponent = TextComponent(
message = currentText, message = currentText,
color = color, color = color,
formatting = formatting, formatting = formatting,

View File

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

View File

@ -13,6 +13,10 @@
package de.bixilon.minosoft.data.text package de.bixilon.minosoft.data.text
import de.bixilon.minosoft.Minosoft 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.RenderConstants
import de.bixilon.minosoft.gui.rendering.RenderWindow import de.bixilon.minosoft.gui.rendering.RenderWindow
import de.bixilon.minosoft.gui.rendering.font.Font import de.bixilon.minosoft.gui.rendering.font.Font
@ -38,6 +42,8 @@ open class TextComponent(
message: Any? = "", message: Any? = "",
var color: RGBColor? = null, var color: RGBColor? = null,
var formatting: MutableSet<ChatFormattingCode> = mutableSetOf(), var formatting: MutableSet<ChatFormattingCode> = mutableSetOf(),
var clickEvent: ClickEvent? = null,
var hoverEvent: HoverEvent? = null,
) : ChatComponent { ) : ChatComponent {
override var message: String = message?.toString() ?: "null" override var message: String = message?.toString() ?: "null"
@ -160,6 +166,26 @@ open class TextComponent(
} }
} }
nodes.add(text) 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 return nodes
} }

View File

@ -1,6 +1,6 @@
/* /*
* Minosoft * 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. * 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. * This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/ */
package de.bixilon.minosoft.data.text package de.bixilon.minosoft.data.text
import de.bixilon.minosoft.data.text.events.ClickEvent import de.bixilon.minosoft.util.KUtil
import de.bixilon.minosoft.data.text.events.HoverEvent import de.bixilon.minosoft.util.enum.ValuesEnum
class MultiChatComponent( enum class URLProtocols(val prefix: String, val restricted: Boolean = false) {
message: String = "", HTTP("http://"),
color: RGBColor? = null, HTTPS("https://"),
formatting: MutableSet<ChatFormattingCode> = mutableSetOf(), FILE("file:", true),
var clickEvent: ClickEvent? = null, ;
var hoverEvent: HoverEvent? = null,
) : TextComponent(message, color, formatting) 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 package de.bixilon.minosoft.data.text.events
import de.bixilon.minosoft.util.KUtil import de.bixilon.minosoft.util.KUtil
import de.bixilon.minosoft.util.Util
import de.bixilon.minosoft.util.enum.ValuesEnum import de.bixilon.minosoft.util.enum.ValuesEnum
class ClickEvent { class ClickEvent {
val action: ClickEventActions val action: ClickEventActions
val value: Any val value: Any
constructor(json: Map<String, Any>) { constructor(json: Map<String, Any>, restrictedMode: Boolean = false) {
action = ClickEventActions[json["action"].toString().lowercase()] action = ClickEventActions[json["action"].toString().lowercase()]
this.value = json["value"]!! this.value = json["value"]!!
if (!restrictedMode) {
return
}
if (action == ClickEventActions.OPEN_URL) {
Util.checkURL(value.toString())
}
} }
constructor(action: ClickEventActions, value: Any) { constructor(action: ClickEventActions, value: Any) {

View File

@ -13,19 +13,29 @@
package de.bixilon.minosoft.gui.eros.dialog 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.controller.JavaFXWindowController
import de.bixilon.minosoft.gui.eros.crash.ErosCrashReport.Companion.crash 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
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.terminal.RunConfiguration
import de.bixilon.minosoft.util.KUtil.asResourceLocation import de.bixilon.minosoft.util.KUtil.asResourceLocation
import de.bixilon.minosoft.util.KUtil.realName
import de.bixilon.minosoft.util.KUtil.toStackTrace import de.bixilon.minosoft.util.KUtil.toStackTrace
import javafx.application.Platform import javafx.application.Platform
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.control.Button
import javafx.scene.control.TextArea import javafx.scene.control.TextArea
import javafx.scene.text.TextFlow
class ErosErrorReport : JavaFXWindowController() { 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 detailsFX: TextArea
@FXML private lateinit var ignoreFX: Button
@FXML private lateinit var fatalCrashFX: Button
var exception: Throwable? = null var exception: Throwable? = null
set(value) { set(value) {
@ -45,9 +55,23 @@ class ErosErrorReport : JavaFXWindowController() {
exception?.crash() exception?.crash()
} }
override fun init() {
super.init()
headerFX.text = HEADER
descriptionFX.text = DESCRIPTION
ignoreFX.ctext = IGNORE
fatalCrashFX.ctext = FATAL_CRASH
}
companion object { companion object {
private val LAYOUT = "minosoft:eros/dialog/error.fxml".asResourceLocation() 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() { fun Throwable?.report() {
if (RunConfiguration.DISABLE_EROS) { if (RunConfiguration.DISABLE_EROS) {
@ -55,7 +79,7 @@ class ErosErrorReport : JavaFXWindowController() {
} }
Platform.runLater { Platform.runLater {
val controller = JavaFXUtil.openModal<ErosErrorReport>("", LAYOUT) val controller = JavaFXUtil.openModal<ErosErrorReport>(TITLE(this), LAYOUT)
controller.exception = this controller.exception = this
controller.stage.show() 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.modding.invoker.JavaFXEventInvoker
import de.bixilon.minosoft.gui.eros.util.JavaFXAccountUtil.avatar 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
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.clickable
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.ctext import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.ctext
import de.bixilon.minosoft.modding.event.events.account.AccountSelectEvent import de.bixilon.minosoft.modding.event.events.account.AccountSelectEvent
import de.bixilon.minosoft.modding.event.master.GlobalEventMaster import de.bixilon.minosoft.modding.event.master.GlobalEventMaster
@ -90,6 +91,10 @@ class MainErosController : JavaFXWindowController() {
ErosMainActivities.ABOUT to aboutIconFX, ErosMainActivities.ABOUT to aboutIconFX,
) )
for (icon in iconMap) {
icon.value.clickable()
}
highlightIcon(playIconFX) highlightIcon(playIconFX)
playIconFX.setOnMouseClicked { playIconFX.setOnMouseClicked {
@ -104,14 +109,19 @@ class MainErosController : JavaFXWindowController() {
aboutIconFX.setOnMouseClicked { aboutIconFX.setOnMouseClicked {
activity = ErosMainActivities.ABOUT activity = ErosMainActivities.ABOUT
} }
exitIconFX.setOnMouseClicked { exitIconFX.apply {
ShutdownManager.shutdown(reason = ShutdownReasons.REQUESTED_BY_USER) clickable()
setOnMouseClicked {
ShutdownManager.shutdown(reason = ShutdownReasons.REQUESTED_BY_USER)
}
} }
GlobalEventMaster.registerEvent(JavaFXEventInvoker.of<AccountSelectEvent> { GlobalEventMaster.registerEvent(JavaFXEventInvoker.of<AccountSelectEvent> {
accountImageFX.image = it.account?.avatar accountImageFX.image = it.account?.avatar
accountNameFX.ctext = it.account?.username ?: NO_ACCOUNT_SELECTED accountNameFX.ctext = it.account?.username ?: NO_ACCOUNT_SELECTED
}) })
accountImageFX.clickable()
accountNameFX.clickable()
activity = ErosMainActivities.PlAY 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.JavaFXController
import de.bixilon.minosoft.gui.eros.controller.JavaFXWindowController import de.bixilon.minosoft.gui.eros.controller.JavaFXWindowController
import de.bixilon.minosoft.util.KUtil.setValue import de.bixilon.minosoft.util.KUtil.setValue
import de.bixilon.minosoft.util.KUtil.unsafeCast
import javafx.application.HostServices import javafx.application.HostServices
import javafx.css.StyleableProperty
import javafx.fxml.FXMLLoader import javafx.fxml.FXMLLoader
import javafx.scene.Parent import javafx.scene.*
import javafx.scene.Scene
import javafx.scene.control.Labeled import javafx.scene.control.Labeled
import javafx.scene.control.TextField import javafx.scene.control.TextField
import javafx.scene.image.Image import javafx.scene.image.Image
@ -92,4 +93,15 @@ object JavaFXUtil {
set(value) { set(value) {
this.text = Minosoft.LANGUAGE_MANAGER.translate(value).message 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 { open fun readChatComponent(): ChatComponent {
return ChatComponent.of(readString()) return ChatComponent.of(readString(), restrictedMode = true)
} }
fun readChatComponentArray(length: Int = readVarInt()): Array<ChatComponent> { fun readChatComponentArray(length: Int = readVarInt()): Array<ChatComponent> {

View File

@ -17,12 +17,10 @@
<RowConstraints maxHeight="Infinity" vgrow="ALWAYS"/> <RowConstraints maxHeight="Infinity" vgrow="ALWAYS"/>
<RowConstraints maxHeight="Infinity" vgrow="NEVER"/> <RowConstraints maxHeight="Infinity" vgrow="NEVER"/>
</rowConstraints> </rowConstraints>
<Text strokeWidth="0.0" text="An error occurred"> <TextFlow fx:id="headerFX" style="-fx-font-size: 30; -fx-font-weight: bold;">
<font> <Text text="An error occurred"/>
<Font name="System Bold" size="19.0"/> </TextFlow>
</font> <TextFlow fx:id="descriptionFX" GridPane.rowIndex="1">
</Text>
<TextFlow 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"/> <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"/> <Hyperlink onAction="#openURL" text="https://gitlab.bixilon.de/bixilon/minosoft/-/issues/" wrapText="true"/>
<GridPane.margin> <GridPane.margin>
@ -51,8 +49,8 @@
<padding> <padding>
<Insets top="5.0"/> <Insets top="5.0"/>
</padding> </padding>
<Button maxWidth="Infinity" onAction="#ignore" text="Ignore" GridPane.columnIndex="2"/> <Button fx:id="ignoreFX" maxWidth="Infinity" onAction="#ignore" text="Ignore" GridPane.columnIndex="2"/>
<Button maxWidth="Infinity" onAction="#fatalCrash" text="Fatal crash"/> <Button fx:id="fatalCrashFX" maxWidth="Infinity" onAction="#fatalCrash" text="Fatal crash"/>
</GridPane> </GridPane>
</GridPane> </GridPane>
<padding> <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.title=Kicked from server
minosoft:connection.login_kick.header=You got kicked 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: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