From 407019dadb71c00adb99b627247600d71110066b Mon Sep 17 00:00:00 2001 From: Bixilon Date: Fri, 30 Jul 2021 14:59:44 +0200 Subject: [PATCH] improve eros error dialog, chat: url parsing --- .../minosoft/data/text/BaseComponent.kt | 35 ++++++++++++++----- .../minosoft/data/text/ChatComponent.kt | 8 ++--- .../minosoft/data/text/TextComponent.kt | 26 ++++++++++++++ ...{MultiChatComponent.kt => URLProtocols.kt} | 25 +++++++------ .../minosoft/data/text/events/ClickEvent.kt | 10 +++++- .../gui/eros/dialog/ErosErrorReport.kt | 26 +++++++++++++- .../gui/eros/main/MainErosController.kt | 14 ++++++-- .../minosoft/gui/eros/util/JavaFXUtil.kt | 16 +++++++-- .../protocol/protocol/InByteBuffer.kt | 2 +- .../assets/minosoft/eros/dialog/error.fxml | 14 ++++---- .../assets/minosoft/language/en_us.lang | 6 ++++ 11 files changed, 145 insertions(+), 37 deletions(-) rename src/main/java/de/bixilon/minosoft/data/text/{MultiChatComponent.kt => URLProtocols.kt} (61%) diff --git a/src/main/java/de/bixilon/minosoft/data/text/BaseComponent.kt b/src/main/java/de/bixilon/minosoft/data/text/BaseComponent.kt index f659174fb..7c33b0b4d 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/BaseComponent.kt +++ b/src/main/java/de/bixilon/minosoft/data/text/BaseComponent.kt @@ -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 = 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()) - currentColor = null - currentText.clear() + 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() } 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) { + constructor(translator: Translator? = null, parent: TextComponent? = null, json: Map, restrictedMode: Boolean = false) { val currentParent: TextComponent? var currentText = "" json["text"]?.nullCast()?.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, diff --git a/src/main/java/de/bixilon/minosoft/data/text/ChatComponent.kt b/src/main/java/de/bixilon/minosoft/data/text/ChatComponent.kt index f1639af65..5bc5a99b2 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/ChatComponent.kt +++ b/src/main/java/de/bixilon/minosoft/data/text/ChatComponent.kt @@ -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) { } } diff --git a/src/main/java/de/bixilon/minosoft/data/text/TextComponent.kt b/src/main/java/de/bixilon/minosoft/data/text/TextComponent.kt index 17a828e62..be0c9cfe6 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/TextComponent.kt +++ b/src/main/java/de/bixilon/minosoft/data/text/TextComponent.kt @@ -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 = 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 } diff --git a/src/main/java/de/bixilon/minosoft/data/text/MultiChatComponent.kt b/src/main/java/de/bixilon/minosoft/data/text/URLProtocols.kt similarity index 61% rename from src/main/java/de/bixilon/minosoft/data/text/MultiChatComponent.kt rename to src/main/java/de/bixilon/minosoft/data/text/URLProtocols.kt index e868f333c..c3461ac33 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/MultiChatComponent.kt +++ b/src/main/java/de/bixilon/minosoft/data/text/URLProtocols.kt @@ -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 = 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 { + override val VALUES: Array = values() + override val NAME_MAP: Map = KUtil.getEnumValues(VALUES) + } +} diff --git a/src/main/java/de/bixilon/minosoft/data/text/events/ClickEvent.kt b/src/main/java/de/bixilon/minosoft/data/text/events/ClickEvent.kt index 188317f14..d57d4e9d7 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/events/ClickEvent.kt +++ b/src/main/java/de/bixilon/minosoft/data/text/events/ClickEvent.kt @@ -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) { + constructor(json: Map, 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) { diff --git a/src/main/java/de/bixilon/minosoft/gui/eros/dialog/ErosErrorReport.kt b/src/main/java/de/bixilon/minosoft/gui/eros/dialog/ErosErrorReport.kt index 3b7f99438..aa45fa599 100644 --- a/src/main/java/de/bixilon/minosoft/gui/eros/dialog/ErosErrorReport.kt +++ b/src/main/java/de/bixilon/minosoft/gui/eros/dialog/ErosErrorReport.kt @@ -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("", LAYOUT) + val controller = JavaFXUtil.openModal(TITLE(this), LAYOUT) controller.exception = this controller.stage.show() } diff --git a/src/main/java/de/bixilon/minosoft/gui/eros/main/MainErosController.kt b/src/main/java/de/bixilon/minosoft/gui/eros/main/MainErosController.kt index 8ca8186b7..72435b73e 100644 --- a/src/main/java/de/bixilon/minosoft/gui/eros/main/MainErosController.kt +++ b/src/main/java/de/bixilon/minosoft/gui/eros/main/MainErosController.kt @@ -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 { - ShutdownManager.shutdown(reason = ShutdownReasons.REQUESTED_BY_USER) + exitIconFX.apply { + clickable() + setOnMouseClicked { + ShutdownManager.shutdown(reason = ShutdownReasons.REQUESTED_BY_USER) + } } GlobalEventMaster.registerEvent(JavaFXEventInvoker.of { accountImageFX.image = it.account?.avatar accountNameFX.ctext = it.account?.username ?: NO_ACCOUNT_SELECTED }) + accountImageFX.clickable() + accountNameFX.clickable() activity = ErosMainActivities.PlAY } diff --git a/src/main/java/de/bixilon/minosoft/gui/eros/util/JavaFXUtil.kt b/src/main/java/de/bixilon/minosoft/gui/eros/util/JavaFXUtil.kt index a78c1ad83..f0bf67416 100644 --- a/src/main/java/de/bixilon/minosoft/gui/eros/util/JavaFXUtil.kt +++ b/src/main/java/de/bixilon/minosoft/gui/eros/util/JavaFXUtil.kt @@ -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>().applyStyle(null, Cursor.HAND) + } } diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/InByteBuffer.kt b/src/main/java/de/bixilon/minosoft/protocol/protocol/InByteBuffer.kt index f9a1b6eba..51c95ccb7 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/InByteBuffer.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/InByteBuffer.kt @@ -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 { diff --git a/src/main/resources/assets/minosoft/eros/dialog/error.fxml b/src/main/resources/assets/minosoft/eros/dialog/error.fxml index bb5fb3fce..49e1ea0a6 100644 --- a/src/main/resources/assets/minosoft/eros/dialog/error.fxml +++ b/src/main/resources/assets/minosoft/eros/dialog/error.fxml @@ -17,12 +17,10 @@ - - - - - - + + + +