Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/Elementa.api
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,8 @@ public final class gg/essential/elementa/components/inspector/Inspector : gg/ess
}

public final class gg/essential/elementa/components/inspector/Inspector$Companion {
public final fun registerComponentFactory (Ljava/lang/Class;Ljava/lang/String;)V
public static synthetic fun registerComponentFactory$default (Lgg/essential/elementa/components/inspector/Inspector$Companion;Ljava/lang/Class;Ljava/lang/String;ILjava/lang/Object;)V
}

public final class gg/essential/elementa/components/inspector/InspectorNode : gg/essential/elementa/components/TreeNode {
Expand Down
20 changes: 20 additions & 0 deletions src/main/kotlin/gg/essential/elementa/UIComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import gg.essential.elementa.components.UIBlock
import gg.essential.elementa.components.UIContainer
import gg.essential.elementa.components.UpdateFunc
import gg.essential.elementa.components.Window
import gg.essential.elementa.components.inspector.Inspector
import gg.essential.elementa.constraints.*
import gg.essential.elementa.constraints.animation.*
import gg.essential.elementa.dsl.animate
Expand Down Expand Up @@ -1310,6 +1311,25 @@ abstract class UIComponent : Observable(), ReferenceHolder {
}
//endregion

//region Source code location
internal val source: Array<StackTraceElement>? = if (elementaDev) Throwable().stackTrace else null

internal val filteredSource: List<StackTraceElement>?
get() = source?.filterNot { it.lineNumber == 1 }

internal val primarySource: StackTraceElement?
get() {
val className = javaClass.name
return (source ?: return null)
.asSequence()
.dropWhile { it.methodName == "<init>" && it.className != className } // super class constructors
.dropWhile { it.methodName == "<init>" && it.className == className } // constructors
.filterNot { it.lineNumber == 1 } // ignore synthetic methods
.dropWhile { frame -> Inspector.factoryMethods.any { (c, m) -> c == frame.className && (m == null || m == frame.methodName) } }
.firstOrNull()
}
//endregion

/**
* Field animation API
*/
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/gg/essential/elementa/components/UIImage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gg.essential.elementa.components

import gg.essential.elementa.UIComponent
import gg.essential.elementa.components.image.*
import gg.essential.elementa.components.inspector.Inspector
import gg.essential.elementa.utils.ResourceCache
import gg.essential.elementa.utils.drawTexture
import gg.essential.universal.UGraphics
Expand Down Expand Up @@ -127,6 +128,10 @@ open class UIImage @JvmOverloads constructor(

val defaultResourceCache = ResourceCache(50)

init {
Inspector.registerComponentFactory(Companion::class.java)
}

@JvmStatic
fun ofFile(file: File): UIImage {
return UIImage(CompletableFuture.supplyAsync { ImageIO.read(file) })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ import gg.essential.elementa.effects.ScissorEffect
import gg.essential.elementa.utils.ObservableAddEvent
import gg.essential.elementa.utils.ObservableClearEvent
import gg.essential.elementa.utils.ObservableRemoveEvent
import gg.essential.elementa.utils.devPropSet
import gg.essential.elementa.utils.elementaDebug
import gg.essential.universal.UGraphics
import gg.essential.universal.UMatrixStack
import org.lwjgl.opengl.GL11
import java.awt.Color
import java.io.FileNotFoundException
import java.net.ConnectException
import java.net.URL
import java.text.NumberFormat

class Inspector @JvmOverloads constructor(
Expand Down Expand Up @@ -201,6 +205,59 @@ class Inspector @JvmOverloads constructor(
infoBlockScroller childOf container
}
selectedNode = node
if (node != null) {
val source = node.targetComponent.primarySource
if (source != null) {
node.selectedSourceIndex = node.targetComponent.filteredSource!!.indexOf(source)
openSourceFile(source)
}
}
}

init {
// Workaround for ScrollComponent.actualContent breaking scroll event propagation
var recursive = false
onMouseScroll { event ->
if (recursive) return@onMouseScroll
recursive = true
treeBlock.mouseScroll(event.delta)
recursive = false
}
}

internal fun scrollSource(node: InspectorNode, up: Boolean) {
val elements = node.targetComponent.filteredSource ?: return
var index = node.selectedSourceIndex
index = (index + if (up) 1 else -1).coerceIn(elements.indices)
node.selectedSourceIndex = index
openSourceFile(elements[index])
}

private fun openSourceFile(frame: StackTraceElement) {
val folder = frame.className.substringBeforeLast(".").replace(".", "/")
val file = folder + "/" + frame.fileName
val line = frame.lineNumber
try {
// For docs see https://www.develar.org/idea-rest-api/#api-Platform-file
// For impl see https://github.com/JetBrains/intellij-community/blob/d9b508478de6d3b5d2e765738d561ead77c97824/plugins/remote-control/src/org/jetbrains/ide/OpenFileHttpService.kt
val url = URL("$INTELLIJ_REST_API/api/file?file=$file&line=$line&focused=false")
val conn = url.openConnection()
// IntelliJ uses Origin/Referrer to determine whether to trust a given request (a JavaScript snippet in your
// browser could try to request this page too after all!), so we use localhost which is trusted by default.
// See https://github.com/JetBrains/intellij-community/blob/d9b508478de6d3b5d2e765738d561ead77c97824/platform/built-in-server/src/org/jetbrains/ide/RestService.kt#L272
// We can't use Origin because Java considers that a restricted header and will just ignore it.
conn.setRequestProperty("Referer", "http://localhost")
conn.connect()
conn.inputStream.skip(Long.MAX_VALUE)
} catch (ignored: ConnectException) {
} catch (e: FileNotFoundException) { // HTTP 404
if (!hintedIntelliJSupport) {
hintedIntelliJSupport = true
println("IntelliJ detected! Install JetBrain's `IDE Remote Control` plugin to automatically jump to the source of the selected component.")
}
} catch (e: Exception) {
e.printStackTrace()
}
}

private fun getClickSelectTarget(mouseX: Float, mouseY: Float): UIComponent? {
Expand Down Expand Up @@ -283,5 +340,22 @@ class Inspector @JvmOverloads constructor(

companion object {
internal val percentFormat: NumberFormat = NumberFormat.getPercentInstance()

private val INTELLIJ_REST_API = System.getProperty("elementa.intellij_rest_api", "http://localhost:63342")
private var hintedIntelliJSupport = false

internal val factoryMethods: MutableList<Pair<String, String?>> = mutableListOf()

fun registerComponentFactory(cls: Class<*>?, method: String? = null) {
if (!devPropSet) return
factoryMethods.add(Pair(cls?.name ?: callerClassName(), method))
}

private fun callerClassName(): String =
Throwable().stackTrace.asSequence()
.filterNot { it.methodName.endsWith("\$default") } // synthetic Kotlin defaults method
.drop(2) // this method + caller of this method
.first()
.className
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,35 @@ import gg.essential.elementa.dsl.toConstraint
import gg.essential.elementa.dsl.childOf
import gg.essential.elementa.dsl.constrain
import gg.essential.elementa.dsl.pixels
import gg.essential.universal.UKeyboard
import gg.essential.universal.UMatrixStack
import java.awt.Color

class InspectorNode(private val inspector: Inspector, val targetComponent: UIComponent) : TreeNode() {
private val componentClassName = targetComponent.javaClass.simpleName.ifEmpty { "UnknownType" }
private val componentDisplayName = targetComponent.componentName.let { if (it == componentClassName) it else "$componentClassName: $it" }
private var wasHidden = false

private var selectedSource = targetComponent.primarySource
internal var selectedSourceIndex: Int = targetComponent.filteredSource?.indexOf(selectedSource) ?: 0
set(value) {
field = value
selectedSource = targetComponent.filteredSource?.get(value)
}

private val component: UIComponent = object : UIBlock(Color(0, 0, 0, 0)) {
private val text = UIText(componentDisplayName).constrain {
width = TextAspectConstraint()
} childOf this

override fun animationFrame() {
super.animationFrame()
override fun draw(matrixStack: UMatrixStack) {
super.draw(matrixStack)

val isCurrentlyHidden = targetComponent.parent != targetComponent && !targetComponent.parent.children.contains(
targetComponent
)
if (isCurrentlyHidden && !wasHidden) {
wasHidden = true
text.setText("§r$componentDisplayName §7§o(Hidden)")
} else if (!isCurrentlyHidden && wasHidden) {
wasHidden = false
text.setText(componentDisplayName)
}
val file = selectedSource?.let { " §7${it.fileName ?: it.className.substringAfterLast(".")}:${it.lineNumber}" } ?: ""
val hidden = if (isCurrentlyHidden) " §7§o(Hidden)" else ""
text.setText("§r$componentDisplayName$file$hidden")
}
}.constrain {
x = SiblingConstraint()
Expand All @@ -45,6 +49,10 @@ class InspectorNode(private val inspector: Inspector, val targetComponent: UICom
}.onMouseClick { event ->
event.stopImmediatePropagation()
toggleSelection()
}.onMouseScroll { event ->
if (!UKeyboard.isShiftKeyDown()) return@onMouseScroll
event.stopImmediatePropagation()
inspector.scrollSource(this@InspectorNode, event.delta > 0)
}

internal fun toggleSelection() {
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/gg/essential/elementa/utils/ResourceCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gg.essential.elementa.utils
import gg.essential.elementa.components.UIImage
import gg.essential.elementa.components.image.CacheableImage
import gg.essential.elementa.components.image.MSDFComponent
import gg.essential.elementa.components.inspector.Inspector
import java.awt.image.BufferedImage
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
Expand Down Expand Up @@ -44,4 +45,10 @@ class ResourceCache(val size: Int = 50) {
cachedImage.supply(it)
}
}

private companion object {
init {
Inspector.registerComponentFactory(ResourceCache::class.java)
}
}
}