Skip to content

Commit

Permalink
chore: make our usage of Kotlin UI DSL compatible with 2022.1+
Browse files Browse the repository at this point in the history
  • Loading branch information
jansorg committed Feb 6, 2024
1 parent bbd8215 commit 4972c3d
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
package appland.remote

import appland.AppMapBundle
import com.intellij.ui.TextFieldWithHistory
import com.intellij.ui.TextAccessor
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
import javax.swing.JComponent
import javax.swing.JPanel

@Suppress("UnstableApiUsage")
class StartRemoteRecordingForm(recentUrls: List<String>) {
lateinit var urlComboBox: TextFieldWithHistory
private lateinit var urlAccessor: TextAccessor
lateinit var urlComboBox: JComponent
private set

val url: String
get() {
return urlComboBox.text
return urlAccessor.text
}

val mainPanel: JPanel = panel {
row(label = AppMapBundle.get("appMapRemoteRecording.urlLabel")) {
urlComboBox = textFieldWithHistory(recentUrls)
.horizontalAlign(HorizontalAlign.FILL)
.focused()
.component
val (cell, accessor) = textFieldWithHistory(recentUrls)
urlComboBox = cell.component
urlAccessor = accessor

cell.horizontalAlign(HorizontalAlign.FILL).focused().component
}
row {
comment(AppMapBundle.get("appMapRemoteRecording.message"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
package appland.remote

import appland.AppMapBundle
import com.intellij.ui.TextFieldWithHistory
import com.intellij.ui.TextAccessor
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
import javax.swing.JComponent

@Suppress("UnstableApiUsage")
class StopRemoteRecordingForm(activeRecordingUrl: String?, recentUrls: List<String>) {
lateinit var urlComboBox: TextFieldWithHistory
private lateinit var urlAccessor: TextAccessor
lateinit var urlComboBox: JComponent
private set
lateinit var appMapNameInput: JBTextField
private set

val mainPanel = panel {
row(AppMapBundle.get("appMapRemoteRecording.urlLabel")) {
urlComboBox = textFieldWithHistory(recentUrls).horizontalAlign(HorizontalAlign.FILL).component
val (cell, accessor) = textFieldWithHistory(recentUrls)
urlComboBox = cell.component
urlAccessor = accessor

cell.horizontalAlign(HorizontalAlign.FILL).component

if (!activeRecordingUrl.isNullOrEmpty()) {
urlComboBox.text = activeRecordingUrl
urlAccessor.text = activeRecordingUrl
}
}

Expand All @@ -28,7 +35,7 @@ class StopRemoteRecordingForm(activeRecordingUrl: String?, recentUrls: List<Stri

val url: String
get() {
return urlComboBox.text
return urlAccessor.text
}

val name: String
Expand Down
48 changes: 43 additions & 5 deletions plugin-core/src/main/kotlin/appland/remote/components.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,58 @@
package appland.remote

import com.intellij.ui.TextAccessor
import com.intellij.ui.TextFieldWithHistory
import com.intellij.ui.dsl.builder.COLUMNS_LARGE
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.columns
import com.intellij.util.ui.SwingHelper
import javax.swing.JComponent
import javax.swing.text.JTextComponent

@Suppress("UnstableApiUsage")
internal fun Row.textFieldWithHistory(history: List<String>): Cell<TextFieldWithHistory> {
internal fun Row.textFieldWithHistory(history: List<String>): Pair<Cell<JComponent>, TextAccessor> {
val component = TextFieldWithHistory()
component.setHistorySize(-1)
component.setMinimumAndPreferredWidth(0)
SwingHelper.addHistoryOnExpansion(component) { history }

val result = cell(component)
result.columns(COLUMNS_LARGE)
return result
return try {
cell(component).columns(COLUMNS_LARGE) to TextComponentAccessor(component.textEditor)
} catch (e: NoSuchMethodException) {
addCellReflective(component)
} catch (e: NoSuchMethodError) {
addCellReflective(component)
}
}

/**
* 2021.3 has `fun <T : JComponent> cell(component: T, viewComponent: JComponent = component): Cell<T>`.
* 2022.1 and later have `fun <T : JComponent> cell(component: T): Cell<T>`, i.e., with only one parameter.
* Compiling against 2021.3 and executing with 2022.1 will throw a `NoSuchMethodError` at runtime,
* in this case we're trying to perform a fallback with reflection here.
*
* If the reflective call didn't work, there's still a fallback to a plain text field without history.
*/
@Suppress("UNCHECKED_CAST")
internal fun Row.addCellReflective(component: TextFieldWithHistory): Pair<Cell<JComponent>, TextComponentAccessor> {
try {
val addCellMethod = this.javaClass.getDeclaredMethod("cell", JComponent::class.java)
val cell = addCellMethod.invoke(this, component) as Cell<TextFieldWithHistory>
cell.columns(COLUMNS_LARGE)
return cell to TextComponentAccessor(component.textEditor)
} catch (e: NoSuchMethodException) {
// fallback to editor without history
val fallbackCell = textField()
return fallbackCell to TextComponentAccessor(fallbackCell.component)
}
}

class TextComponentAccessor(val component: JTextComponent) : TextAccessor {
override fun setText(text: String?) {
component.text = text ?: ""
}

override fun getText(): String {
return component.text
}
}
33 changes: 33 additions & 0 deletions plugin-core/src/test/kotlin/appland/remote/ComponentsKtTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package appland.remote

import appland.AppMapBaseTest
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.ui.TextFieldWithHistory
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.panel
import org.junit.Assume
import org.junit.Test
import java.util.concurrent.atomic.AtomicReference
import javax.swing.JComponent

class ComponentsKtTest : AppMapBaseTest() {
@Test
fun textFieldWithHistoryReflective() {
Assume.assumeTrue(ApplicationInfo.getInstance().build.baselineVersion >= 221)

val component = TextFieldWithHistory()
val componentAccessorPair = AtomicReference<Pair<Cell<JComponent>, TextComponentAccessor>>()

panel {
row {
componentAccessorPair.set(addCellReflective(component))
}
}

val (cell, textAccessor) = componentAccessorPair.get()
assertEquals("Component of the cell must match the input", component, cell.component)

textAccessor.text = "updated text"
assertEquals("Text accessor must modify input component", component.text, "updated text")
}
}

0 comments on commit 4972c3d

Please sign in to comment.