Wizard: make error messages in ui clickable

#KT-36162 fixed
This commit is contained in:
Ilya Kirillov
2020-02-11 14:07:11 +03:00
parent 087de68b3a
commit bb455ec8f6
14 changed files with 144 additions and 22 deletions
@@ -47,4 +47,8 @@ abstract class DynamicComponent(private val valuesReadingContext: ValuesReadingC
reader(valuesReadingContext)
open fun onValueUpdated(reference: SettingReference<*, *>?) {}
}
interface FocusableComponent {
fun focusOn() {}
}
@@ -46,4 +46,8 @@ class PathFieldComponent(
}
override fun getUiValue(): Path = Paths.get(uiComponent.text.trim())
override fun focusOn() {
uiComponent.textField.requestFocus()
}
}
@@ -6,11 +6,12 @@
package org.jetbrains.kotlin.tools.projectWizard.wizard.ui.components
import org.jetbrains.kotlin.tools.projectWizard.core.ValuesReadingContext
import org.jetbrains.kotlin.tools.projectWizard.core.entity.Setting
import org.jetbrains.kotlin.tools.projectWizard.core.entity.SettingType
import org.jetbrains.kotlin.tools.projectWizard.core.entity.SettingValidator
import org.jetbrains.kotlin.tools.projectWizard.core.entity.*
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.DynamicComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.FocusableComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.panel
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting.ErrorAwareComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting.SettingComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting.ValidationIndicator
import java.awt.BorderLayout
import javax.swing.JComponent
@@ -20,7 +21,7 @@ abstract class UIComponent<V : Any>(
labelText: String? = null,
private val validator: SettingValidator<V>? = null,
private val onValueUpdate: (V) -> Unit = {}
) : DynamicComponent(valuesReadingContext) {
) : DynamicComponent(valuesReadingContext), ErrorAwareComponent, FocusableComponent {
private val validationIndicator = if (validator != null)
ValidationIndicator(defaultText = labelText, showText = true)
else null
@@ -63,6 +64,14 @@ abstract class UIComponent<V : Any>(
if (validationIndicator == null) return
validationIndicator.validationState = validator.validate(valuesReadingContext, value)
}
override fun focusOn() {
uiComponent.requestFocus()
}
override fun findComponentWithError(error: ValidationResult.ValidationError): FocusableComponent? = takeIf {
getUiValue()?.let { validator?.validate?.invoke(valuesReadingContext, it)?.isSpecificError(error) } == true
}
}
fun <V : Any> UIComponent<V>.valueForSetting(setting: Setting<V, SettingType<V>>): V? =
@@ -4,6 +4,7 @@ import com.intellij.ui.components.JBTabbedPane
import org.jetbrains.kotlin.idea.projectWizard.UiEditorUsageStats
import org.jetbrains.kotlin.tools.projectWizard.core.ValuesReadingContext
import org.jetbrains.kotlin.tools.projectWizard.core.entity.StringValidators
import org.jetbrains.kotlin.tools.projectWizard.core.entity.ValidationResult
import org.jetbrains.kotlin.tools.projectWizard.moduleConfigurators.configuratorSettings
import org.jetbrains.kotlin.tools.projectWizard.moduleConfigurators.moduleType
import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModuleType
@@ -11,6 +12,7 @@ import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module.Companion.ALLOWED_SPECIAL_CHARS_IN_MODULE_NAMES
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.ModuleKind
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.DynamicComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.FocusableComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.components.TextFieldComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.panel
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting.SettingsList
@@ -33,6 +35,13 @@ class ModuleSettingsComponent(
add("Module Settings", moduleConfiguratorSettingsList.component)
}
fun selectSettingWithError(error: ValidationResult.ValidationError) {
val componentWithError = nameField.findComponentWithError(error)
?: moduleConfiguratorSettingsList.findComponentWithError(error)?.also { tabPanel.selectedIndex = 1 }
?: templateComponent.findComponentWithError(error)?.also { tabPanel.selectedIndex = 0 }
componentWithError?.focusOn()
}
private val nameField = TextFieldComponent(
valuesReadingContext,
labelText = "Name",
@@ -2,6 +2,7 @@ package org.jetbrains.kotlin.tools.projectWizard.wizard.ui.secondStep
import org.jetbrains.kotlin.idea.projectWizard.UiEditorUsageStats
import org.jetbrains.kotlin.tools.projectWizard.core.ValuesReadingContext
import org.jetbrains.kotlin.tools.projectWizard.core.entity.ValidationResult
import org.jetbrains.kotlin.tools.projectWizard.settings.DisplayableSettingItem
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Sourceset
@@ -17,7 +18,9 @@ class SecondStepWizardComponent(
uiEditorUsagesStats: UiEditorUsageStats
) : WizardStepComponent(wizard.valuesReadingContext) {
private val moduleEditorSubStep =
ModulesEditorSubStep(wizard.valuesReadingContext, uiEditorUsagesStats, ::onNodeSelected).asSubComponent()
ModulesEditorSubStep(wizard.valuesReadingContext, uiEditorUsagesStats, ::onNodeSelected) {
templatesSubStep.selectSettingWithError(it)
}.asSubComponent()
private val templatesSubStep = ModuleSettingsSubStep(wizard, uiEditorUsagesStats).asSubComponent()
override val component = splitterFor(
@@ -34,12 +37,14 @@ class SecondStepWizardComponent(
class ModulesEditorSubStep(
valuesReadingContext: ValuesReadingContext,
uiEditorUsagesStats: UiEditorUsageStats,
onNodeSelected: (data: DisplayableSettingItem?) -> Unit
onNodeSelected: (data: DisplayableSettingItem?) -> Unit,
selectSettingWithError: (ValidationResult.ValidationError) -> Unit
) : SubStep(valuesReadingContext) {
private val moduleSettingComponent = ModulesEditorComponent(
valuesReadingContext,
uiEditorUsagesStats,
onNodeSelected
onNodeSelected,
selectSettingWithError
).asSubComponent()
override fun buildContent(): JComponent = panel {
@@ -74,6 +79,10 @@ class ModuleSettingsSubStep(
changeComponent()
}
fun selectSettingWithError(error: ValidationResult.ValidationError) {
moduleSettingsComponent.selectSettingWithError(error)
}
private fun changeComponent() {
panel.removeAll()
val component = when (selectedNode) {
@@ -10,6 +10,7 @@ import com.intellij.util.ui.UIUtil
import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.idea.projectWizard.UiEditorUsageStats
import org.jetbrains.kotlin.tools.projectWizard.core.ValuesReadingContext
import org.jetbrains.kotlin.tools.projectWizard.core.entity.ValidationResult
import org.jetbrains.kotlin.tools.projectWizard.moduleConfigurators.moduleType
import org.jetbrains.kotlin.tools.projectWizard.plugins.templates.TemplatesPlugin
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module
@@ -17,6 +18,8 @@ import org.jetbrains.kotlin.tools.projectWizard.templates.Template
import org.jetbrains.kotlin.tools.projectWizard.templates.settings
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.*
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.Component
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting.ErrorAwareComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting.SettingComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting.SettingsList
import java.awt.*
import javax.swing.JComponent
@@ -25,7 +28,7 @@ import javax.swing.JPanel
class TemplatesComponent(
valuesReadingContext: ValuesReadingContext,
uiEditorUsagesStats: UiEditorUsageStats
) : DynamicComponent(valuesReadingContext) {
) : DynamicComponent(valuesReadingContext), ErrorAwareComponent {
private val chooseTemplateComponent: ChooseTemplateComponent =
ChooseTemplateComponent(valuesReadingContext) { template ->
uiEditorUsagesStats.moduleTemplatesSet++
@@ -49,6 +52,9 @@ class TemplatesComponent(
}
}
override fun findComponentWithError(error: ValidationResult.ValidationError): SettingComponent<*, *>? =
templateSettingsComponent.findComponentWithError(error)
private fun switchState(selectedTemplate: Template?) {
panel.removeAll()
when (selectedTemplate) {
@@ -243,7 +249,7 @@ class TemplateDescriptionComponent(
private class TemplateSettingsComponent(
valuesReadingContext: ValuesReadingContext,
removeTemplate: () -> Unit
) : DynamicComponent(valuesReadingContext) {
) : DynamicComponent(valuesReadingContext), ErrorAwareComponent {
private val templateDescriptionComponent = TemplateDescriptionComponent(
needRemoveButton = true,
nonDefaultBackgroundColor = UIUtil.getEditorPaneBackground(),
@@ -256,6 +262,10 @@ private class TemplateSettingsComponent(
component.bordered()
}
override fun findComponentWithError(error: ValidationResult.ValidationError): SettingComponent<*, *>? =
settings.findComponentWithError(error)
fun setTemplate(module: Module, selectedTemplate: Template) {
settings.setSettings(selectedTemplate.settings(module))
templateDescriptionComponent.updateSelectedTemplate(selectedTemplate)
@@ -4,6 +4,7 @@ import com.intellij.ui.JBColor
import org.jetbrains.kotlin.idea.projectWizard.UiEditorUsageStats
import org.jetbrains.kotlin.tools.projectWizard.core.entity.ListSettingType
import org.jetbrains.kotlin.tools.projectWizard.core.ValuesReadingContext
import org.jetbrains.kotlin.tools.projectWizard.core.entity.ValidationResult
import org.jetbrains.kotlin.tools.projectWizard.core.entity.reference
import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.KotlinPlugin
import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ProjectKind
@@ -20,7 +21,8 @@ import javax.swing.JComponent
class ModulesEditorComponent(
valuesReadingContext: ValuesReadingContext,
uiEditorUsagesStats: UiEditorUsageStats,
oneEntrySelected: (data: DisplayableSettingItem?) -> Unit
oneEntrySelected: (data: DisplayableSettingItem?) -> Unit,
selectSettingWithError: (ValidationResult.ValidationError) -> Unit
) : SettingComponent<List<Module>, ListSettingType<Module>>(KotlinPlugin::modules.reference, valuesReadingContext) {
private val tree: ModulesEditorTree =
ModulesEditorTree(
@@ -64,7 +66,11 @@ class ModulesEditorComponent(
}
}
override val validationIndicator = ValidationIndicator(showText = true).apply {
override val validationIndicator = ValidationIndicator(showText = true) { error ->
val module = error.target as? Module ?: return@ValidationIndicator
tree.selectModule(module)
selectSettingWithError(error)
}.apply {
background = tree.background
}
}
@@ -7,12 +7,14 @@ import com.intellij.ui.treeStructure.Tree
import org.jetbrains.kotlin.tools.projectWizard.settings.DisplayableSettingItem
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Sourceset
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.path
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.icon
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import javax.swing.JComponent
import javax.swing.JTree
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreePath
import javax.swing.tree.TreeSelectionModel
class ModulesEditorTree(
@@ -32,6 +34,17 @@ class ModulesEditorTree(
openPaths.forEach(::expandPath)
}
fun selectModule(moduleToSelect: Module) {
for (value in (model.root as DefaultMutableTreeNode).depthFirstEnumeration().iterator()) {
val node = value as? DefaultMutableTreeNode ?: continue
val module = node.userObject as? Module ?: continue
if (module.path == moduleToSelect.path) {
selectionPath = TreePath(node.path)
break
}
}
}
val selectedSettingItem
get() = selectedNode?.userObject?.safeAs<DisplayableSettingItem>()
@@ -4,11 +4,12 @@ import org.jetbrains.kotlin.tools.projectWizard.core.ValuesReadingContext
import org.jetbrains.kotlin.tools.projectWizard.core.entity.*
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.Displayable
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.DynamicComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.FocusableComponent
abstract class SettingComponent<V : Any, T: SettingType<V>>(
val reference: SettingReference<V, T>,
val valuesReadingContext: ValuesReadingContext
) : DynamicComponent(valuesReadingContext), Displayable {
) : DynamicComponent(valuesReadingContext), Displayable, FocusableComponent {
var value: V?
get() = reference.value
set(value) {
@@ -3,18 +3,25 @@ package org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting
import com.intellij.ui.components.panels.VerticalLayout
import org.jetbrains.kotlin.tools.projectWizard.core.ValuesReadingContext
import org.jetbrains.kotlin.tools.projectWizard.core.entity.SettingReference
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.Component
import org.jetbrains.kotlin.tools.projectWizard.core.entity.ValidationResult
import org.jetbrains.kotlin.tools.projectWizard.core.entity.isSpecificError
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.DynamicComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.FocusableComponent
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.PanelWithStatusText
import javax.swing.JComponent
interface ErrorAwareComponent {
fun findComponentWithError(error: ValidationResult.ValidationError): FocusableComponent?
}
class SettingsList(
settings: List<SettingReference<*, *>>,
private val valuesReadingContext: ValuesReadingContext
) : DynamicComponent(valuesReadingContext) {
) : DynamicComponent(valuesReadingContext), ErrorAwareComponent {
private val panel = PanelWithStatusText(VerticalLayout(5), "This module has no settings to configure")
private var settingComponents: List<Component> = emptyList()
var settingComponents: List<SettingComponent<*, *>> = emptyList()
private set
init {
setSettings(settings)
@@ -35,4 +42,11 @@ class SettingsList(
}
settingComponents.forEach { setting -> setting.onInit(); panel.add(setting.component) }
}
override fun findComponentWithError(error: ValidationResult.ValidationError) =
settingComponents.firstOrNull { component ->
val value = component.value ?: return@firstOrNull false
val result = component.setting.validator.validate(valuesReadingContext, value)
result isSpecificError error
}
}
@@ -29,5 +29,9 @@ abstract class UIComponentDelegatingSettingComponent<V : Any, T : SettingType<V>
value?.let(uiComponent::updateUiValue)
}
override fun focusOn() {
uiComponent.focusOn()
}
override val component: JComponent by lazy(LazyThreadSafetyMode.NONE) { uiComponent.component }
}
@@ -2,28 +2,49 @@ package org.jetbrains.kotlin.tools.projectWizard.wizard.ui.setting
import com.intellij.icons.AllIcons
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.EmptyIcon
import org.jetbrains.kotlin.tools.projectWizard.core.entity.ValidationResult
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.asHtml
import org.jetbrains.kotlin.tools.projectWizard.wizard.ui.label
import java.awt.BorderLayout
import java.awt.Cursor
import java.awt.event.MouseEvent
import java.awt.event.MouseListener
import javax.swing.BorderFactory
import javax.swing.Icon
import javax.swing.JPanel
class ValidationIndicator(
defaultText: String? = null,
private val showText: Boolean
private val showText: Boolean,
private val onClickAction: ((ValidationResult.ValidationError) -> Unit)? = null
) : JPanel(BorderLayout()) {
private val textLabel = defaultText?.let { label("$it: ") }
private val errorLabel = label(" ").apply {
foreground = JBColor.red
}
private var currentError: ValidationResult.ValidationError? = null
init {
border = BorderFactory.createEmptyBorder(3, 3, 3, 3)
textLabel?.let { add(it, BorderLayout.WEST) }
add(errorLabel, BorderLayout.CENTER)
if (onClickAction != null) {
cursor = Cursor(Cursor.HAND_CURSOR)
addMouseListener(object : MouseListener {
override fun mouseReleased(p0: MouseEvent?) {}
override fun mouseEntered(p0: MouseEvent?) {}
override fun mouseExited(p0: MouseEvent?) {}
override fun mousePressed(p0: MouseEvent?) {}
override fun mouseClicked(p0: MouseEvent?) {
currentError?.let(onClickAction)
}
})
}
}
private fun setIcon(icon: Icon?) {
@@ -31,6 +52,9 @@ class ValidationIndicator(
else errorLabel.icon = icon ?: EmptyIcon.ICON_16
}
private fun String.linkifyErrorText() =
if (onClickAction == null) this
else "<u>$this</u>".asHtml()
var validationState: ValidationResult = ValidationResult.OK
set(value) {
@@ -40,12 +64,14 @@ class ValidationIndicator(
setIcon(null)
errorLabel.text = " "
toolTipText = null
currentError = null
}
is ValidationResult.ValidationError -> {
setIcon(AllIcons.General.Error)
val errors = value.messages.firstOrNull().orEmpty()
toolTipText = errors
if (showText) errorLabel.text = errors
if (showText) errorLabel.text = errors.linkifyErrorText()
currentError = value
}
}
}
@@ -354,7 +354,7 @@ class ListSettingType<V : Any>(private val parser: Parser<V>) : SettingType<List
validate { values ->
values.fold(ValidationResult.OK as ValidationResult) { result, value ->
result and when (value) {
is Validatable<*> -> (value.validator as SettingValidator<Any>).validate(this, value)
is Validatable<*> -> (value.validator as SettingValidator<Any>).validate(this, value).withTargetIfNull(value)
else -> ValidationResult.OK
}
}
@@ -55,8 +55,8 @@ sealed class ValidationResult {
override val isOk = true
}
class ValidationError(val messages: List<String>) : ValidationResult() {
constructor(message: String) : this(listOf(message))
data class ValidationError(val messages: List<String>, val target: Any? = null) : ValidationResult() {
constructor(message: String, target: Any? = null) : this(listOf(message), target)
override val isOk = false
}
@@ -73,6 +73,19 @@ sealed class ValidationResult {
}
}
infix fun ValidationResult.isSpecificError(error: ValidationResult.ValidationError) =
this is ValidationResult.ValidationError && messages.firstOrNull() == error.messages.firstOrNull()
fun ValidationResult.withTarget(target: Any) = when (this) {
ValidationResult.OK -> this
is ValidationResult.ValidationError -> copy(target = target)
}
fun ValidationResult.withTargetIfNull(target: Any) = when (this) {
ValidationResult.OK -> this
is ValidationResult.ValidationError -> if (this.target == null) copy(target = target) else this
}
fun ValidationResult.toResult() = when (this) {
ValidationResult.OK -> UNIT_SUCCESS
is ValidationResult.ValidationError -> Failure(messages.map { ValidationError(it) })
@@ -85,7 +98,7 @@ interface Validatable<out V> {
fun <V, Q : Validatable<Q>> ValuesReadingContext.validateList(list: List<Q>) = settingValidator<V> {
list.fold(ValidationResult.OK as ValidationResult) { result, value ->
result and value.validator.validate(this, value)
result and value.validator.validate(this, value).withTarget(value)
}
}