mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-30 19:32:39 +08:00
fix(kilo-jetbrains): surface model settings save failures
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
package ai.kilocode.client
|
||||
|
||||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
|
||||
object KiloNotifications {
|
||||
private const val GROUP = "Kilo Code"
|
||||
|
||||
fun error(title: String, content: String? = null) {
|
||||
val project = ProjectManager.getInstance().openProjects.firstOrNull { !it.isDefault }
|
||||
val notification = NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup(GROUP)
|
||||
?.createNotification(title, content ?: "", NotificationType.ERROR)
|
||||
?: Notification(GROUP, title, content ?: "", NotificationType.ERROR)
|
||||
notification.notify(project)
|
||||
}
|
||||
}
|
||||
+8
@@ -224,6 +224,14 @@ class KiloAppService internal constructor(
|
||||
null
|
||||
}
|
||||
|
||||
fun updateConfigAsync(
|
||||
patch: ConfigPatchDto,
|
||||
done: (KiloAppStateDto?) -> Unit,
|
||||
): Job = cs.launch {
|
||||
val state = updateConfig(patch)
|
||||
done(state)
|
||||
}
|
||||
|
||||
private fun setModelState(state: ModelStateDto) {
|
||||
_models.value = state
|
||||
_favorites.value = state.favorite
|
||||
|
||||
+21
-6
@@ -1,5 +1,6 @@
|
||||
package ai.kilocode.client.settings.models
|
||||
|
||||
import ai.kilocode.client.KiloNotifications
|
||||
import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
@@ -24,6 +25,7 @@ import ai.kilocode.rpc.dto.ModelsWorkspaceDto
|
||||
import ai.kilocode.rpc.dto.ProvidersDto
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.ConfigurableWithId
|
||||
@@ -67,6 +69,8 @@ internal class ModelsSettingsUi(
|
||||
private var allItems: List<ModelPicker.Item> = emptyList()
|
||||
private var saving = false
|
||||
private var pending: ModelsDraft? = null
|
||||
private var saveError: String? = null
|
||||
private var disposed = false
|
||||
|
||||
init {
|
||||
setContent(form)
|
||||
@@ -84,6 +88,7 @@ internal class ModelsSettingsUi(
|
||||
fun resetDraft() {
|
||||
checkEdt()
|
||||
draft = baseline
|
||||
saveError = null
|
||||
if (!saving) clearProgress()
|
||||
sync()
|
||||
}
|
||||
@@ -97,11 +102,15 @@ internal class ModelsSettingsUi(
|
||||
if (patch.values.isEmpty() && patch.agents.isEmpty()) return
|
||||
pending = next
|
||||
saving = true
|
||||
saveError = null
|
||||
showProgress(KiloBundle.message("settings.models.save.pending"))
|
||||
sync()
|
||||
cs.launch {
|
||||
val state = app.updateConfig(patch)
|
||||
withContext(edt) {
|
||||
app.updateConfigAsync(patch) { state ->
|
||||
ApplicationManager.getApplication().invokeLater({
|
||||
if (disposed) {
|
||||
if (state == null) KiloNotifications.error(KiloBundle.message("settings.models.save.failed"))
|
||||
return@invokeLater
|
||||
}
|
||||
if (state != null) {
|
||||
appState = state
|
||||
val base = modelsDraft(state.config, agents)
|
||||
@@ -109,17 +118,18 @@ internal class ModelsSettingsUi(
|
||||
draft = next
|
||||
pending = null
|
||||
saving = false
|
||||
saveError = null
|
||||
clearProgress()
|
||||
sync()
|
||||
return@withContext
|
||||
return@invokeLater
|
||||
}
|
||||
baseline = prev
|
||||
draft = next
|
||||
pending = null
|
||||
saving = false
|
||||
clearProgress()
|
||||
saveError = KiloBundle.message("settings.models.save.failed")
|
||||
sync()
|
||||
}
|
||||
}, ModalityState.any())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +174,7 @@ internal class ModelsSettingsUi(
|
||||
@RequiresEdt
|
||||
fun dispose() {
|
||||
checkEdt()
|
||||
disposed = true
|
||||
jobs.forEach { it.cancel() }
|
||||
jobs.clear()
|
||||
cs.cancel()
|
||||
@@ -236,8 +247,11 @@ internal class ModelsSettingsUi(
|
||||
else -> top.hideBanner()
|
||||
}
|
||||
}
|
||||
val err = saveError
|
||||
if (saving || state == ModelsStatus.SAVING) {
|
||||
showProgress(KiloBundle.message("settings.models.save.pending"))
|
||||
} else if (err != null) {
|
||||
showError(err)
|
||||
} else if (state == ModelsStatus.UNAVAILABLE || state == ModelsStatus.LOADING) {
|
||||
showProgress(KiloBundle.message("settings.models.loading"))
|
||||
} else {
|
||||
@@ -346,6 +360,7 @@ internal class ModelsSettingsUi(
|
||||
private fun update(fn: ModelsDraft.() -> ModelsDraft) {
|
||||
checkEdt()
|
||||
draft = draft.fn()
|
||||
saveError = null
|
||||
sync()
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -43,6 +43,12 @@ internal open class SettingsPanel : LayeredOverlayPanel() {
|
||||
overlay.repaint()
|
||||
}
|
||||
|
||||
fun showError(text: String) {
|
||||
progress.showError(text)
|
||||
overlay.revalidate()
|
||||
overlay.repaint()
|
||||
}
|
||||
|
||||
fun clearProgress() {
|
||||
progress.clearProgress()
|
||||
overlay.revalidate()
|
||||
|
||||
+32
-5
@@ -1,7 +1,6 @@
|
||||
package ai.kilocode.client.settings.ui
|
||||
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.codeInsight.hint.HintUtil
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
@@ -11,7 +10,10 @@ import java.awt.RenderingHints
|
||||
import javax.swing.JPanel
|
||||
|
||||
internal class SettingsProgressOverlay : JPanel(BorderLayout()) {
|
||||
private enum class Kind { INFO, ERROR }
|
||||
|
||||
private var label: JBLabel? = null
|
||||
private var kind: Kind? = null
|
||||
|
||||
init {
|
||||
val view = JBLabel()
|
||||
@@ -24,7 +26,19 @@ internal class SettingsProgressOverlay : JPanel(BorderLayout()) {
|
||||
}
|
||||
|
||||
fun showProgress(text: String) {
|
||||
show(text, Kind.INFO)
|
||||
}
|
||||
|
||||
fun showError(text: String) {
|
||||
show(text, Kind.ERROR)
|
||||
}
|
||||
|
||||
private fun show(text: String, next: Kind) {
|
||||
val view = requireNotNull(label)
|
||||
if (kind != next) {
|
||||
kind = next
|
||||
syncColors()
|
||||
}
|
||||
if (view.text != text) view.text = text
|
||||
if (!isVisible) isVisible = true
|
||||
revalidate()
|
||||
@@ -46,19 +60,32 @@ internal class SettingsProgressOverlay : JPanel(BorderLayout()) {
|
||||
}
|
||||
|
||||
private fun syncColors() {
|
||||
val hint = HintUtil.getInformationHint()
|
||||
background = hint.textBackground
|
||||
foreground = hint.textForeground
|
||||
val current = kind ?: Kind.INFO
|
||||
background = when (current) {
|
||||
Kind.INFO -> UiStyle.Colors.infoOverlayBackground()
|
||||
Kind.ERROR -> UiStyle.Colors.errorOverlayBackground()
|
||||
}
|
||||
foreground = when (current) {
|
||||
Kind.INFO -> UiStyle.Colors.infoOverlayForeground()
|
||||
Kind.ERROR -> UiStyle.Colors.errorOverlayForeground()
|
||||
}
|
||||
label?.foreground = foreground
|
||||
}
|
||||
|
||||
private fun borderColor() = when (kind ?: Kind.INFO) {
|
||||
Kind.INFO -> UiStyle.Colors.infoOverlayBorder()
|
||||
Kind.ERROR -> UiStyle.Colors.errorOverlayBorder()
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = background
|
||||
val arc = UiStyle.Arc.component()
|
||||
g2.color = background
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
g2.color = borderColor()
|
||||
g2.drawRoundRect(0, 0, width - 1, height - 1, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
|
||||
@@ -111,6 +111,18 @@ object UiStyle {
|
||||
?: UIUtil.getContextHelpForeground()
|
||||
}
|
||||
|
||||
fun infoOverlayBackground(): Color = JBUI.CurrentTheme.NotificationInfo.backgroundColor()
|
||||
|
||||
fun infoOverlayForeground(): Color = JBUI.CurrentTheme.NotificationInfo.foregroundColor()
|
||||
|
||||
fun infoOverlayBorder(): Color = JBUI.CurrentTheme.NotificationInfo.borderColor()
|
||||
|
||||
fun errorOverlayBackground(): Color = JBUI.CurrentTheme.NotificationError.backgroundColor()
|
||||
|
||||
fun errorOverlayForeground(): Color = JBUI.CurrentTheme.NotificationError.foregroundColor()
|
||||
|
||||
fun errorOverlayBorder(): Color = JBUI.CurrentTheme.NotificationError.borderColor()
|
||||
|
||||
internal fun contrast(base: Color, delta: Int): Color {
|
||||
val step = if (bright(base)) -delta else delta
|
||||
return Color(
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
<applicationService serviceImplementation="ai.kilocode.client.migration.KiloMigrationService"/>
|
||||
<applicationService serviceImplementation="ai.kilocode.client.autocomplete.KiloAutocompleteSettingsService"/>
|
||||
|
||||
<notificationGroup id="Kilo Code"
|
||||
displayType="BALLOON"
|
||||
bundle="messages.KiloBundle"
|
||||
key="notification.group.kilo"/>
|
||||
|
||||
<toolWindow id="Kilo Code"
|
||||
anchor="left"
|
||||
icon="/icons/kilo.svg"
|
||||
|
||||
@@ -5,6 +5,8 @@ session.connection.error.unknown=Unknown error
|
||||
session.connection.retry=Try again
|
||||
session.connection.warning.config=Configuration warnings
|
||||
|
||||
notification.group.kilo=Kilo Code
|
||||
|
||||
session.empty.welcome=Kilo Code is an AI coding assistant. Ask it to build features, fix bugs, or explain your codebase.
|
||||
session.account.balance=Balance: {0}
|
||||
session.account.switcher=Switch account
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
package ai.kilocode.client.settings.models
|
||||
|
||||
import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.session.ui.model.ModelPicker
|
||||
import ai.kilocode.client.testing.FakeAppRpcApi
|
||||
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
|
||||
import ai.kilocode.rpc.dto.ConfigDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.ModelDto
|
||||
import ai.kilocode.rpc.dto.ModelsWorkspaceDto
|
||||
import ai.kilocode.rpc.dto.ProfileDto
|
||||
import ai.kilocode.rpc.dto.ProviderDto
|
||||
import ai.kilocode.rpc.dto.ProvidersDto
|
||||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.notification.Notifications
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.awt.Container
|
||||
import javax.swing.AbstractButton
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JTextField
|
||||
import javax.swing.text.JTextComponent
|
||||
|
||||
class ModelsSettingsUiTest : BasePlatformTestCase() {
|
||||
private lateinit var appScope: CoroutineScope
|
||||
private lateinit var uiScope: CoroutineScope
|
||||
private lateinit var rpc: FakeAppRpcApi
|
||||
private lateinit var workspaceRpc: FakeWorkspaceRpcApi
|
||||
private lateinit var app: KiloAppService
|
||||
private lateinit var workspaces: KiloWorkspaceService
|
||||
private var ui: ModelsSettingsUi? = null
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
appScope = CoroutineScope(SupervisorJob())
|
||||
uiScope = CoroutineScope(SupervisorJob())
|
||||
rpc = FakeAppRpcApi()
|
||||
workspaceRpc = FakeWorkspaceRpcApi()
|
||||
app = KiloAppService(appScope, rpc)
|
||||
workspaces = KiloWorkspaceService(appScope, workspaceRpc)
|
||||
val state = KiloAppStateDto(
|
||||
KiloAppStatusDto.READY,
|
||||
config = ConfigDto(model = "kilo/old"),
|
||||
profile = ProfileDto(email = "alice@test.com"),
|
||||
)
|
||||
rpc.state.value = state
|
||||
app._state.value = state
|
||||
workspaceRpc.models = ModelsWorkspaceDto(providers = providers())
|
||||
edt { ui = ModelsSettingsUi(uiScope, app, workspaces, directory = "/test") }
|
||||
flushUntil { text(requireUi()).contains("Old") }
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
try {
|
||||
val panel = ui
|
||||
if (panel != null) edt { panel.dispose() }
|
||||
ui = null
|
||||
uiScope.cancel()
|
||||
appScope.cancel()
|
||||
} finally {
|
||||
super.tearDown()
|
||||
}
|
||||
}
|
||||
|
||||
fun `test failed apply stays visible while panel open`() {
|
||||
val panel = requireUi()
|
||||
rpc.configUpdateError = RuntimeException("save failed")
|
||||
|
||||
edt {
|
||||
select(panel, "new")
|
||||
panel.applyDraft()
|
||||
}
|
||||
|
||||
flushUntil { text(panel).contains("Failed to save model settings") }
|
||||
edt {
|
||||
assertTrue(text(panel.progress).contains("Failed to save model settings"))
|
||||
assertTrue(panel.modified())
|
||||
}
|
||||
}
|
||||
|
||||
fun `test edit clears save error`() {
|
||||
val panel = requireUi()
|
||||
rpc.configUpdateError = RuntimeException("save failed")
|
||||
edt {
|
||||
select(panel, "new")
|
||||
panel.applyDraft()
|
||||
}
|
||||
flushUntil { text(panel).contains("Failed to save model settings") }
|
||||
|
||||
edt { select(panel, "old") }
|
||||
|
||||
flushUntil { !text(panel).contains("Failed to save model settings") }
|
||||
}
|
||||
|
||||
fun `test failed save after dispose shows notification`() {
|
||||
val notes = mutableListOf<Notification>()
|
||||
ApplicationManager.getApplication().messageBus.connect(testRootDisposable).subscribe(
|
||||
Notifications.TOPIC,
|
||||
object : Notifications {
|
||||
override fun notify(notification: Notification) {
|
||||
notes.add(notification)
|
||||
}
|
||||
},
|
||||
)
|
||||
project.messageBus.connect(testRootDisposable).subscribe(
|
||||
Notifications.TOPIC,
|
||||
object : Notifications {
|
||||
override fun notify(notification: Notification) {
|
||||
notes.add(notification)
|
||||
}
|
||||
},
|
||||
)
|
||||
val panel = requireUi()
|
||||
rpc.configUpdateGate = CompletableDeferred()
|
||||
rpc.configUpdateError = RuntimeException("save failed")
|
||||
|
||||
edt {
|
||||
select(panel, "new")
|
||||
panel.applyDraft()
|
||||
panel.dispose()
|
||||
ui = null
|
||||
}
|
||||
rpc.configUpdateGate?.complete(Unit)
|
||||
|
||||
flushUntil {
|
||||
notes.any { it.groupId == "Kilo Code" && it.type == NotificationType.ERROR }
|
||||
}
|
||||
assertEquals(1, rpc.configUpdateAttempts)
|
||||
assertTrue(notes.any { it.title == "Failed to save model settings" })
|
||||
}
|
||||
|
||||
private fun providers(): ProvidersDto = ProvidersDto(
|
||||
providers = listOf(
|
||||
ProviderDto(
|
||||
id = "kilo",
|
||||
name = "Kilo",
|
||||
models = mapOf(
|
||||
"old" to ModelDto(id = "old", name = "Old"),
|
||||
"new" to ModelDto(id = "new", name = "New"),
|
||||
),
|
||||
),
|
||||
),
|
||||
connected = emptyList(),
|
||||
defaults = emptyMap(),
|
||||
)
|
||||
|
||||
private fun select(panel: ModelsSettingsUi, id: String) {
|
||||
val picker = components(panel).filterIsInstance<ModelPicker>().first()
|
||||
picker.onSelect(ModelPicker.Item(id, id.replaceFirstChar { it.titlecase() }, "kilo", "Kilo"))
|
||||
}
|
||||
|
||||
private fun requireUi(): ModelsSettingsUi = requireNotNull(ui)
|
||||
|
||||
private fun <T> edt(block: () -> T): T {
|
||||
var result: T? = null
|
||||
ApplicationManager.getApplication().invokeAndWait { result = block() }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result as T
|
||||
}
|
||||
|
||||
private fun flushUntil(done: () -> Boolean) = runBlocking {
|
||||
repeat(20) {
|
||||
delay(100)
|
||||
edt { UIUtil.dispatchAllInvocationEvents() }
|
||||
if (done()) return@runBlocking
|
||||
}
|
||||
edt { UIUtil.dispatchAllInvocationEvents() }
|
||||
assertTrue(done())
|
||||
}
|
||||
|
||||
private fun text(root: Container): String {
|
||||
val out = mutableListOf<String>()
|
||||
for (comp in components(root)) {
|
||||
if (!comp.isVisible) continue
|
||||
when (comp) {
|
||||
is AbstractButton -> comp.text?.let { out.add(it) }
|
||||
is JLabel -> comp.text?.let { out.add(it) }
|
||||
is JTextComponent -> comp.text?.let { out.add(it) }
|
||||
is JTextField -> comp.text?.let { out.add(it) }
|
||||
}
|
||||
}
|
||||
return out.joinToString("\n")
|
||||
}
|
||||
|
||||
private fun components(root: Container): List<java.awt.Component> = buildList {
|
||||
fun visit(comp: java.awt.Component) {
|
||||
add(comp)
|
||||
if (comp is Container) comp.components.forEach { visit(it) }
|
||||
}
|
||||
visit(root)
|
||||
}
|
||||
}
|
||||
+57
-6
@@ -1,12 +1,13 @@
|
||||
package ai.kilocode.client.settings.ui
|
||||
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.codeInsight.hint.HintUtil
|
||||
import com.intellij.ui.SeparatorComponent
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import java.awt.Color
|
||||
import java.awt.Container
|
||||
import java.awt.Rectangle
|
||||
import java.awt.image.BufferedImage
|
||||
import javax.swing.AbstractButton
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
@@ -112,14 +113,64 @@ class SettingsRowsTest : BasePlatformTestCase() {
|
||||
|
||||
fun `test settings progress overlay uses information colors`() {
|
||||
val panel = SettingsPanel()
|
||||
val hint = HintUtil.getInformationHint()
|
||||
|
||||
panel.showProgress("Loading")
|
||||
|
||||
val label = components(panel.progress).filterIsInstance<JBLabel>().single { it.text == "Loading" }
|
||||
assertEquals(hint.textBackground, panel.progress.background)
|
||||
assertEquals(hint.textForeground, panel.progress.foreground)
|
||||
assertEquals(hint.textForeground, label.foreground)
|
||||
assertEquals(UiStyle.Colors.infoOverlayBackground(), panel.progress.background)
|
||||
assertEquals(UiStyle.Colors.infoOverlayForeground(), panel.progress.foreground)
|
||||
assertEquals(UiStyle.Colors.infoOverlayForeground(), label.foreground)
|
||||
}
|
||||
|
||||
fun `test settings progress overlay paints styled background`() {
|
||||
val panel = SettingsPanel()
|
||||
|
||||
panel.showProgress("Saving")
|
||||
panel.progress.setSize(panel.progress.preferredSize)
|
||||
panel.progress.doLayout()
|
||||
|
||||
assertEquals(UiStyle.Colors.infoOverlayBackground().rgb, paint(panel.progress, UiStyle.Gap.lg(), panel.progress.height / 2).rgb)
|
||||
assertNotSameColor(UiStyle.Colors.bg(), panel.progress.background)
|
||||
}
|
||||
|
||||
fun `test settings progress overlay can show error`() {
|
||||
val panel = SettingsPanel()
|
||||
|
||||
panel.showError("Failed to save model settings")
|
||||
|
||||
val label = components(panel.progress).filterIsInstance<JBLabel>().single { it.text == "Failed to save model settings" }
|
||||
assertTrue(panel.progress.isVisible)
|
||||
assertEquals(UiStyle.Colors.errorOverlayBackground(), panel.progress.background)
|
||||
assertEquals(UiStyle.Colors.errorOverlayForeground(), panel.progress.foreground)
|
||||
assertEquals(UiStyle.Colors.errorOverlayForeground(), label.foreground)
|
||||
}
|
||||
|
||||
fun `test settings progress overlay switches error back to info`() {
|
||||
val panel = SettingsPanel()
|
||||
|
||||
panel.showError("Failed")
|
||||
val label = components(panel.progress).filterIsInstance<JBLabel>().single { it.text == "Failed" }
|
||||
panel.showProgress("Saving")
|
||||
|
||||
assertSame(label, components(panel.progress).filterIsInstance<JBLabel>().single { it.text == "Saving" })
|
||||
assertEquals(UiStyle.Colors.infoOverlayBackground(), panel.progress.background)
|
||||
assertEquals(UiStyle.Colors.infoOverlayForeground(), panel.progress.foreground)
|
||||
assertEquals(UiStyle.Colors.infoOverlayForeground(), label.foreground)
|
||||
}
|
||||
|
||||
private fun paint(component: JComponent, x: Int, y: Int): Color {
|
||||
val image = BufferedImage(component.width, component.height, BufferedImage.TYPE_INT_ARGB)
|
||||
val g = image.createGraphics()
|
||||
try {
|
||||
component.paint(g)
|
||||
} finally {
|
||||
g.dispose()
|
||||
}
|
||||
return Color(image.getRGB(x, y), true)
|
||||
}
|
||||
|
||||
private fun assertNotSameColor(a: Color, b: Color) {
|
||||
assertFalse("Expected colors to differ: $a", a.rgb == b.rgb)
|
||||
}
|
||||
|
||||
private fun text(component: JComponent): String = components(component)
|
||||
|
||||
+5
@@ -35,6 +35,9 @@ class FakeAppRpcApi : KiloAppRpcApi {
|
||||
val cleared = mutableListOf<String>()
|
||||
val variants = mutableListOf<ModelVariantUpdateDto>()
|
||||
val configPatches = mutableListOf<ConfigPatchDto>()
|
||||
var configUpdateAttempts = 0
|
||||
private set
|
||||
var configUpdateGate: CompletableDeferred<Unit>? = null
|
||||
var configUpdateError: Exception? = null
|
||||
|
||||
var connected = false
|
||||
@@ -120,6 +123,8 @@ class FakeAppRpcApi : KiloAppRpcApi {
|
||||
|
||||
override suspend fun updateConfig(patch: ConfigPatchDto): KiloAppStateDto {
|
||||
assertNotEdt("updateConfig")
|
||||
configUpdateAttempts += 1
|
||||
configUpdateGate?.await()
|
||||
configUpdateError?.let { throw it }
|
||||
configPatches.add(patch)
|
||||
val current = state.value
|
||||
|
||||
Reference in New Issue
Block a user