47cfc6dfa601065ea79d4bc65bd0e6db33ad82bf
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / layout / Cell.kt
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.ui.layout
3
4 import com.intellij.BundleBase
5 import com.intellij.icons.AllIcons
6 import com.intellij.openapi.actionSystem.AnAction
7 import com.intellij.openapi.actionSystem.DataContext
8 import com.intellij.openapi.actionSystem.DefaultActionGroup
9 import com.intellij.openapi.actionSystem.PlatformDataKeys
10 import com.intellij.openapi.actionSystem.ex.ActionUtil
11 import com.intellij.openapi.fileChooser.FileChooserDescriptor
12 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
13 import com.intellij.openapi.observable.properties.GraphProperty
14 import com.intellij.openapi.project.Project
15 import com.intellij.openapi.ui.ComboBox
16 import com.intellij.openapi.ui.TextFieldWithBrowseButton
17 import com.intellij.openapi.ui.ValidationInfo
18 import com.intellij.openapi.ui.panel.ComponentPanelBuilder
19 import com.intellij.openapi.ui.popup.JBPopupFactory
20 import com.intellij.openapi.util.text.StringUtil
21 import com.intellij.openapi.vfs.VirtualFile
22 import com.intellij.ui.*
23 import com.intellij.ui.components.*
24 import com.intellij.ui.components.fields.ExpandableTextField
25 import com.intellij.util.Function
26 import com.intellij.util.execution.ParametersListUtil
27 import com.intellij.util.ui.UIUtil
28 import org.jetbrains.annotations.ApiStatus
29 import org.jetbrains.annotations.Nls
30 import java.awt.Component
31 import java.awt.Dimension
32 import java.awt.event.ActionEvent
33 import java.awt.event.ActionListener
34 import java.awt.event.ItemEvent
35 import java.awt.event.MouseEvent
36 import java.util.*
37 import java.util.concurrent.atomic.AtomicBoolean
38 import javax.swing.*
39 import javax.swing.event.DocumentEvent
40 import kotlin.jvm.internal.CallableReference
41 import kotlin.reflect.KMutableProperty0
42
43 @DslMarker
44 annotation class CellMarker
45
46 data class PropertyBinding<V>(val get: () -> V, val set: (V) -> Unit)
47
48 @PublishedApi
49 internal fun <T> createPropertyBinding(prop: KMutableProperty0<T>, propType: Class<T>): PropertyBinding<T> {
50   if (prop is CallableReference) {
51     val name = prop.name
52     val receiver = (prop as CallableReference).boundReceiver
53     if (receiver != null) {
54       val baseName = name.removePrefix("is")
55       val nameCapitalized = StringUtil.capitalize(baseName)
56       val getterName = if (name.startsWith("is")) name else "get$nameCapitalized"
57       val setterName = "set$nameCapitalized"
58       val receiverClass = receiver::class.java
59
60       try {
61         val getter = receiverClass.getMethod(getterName)
62         val setter = receiverClass.getMethod(setterName, propType)
63         return PropertyBinding({ getter.invoke(receiver) as T }, { setter.invoke(receiver, it) })
64       }
65       catch (e: Exception) {
66         // ignore
67       }
68
69       try {
70         val field = receiverClass.getDeclaredField(name)
71         field.isAccessible = true
72         return PropertyBinding({ field.get(receiver) as T }, { field.set(receiver, it) })
73       }
74       catch (e: Exception) {
75         // ignore
76       }
77     }
78   }
79   return PropertyBinding(prop.getter, prop.setter)
80 }
81
82 fun <T> PropertyBinding<T>.toNullable(): PropertyBinding<T?> {
83   return PropertyBinding<T?>({ get() }, { set(it!!) })
84 }
85
86 inline fun <reified T : Any> KMutableProperty0<T>.toBinding(): PropertyBinding<T> {
87   return createPropertyBinding(this, T::class.javaPrimitiveType ?: T::class.java)
88 }
89
90 inline fun <reified T : Any> KMutableProperty0<T?>.toNullableBinding(defaultValue: T): PropertyBinding<T> {
91   return PropertyBinding({ get() ?: defaultValue }, { set(it) })
92 }
93
94 class ValidationInfoBuilder(val component: JComponent) {
95   fun error(@Nls message: String): ValidationInfo = ValidationInfo(message, component)
96   fun warning(@Nls message: String): ValidationInfo = ValidationInfo(message, component).asWarning().withOKEnabled()
97 }
98
99 interface CellBuilder<out T : JComponent> {
100   val component: T
101
102   fun comment(text: String, maxLineLength: Int = 70, forComponent: Boolean = false): CellBuilder<T>
103   fun commentComponent(component: JComponent, forComponent: Boolean = false): CellBuilder<T>
104   fun focused(): CellBuilder<T>
105   fun withValidationOnApply(callback: ValidationInfoBuilder.(T) -> ValidationInfo?): CellBuilder<T>
106   fun withValidationOnInput(callback: ValidationInfoBuilder.(T) -> ValidationInfo?): CellBuilder<T>
107   fun onApply(callback: () -> Unit): CellBuilder<T>
108   fun onReset(callback: () -> Unit): CellBuilder<T>
109   fun onIsModified(callback: () -> Boolean): CellBuilder<T>
110
111   /**
112    * All components of the same group share will get the same BoundSize (min/preferred/max),
113    * which is that of the biggest component in the group
114    */
115   fun sizeGroup(name: String): CellBuilder<T>
116   fun growPolicy(growPolicy: GrowPolicy): CellBuilder<T>
117   fun constraints(vararg constraints: CCFlags): CellBuilder<T>
118
119   /**
120    * If this method is called, the value of the component will be stored to the backing property only if the component is enabled.
121    */
122   fun applyIfEnabled(): CellBuilder<T>
123
124   fun <V> withBinding(
125     componentGet: (T) -> V,
126     componentSet: (T, V) -> Unit,
127     modelBinding: PropertyBinding<V>
128   ): CellBuilder<T> {
129     onApply { if (shouldSaveOnApply()) modelBinding.set(componentGet(component)) }
130     onReset { componentSet(component, modelBinding.get()) }
131     onIsModified { shouldSaveOnApply() && componentGet(component) != modelBinding.get() }
132     return this
133   }
134
135   fun withGraphProperty(property: GraphProperty<*>): CellBuilder<T>
136
137   fun enabled(isEnabled: Boolean)
138   fun enableIf(predicate: ComponentPredicate): CellBuilder<T>
139
140   fun withErrorOnApplyIf(message: String, callback: (T) -> Boolean): CellBuilder<T> {
141     withValidationOnApply { if (callback(it)) error(message) else null }
142     return this
143   }
144
145   @ApiStatus.Internal
146   fun shouldSaveOnApply(): Boolean
147
148   fun withLargeLeftGap(): CellBuilder<T>
149
150   @Deprecated("Prefer not to use hardcoded values")
151   fun withLeftGap(gapLeft: Int): CellBuilder<T>
152 }
153
154 internal interface CheckboxCellBuilder {
155   fun actsAsLabel()
156 }
157
158 fun <T : JCheckBox> CellBuilder<T>.actsAsLabel(): CellBuilder<T> {
159   (this as CheckboxCellBuilder).actsAsLabel()
160   return this
161 }
162
163 fun <T : JComponent> CellBuilder<T>.applyToComponent(task: T.() -> Unit): CellBuilder<T> {
164   return also { task(component) }
165 }
166
167 internal interface ScrollPaneCellBuilder {
168   fun noGrowY()
169 }
170
171 fun <T : JScrollPane> CellBuilder<T>.noGrowY(): CellBuilder<T> {
172   (this as ScrollPaneCellBuilder).noGrowY()
173   return this
174 }
175
176 fun <T : JTextField> CellBuilder<T>.withTextBinding(modelBinding: PropertyBinding<String>): CellBuilder<T> {
177   return withBinding(JTextField::getText, JTextField::setText, modelBinding)
178 }
179
180 fun <T : AbstractButton> CellBuilder<T>.withSelectedBinding(modelBinding: PropertyBinding<Boolean>): CellBuilder<T> {
181   return withBinding(AbstractButton::isSelected, AbstractButton::setSelected, modelBinding)
182 }
183
184 val CellBuilder<AbstractButton>.selected
185   get() = component.selected
186
187 const val UNBOUND_RADIO_BUTTON = "unbound.radio.button"
188
189 // separate class to avoid row related methods in the `cell { } `
190 @CellMarker
191 abstract class Cell : BaseBuilder {
192   /**
193    * Sets how keen the component should be to grow in relation to other component **in the same cell**. Use `push` in addition if need.
194    * If this constraint is not set the grow weight is set to 0 and the component will not grow (unless some automatic rule is not applied (see [com.intellij.ui.layout.panel])).
195    * Grow weight will only be compared against the weights for the same cell.
196    */
197   val growX = CCFlags.growX
198
199   @Suppress("unused")
200   val growY = CCFlags.growY
201   val grow = CCFlags.grow
202
203   /**
204    * Makes the column that the component is residing in grow with `weight`.
205    */
206   val pushX = CCFlags.pushX
207
208   /**
209    * Makes the row that the component is residing in grow with `weight`.
210    */
211   @Suppress("unused")
212   val pushY = CCFlags.pushY
213   val push = CCFlags.push
214
215   fun label(@Nls text: String,
216             style: UIUtil.ComponentStyle? = null,
217             fontColor: UIUtil.FontColor? = null,
218             bold: Boolean = false): CellBuilder<JLabel> {
219     val label = Label(text, style, fontColor, bold)
220     return component(label)
221   }
222
223   fun link(@Nls text: String,
224            style: UIUtil.ComponentStyle? = null,
225            action: () -> Unit): CellBuilder<JComponent> {
226     val result = Link(text, action = action)
227     style?.let { UIUtil.applyStyle(it, result) }
228     return component(result)
229   }
230
231   fun browserLink(@Nls text: String, url: String): CellBuilder<JComponent> {
232     val result = HyperlinkLabel()
233     result.setHyperlinkText(text)
234     result.setHyperlinkTarget(url)
235     return component(result)
236   }
237
238   fun buttonFromAction(@Nls text: String, actionPlace: String, action: AnAction): CellBuilder<JButton> {
239     val button = JButton(BundleBase.replaceMnemonicAmpersand(text))
240     button.addActionListener { ActionUtil.invokeAction(action, button, actionPlace, null, null) }
241     return component(button)
242   }
243
244   fun button(@Nls text: String, actionListener: (event: ActionEvent) -> Unit): CellBuilder<JButton> {
245     val button = JButton(BundleBase.replaceMnemonicAmpersand(text))
246     button.addActionListener(actionListener)
247     return component(button)
248   }
249
250   inline fun checkBox(@Nls text: String,
251                       isSelected: Boolean = false,
252                       comment: String? = null,
253                       crossinline actionListener: (event: ActionEvent, component: JCheckBox) -> Unit): CellBuilder<JBCheckBox> {
254     return checkBox(text, isSelected, comment)
255       .applyToComponent {
256         addActionListener(ActionListener { actionListener(it, this) })
257       }
258   }
259
260   @JvmOverloads
261   fun checkBox(@Nls text: String,
262                isSelected: Boolean = false,
263                comment: String? = null): CellBuilder<JBCheckBox> {
264     val result = JBCheckBox(text, isSelected)
265     return result(comment = comment)
266   }
267
268   fun checkBox(@Nls text: String, prop: KMutableProperty0<Boolean>, comment: String? = null): CellBuilder<JBCheckBox> {
269     return checkBox(text, prop.toBinding(), comment)
270   }
271
272   fun checkBox(@Nls text: String, getter: () -> Boolean, setter: (Boolean) -> Unit, comment: String? = null): CellBuilder<JBCheckBox> {
273     return checkBox(text, PropertyBinding(getter, setter), comment)
274   }
275
276   private fun checkBox(@Nls text: String,
277                        modelBinding: PropertyBinding<Boolean>,
278                        comment: String?): CellBuilder<JBCheckBox> {
279     val component = JBCheckBox(text, modelBinding.get())
280     return component(comment = comment).withSelectedBinding(modelBinding)
281   }
282
283   fun checkBox(@Nls text: String,
284                property: GraphProperty<Boolean>,
285                comment: String? = null): CellBuilder<JBCheckBox> {
286     val component = JBCheckBox(text, property.get())
287     return component(comment = comment).withGraphProperty(property).applyToComponent { component.bind(property) }
288   }
289
290   open fun radioButton(@Nls text: String, @Nls comment: String? = null): CellBuilder<JBRadioButton> {
291     val component = JBRadioButton(text)
292     component.putClientProperty(UNBOUND_RADIO_BUTTON, true)
293     return component(comment = comment)
294   }
295
296   open fun radioButton(@Nls text: String, getter: () -> Boolean, setter: (Boolean) -> Unit, @Nls comment: String? = null): CellBuilder<JBRadioButton> {
297     val component = JBRadioButton(text, getter())
298     return component(comment = comment).withSelectedBinding(PropertyBinding(getter, setter))
299   }
300
301   open fun radioButton(@Nls text: String, prop: KMutableProperty0<Boolean>, @Nls comment: String? = null): CellBuilder<JBRadioButton> {
302     val component = JBRadioButton(text, prop.get())
303     return component(comment = comment).withSelectedBinding(prop.toBinding())
304   }
305
306   fun <T> comboBox(model: ComboBoxModel<T>,
307                    getter: () -> T?,
308                    setter: (T?) -> Unit,
309                    renderer: ListCellRenderer<T?>? = null): CellBuilder<ComboBox<T>> {
310     return comboBox(model, PropertyBinding(getter, setter), renderer)
311   }
312
313   fun <T> comboBox(model: ComboBoxModel<T>,
314                    modelBinding: PropertyBinding<T?>,
315                    renderer: ListCellRenderer<T?>? = null): CellBuilder<ComboBox<T>> {
316     return component(ComboBox(model))
317       .applyToComponent {
318         this.renderer = renderer ?: SimpleListCellRenderer.create("") { it.toString() }
319         selectedItem = modelBinding.get()
320       }
321       .withBinding(
322         { component -> component.selectedItem as T? },
323         { component, value -> component.setSelectedItem(value) },
324         modelBinding
325       )
326   }
327
328   inline fun <reified T : Any> comboBox(
329     model: ComboBoxModel<T>,
330     prop: KMutableProperty0<T>,
331     renderer: ListCellRenderer<T?>? = null
332   ): CellBuilder<ComboBox<T>> {
333     return comboBox(model, prop.toBinding().toNullable(), renderer)
334   }
335
336   fun <T> comboBox(
337     model: ComboBoxModel<T>,
338     property: GraphProperty<T>,
339     renderer: ListCellRenderer<T?>? = null
340   ): CellBuilder<ComboBox<T>> {
341     return comboBox(model, PropertyBinding(property::get, property::set).toNullable(), renderer)
342       .withGraphProperty(property)
343       .applyToComponent { bind(property) }
344   }
345
346   fun textField(prop: KMutableProperty0<String>, columns: Int? = null): CellBuilder<JBTextField> = textField(prop.toBinding(), columns)
347
348   fun textField(getter: () -> String, setter: (String) -> Unit, columns: Int? = null) = textField(PropertyBinding(getter, setter), columns)
349
350   fun textField(binding: PropertyBinding<String>, columns: Int? = null): CellBuilder<JBTextField> {
351     return component(JBTextField(binding.get(), columns ?: 0))
352       .withTextBinding(binding)
353   }
354
355   fun textField(property: GraphProperty<String>, columns: Int? = null): CellBuilder<JBTextField> {
356     return textField(property::get, property::set, columns)
357       .withGraphProperty(property)
358       .applyToComponent { bind(property) }
359   }
360
361   fun intTextField(prop: KMutableProperty0<Int>, columns: Int? = null, range: IntRange? = null): CellBuilder<JBTextField> {
362     return intTextField(prop.toBinding(), columns, range)
363   }
364
365   fun intTextField(getter: () -> Int, setter: (Int) -> Unit, columns: Int? = null, range: IntRange? = null): CellBuilder<JBTextField> {
366     return intTextField(PropertyBinding(getter, setter), columns, range)
367   }
368
369   fun intTextField(binding: PropertyBinding<Int>, columns: Int? = null, range: IntRange? = null): CellBuilder<JBTextField> {
370     return textField(
371       { binding.get().toString() },
372       { value -> value.toIntOrNull()?.let { intValue -> binding.set(range?.let { intValue.coerceIn(it.first, it.last) } ?: intValue) } },
373       columns
374     ).withValidationOnInput {
375       val value = it.text.toIntOrNull()
376       when {
377         value == null -> error(UIBundle.message("please.enter.a.number"))
378         range != null && value !in range -> error(UIBundle.message("please.enter.a.number.from.0.to.1", range.first, range.last))
379         else -> null
380       }
381     }
382   }
383
384   fun spinner(prop: KMutableProperty0<Int>, minValue: Int, maxValue: Int, step: Int = 1): CellBuilder<JBIntSpinner> {
385     val spinner = JBIntSpinner(prop.get(), minValue, maxValue, step)
386     return component(spinner).withBinding(JBIntSpinner::getNumber, JBIntSpinner::setNumber, prop.toBinding())
387   }
388
389   fun spinner(getter: () -> Int, setter: (Int) -> Unit, minValue: Int, maxValue: Int, step: Int = 1): CellBuilder<JBIntSpinner> {
390     val spinner = JBIntSpinner(getter(), minValue, maxValue, step)
391     return component(spinner).withBinding(JBIntSpinner::getNumber, JBIntSpinner::setNumber, PropertyBinding(getter, setter))
392   }
393
394   fun textFieldWithHistoryWithBrowseButton(
395     browseDialogTitle: String,
396     value: String? = null,
397     project: Project? = null,
398     fileChooserDescriptor: FileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(),
399     historyProvider: (() -> List<String>)? = null,
400     fileChosen: ((chosenFile: VirtualFile) -> String)? = null
401   ): CellBuilder<TextFieldWithHistoryWithBrowseButton> {
402     val textField = textFieldWithHistoryWithBrowseButton(project, browseDialogTitle, fileChooserDescriptor, historyProvider, fileChosen)
403     if (value != null) textField.text = value
404     return component(textField)
405   }
406
407   fun textFieldWithBrowseButton(
408     browseDialogTitle: String? = null,
409     value: String? = null,
410     project: Project? = null,
411     fileChooserDescriptor: FileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(),
412     fileChosen: ((chosenFile: VirtualFile) -> String)? = null
413   ): CellBuilder<TextFieldWithBrowseButton> {
414     val textField = textFieldWithBrowseButton(project, browseDialogTitle, fileChooserDescriptor, fileChosen)
415     if (value != null) textField.text = value
416     return component(textField)
417   }
418
419   fun textFieldWithBrowseButton(
420     prop: KMutableProperty0<String>,
421     browseDialogTitle: String? = null,
422     project: Project? = null,
423     fileChooserDescriptor: FileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(),
424     fileChosen: ((chosenFile: VirtualFile) -> String)? = null
425   ): CellBuilder<TextFieldWithBrowseButton> {
426     val modelBinding = prop.toBinding()
427     return textFieldWithBrowseButton(modelBinding, browseDialogTitle, project, fileChooserDescriptor, fileChosen)
428   }
429
430   fun textFieldWithBrowseButton(
431     getter: () -> String,
432     setter: (String) -> Unit,
433     browseDialogTitle: String? = null,
434     project: Project? = null,
435     fileChooserDescriptor: FileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(),
436     fileChosen: ((chosenFile: VirtualFile) -> String)? = null
437   ): CellBuilder<TextFieldWithBrowseButton> {
438     val modelBinding = PropertyBinding(getter, setter)
439     return textFieldWithBrowseButton(modelBinding, browseDialogTitle, project, fileChooserDescriptor, fileChosen)
440   }
441
442   fun textFieldWithBrowseButton(
443     modelBinding: PropertyBinding<String>,
444     browseDialogTitle: String? = null,
445     project: Project? = null,
446     fileChooserDescriptor: FileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(),
447     fileChosen: ((chosenFile: VirtualFile) -> String)? = null
448   ): CellBuilder<TextFieldWithBrowseButton> {
449     val textField = textFieldWithBrowseButton(project, browseDialogTitle, fileChooserDescriptor, fileChosen)
450     textField.text = modelBinding.get()
451     return component(textField)
452       .constraints(growX)
453       .withBinding(TextFieldWithBrowseButton::getText, TextFieldWithBrowseButton::setText, modelBinding)
454   }
455
456   fun textFieldWithBrowseButton(
457     property: GraphProperty<String>,
458     browseDialogTitle: String? = null,
459     project: Project? = null,
460     fileChooserDescriptor: FileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(),
461     fileChosen: ((chosenFile: VirtualFile) -> String)? = null
462   ): CellBuilder<TextFieldWithBrowseButton> {
463     return textFieldWithBrowseButton(property::get, property::set, browseDialogTitle, project, fileChooserDescriptor, fileChosen)
464       .withGraphProperty(property)
465       .applyToComponent { bind(property) }
466   }
467
468   fun gearButton(vararg actions: AnAction): CellBuilder<JComponent> {
469     val label = JLabel(LayeredIcon(AllIcons.General.GearPlain, AllIcons.General.Dropdown))
470     label.disabledIcon = AllIcons.General.GearPlain
471     object : ClickListener() {
472       override fun onClick(e: MouseEvent, clickCount: Int): Boolean {
473         if (!label.isEnabled) return true
474         JBPopupFactory.getInstance()
475           .createActionGroupPopup(null, DefaultActionGroup(*actions), DataContext { dataId ->
476             when (dataId) {
477               PlatformDataKeys.CONTEXT_COMPONENT.name -> label
478               else -> null
479             }
480           }, true, null, 10)
481           .showUnderneathOf(label)
482         return true
483       }
484     }.installOn(label)
485
486     return component(label)
487   }
488
489   fun expandableTextField(getter: () -> String,
490                           setter: (String) -> Unit,
491                           parser: Function<in String, out MutableList<String>> = ParametersListUtil.DEFAULT_LINE_PARSER,
492                           joiner: Function<in MutableList<String>, String> = ParametersListUtil.DEFAULT_LINE_JOINER)
493     : CellBuilder<ExpandableTextField> {
494     return ExpandableTextField(parser, joiner)()
495       .withBinding({ editor -> editor.text.orEmpty() },
496                    { editor, value -> editor.text = value },
497                    PropertyBinding(getter, setter))
498   }
499
500   fun expandableTextField(prop: KMutableProperty0<String>,
501                           parser: Function<in String, out MutableList<String>> = ParametersListUtil.DEFAULT_LINE_PARSER,
502                           joiner: Function<in MutableList<String>, String> = ParametersListUtil.DEFAULT_LINE_JOINER)
503     : CellBuilder<ExpandableTextField> {
504     return expandableTextField(prop::get, prop::set, parser, joiner)
505   }
506
507   fun expandableTextField(prop: GraphProperty<String>,
508                           parser: Function<in String, out MutableList<String>> = ParametersListUtil.DEFAULT_LINE_PARSER,
509                           joiner: Function<in MutableList<String>, String> = ParametersListUtil.DEFAULT_LINE_JOINER)
510     : CellBuilder<ExpandableTextField> {
511     return expandableTextField(prop::get, prop::set, parser, joiner)
512       .withGraphProperty(prop)
513       .applyToComponent { bind(prop) }
514   }
515
516   /**
517    * @see LayoutBuilder.titledRow
518    */
519   @JvmOverloads
520   fun panel(title: String, wrappedComponent: Component, hasSeparator: Boolean = true): CellBuilder<JPanel> {
521     val panel = Panel(title, hasSeparator)
522     panel.add(wrappedComponent)
523     return component(panel)
524   }
525
526   fun scrollPane(component: Component): CellBuilder<JScrollPane> {
527     return component(JBScrollPane(component))
528   }
529
530   fun comment(text: String, maxLineLength: Int = -1): CellBuilder<JLabel> {
531     return component(ComponentPanelBuilder.createCommentComponent(text, true, maxLineLength, true))
532   }
533
534   fun commentNoWrap(text: String): CellBuilder<JLabel> {
535     return component(ComponentPanelBuilder.createNonWrappingCommentComponent(text))
536   }
537
538   fun placeholder(): CellBuilder<JComponent> {
539     return component(JPanel().apply {
540       minimumSize = Dimension(0, 0)
541       preferredSize = Dimension(0, 0)
542       maximumSize = Dimension(0, 0)
543     })
544   }
545
546   abstract fun <T : JComponent> component(component: T): CellBuilder<T>
547
548   operator fun <T : JComponent> T.invoke(
549     vararg constraints: CCFlags,
550     growPolicy: GrowPolicy? = null,
551     comment: String? = null
552   ): CellBuilder<T> = component(this).apply {
553     constraints(*constraints)
554     if (comment != null) comment(comment)
555     if (growPolicy != null) growPolicy(growPolicy)
556   }
557 }
558
559 private fun JBCheckBox.bind(property: GraphProperty<Boolean>) {
560   val mutex = AtomicBoolean()
561   property.afterChange {
562     mutex.lockOrSkip {
563       isSelected = property.get()
564     }
565   }
566   addItemListener {
567     mutex.lockOrSkip {
568       property.set(isSelected)
569     }
570   }
571 }
572
573 class InnerCell(val cell: Cell) : Cell() {
574   override fun <T : JComponent> component(component: T): CellBuilder<T> {
575     return cell.component(component)
576   }
577
578   override fun withButtonGroup(title: String?, buttonGroup: ButtonGroup, body: () -> Unit) {
579     cell.withButtonGroup(title, buttonGroup, body)
580   }
581 }
582
583 fun <T> listCellRenderer(renderer: SimpleListCellRenderer<T?>.(value: T, index: Int, isSelected: Boolean) -> Unit): SimpleListCellRenderer<T?> {
584   return object : SimpleListCellRenderer<T?>() {
585     override fun customize(list: JList<out T?>, value: T?, index: Int, selected: Boolean, hasFocus: Boolean) {
586       if (value != null) {
587         renderer(this, value, index, selected)
588       }
589     }
590   }
591 }
592
593 private fun <T> ComboBox<T>.bind(property: GraphProperty<T>) {
594   val mutex = AtomicBoolean()
595   property.afterChange {
596     mutex.lockOrSkip {
597       selectedItem = it
598     }
599   }
600   addItemListener {
601     if (it.stateChange == ItemEvent.SELECTED) {
602       mutex.lockOrSkip {
603         @Suppress("UNCHECKED_CAST")
604         property.set(it.item as T)
605       }
606     }
607   }
608 }
609
610 private fun TextFieldWithBrowseButton.bind(property: GraphProperty<String>) {
611   textField.bind(property)
612 }
613
614 private fun JTextField.bind(property: GraphProperty<String>) {
615   val mutex = AtomicBoolean()
616   property.afterChange {
617     mutex.lockOrSkip {
618       text = it
619     }
620   }
621   document.addDocumentListener(
622     object : DocumentAdapter() {
623       override fun textChanged(e: DocumentEvent) {
624         mutex.lockOrSkip {
625           property.set(text)
626         }
627       }
628     }
629   )
630 }
631
632 private fun AtomicBoolean.lockOrSkip(action: () -> Unit) {
633   if (!compareAndSet(false, true)) return
634   try {
635     action()
636   }
637   finally {
638     set(false)
639   }
640 }
641
642 fun Cell.slider(min: Int, max: Int, minorTick: Int, majorTick: Int): CellBuilder<JSlider> {
643   val slider = JSlider()
644   UIUtil.setSliderIsFilled(slider, true)
645   slider.paintLabels = true
646   slider.paintTicks = true
647   slider.paintTrack = true
648   slider.minimum = min
649   slider.maximum = max
650   slider.minorTickSpacing = minorTick
651   slider.majorTickSpacing = majorTick
652   return slider()
653 }
654
655 fun <T : JSlider> CellBuilder<T>.labelTable(table: Hashtable<Int, JComponent>.() -> Unit): CellBuilder<T> {
656   component.labelTable = Hashtable<Int, JComponent>().apply(table)
657   return this
658 }
659
660 fun <T : JSlider> CellBuilder<T>.withValueBinding(modelBinding: PropertyBinding<Int>): CellBuilder<T> {
661   return withBinding(JSlider::getValue, JSlider::setValue, modelBinding)
662 }