[ui-dsl] Mark row comments content as Nls
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / layout / migLayout / MigLayoutRow.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.migLayout
3
4 import com.intellij.icons.AllIcons
5 import com.intellij.openapi.observable.properties.GraphProperty
6 import com.intellij.openapi.ui.OnePixelDivider
7 import com.intellij.openapi.ui.TextFieldWithBrowseButton
8 import com.intellij.openapi.ui.ValidationInfo
9 import com.intellij.openapi.ui.panel.ComponentPanelBuilder
10 import com.intellij.ui.HideableTitledSeparator
11 import com.intellij.ui.SeparatorComponent
12 import com.intellij.ui.TitledSeparator
13 import com.intellij.ui.UIBundle
14 import com.intellij.ui.components.JBRadioButton
15 import com.intellij.ui.components.Label
16 import com.intellij.ui.layout.*
17 import com.intellij.util.SmartList
18 import net.miginfocom.layout.BoundSize
19 import net.miginfocom.layout.CC
20 import net.miginfocom.layout.LayoutUtil
21 import org.jetbrains.annotations.Nls
22 import javax.swing.*
23 import javax.swing.border.LineBorder
24 import kotlin.math.max
25 import kotlin.reflect.KMutableProperty0
26
27 internal class MigLayoutRow(private val parent: MigLayoutRow?,
28                             override val builder: MigLayoutBuilder,
29                             val labeled: Boolean = false,
30                             val noGrid: Boolean = false,
31                             private val indent: Int /* level number (nested rows) */,
32                             private val incrementsIndent: Boolean = parent != null) : Row() {
33   companion object {
34     private const val COMPONENT_ENABLED_STATE_KEY = "MigLayoutRow.enabled"
35
36     // as static method to ensure that members of current row are not used
37     private fun createCommentRow(parent: MigLayoutRow,
38                                  component: JComponent,
39                                  indent: Int,
40                                  isParentRowLabeled: Boolean,
41                                  forComponent: Boolean,
42                                  columnIndex: Int) {
43       val cc = CC()
44       val commentRow = parent.createChildRow()
45       commentRow.isComment = true
46       commentRow.addComponent(component, cc)
47       if (forComponent) {
48         cc.horizontal.gapBefore = BoundSize.NULL_SIZE
49         cc.skip = columnIndex
50       }
51       else if (isParentRowLabeled) {
52         cc.horizontal.gapBefore = BoundSize.NULL_SIZE
53         cc.skip()
54       }
55       else {
56         cc.horizontal.gapBefore = gapToBoundSize(indent + parent.spacing.indentLevel, true)
57       }
58     }
59
60     // as static method to ensure that members of current row are not used
61     private fun configureSeparatorRow(row: MigLayoutRow, title: String?) {
62       val separatorComponent = if (title == null) SeparatorComponent(0, OnePixelDivider.BACKGROUND, null) else TitledSeparator(title)
63       row.addTitleComponent(separatorComponent, isEmpty = title == null)
64     }
65   }
66
67   val components: MutableList<JComponent> = SmartList()
68   var rightIndex = Int.MAX_VALUE
69
70   private var lastComponentConstraintsWithSplit: CC? = null
71
72   private var columnIndex = -1
73
74   internal var subRows: MutableList<MigLayoutRow>? = null
75     private set
76
77   var gapAfter: String? = null
78
79   private var componentIndexWhenCellModeWasEnabled = -1
80
81   private val spacing: SpacingConfiguration
82     get() = builder.spacing
83
84   private var isTrailingSeparator = false
85   private var isComment = false
86
87   override fun withButtonGroup(title: String?, buttonGroup: ButtonGroup, body: () -> Unit) {
88     if (title != null) {
89       label(title)
90       gapAfter = "${spacing.radioGroupTitleVerticalGap}px!"
91     }
92     builder.withButtonGroup(buttonGroup, body)
93   }
94
95   override fun checkBoxGroup(title: String?, body: () -> Unit) {
96     if (title != null) {
97       label(title)
98       gapAfter = "${spacing.radioGroupTitleVerticalGap}px!"
99     }
100     body()
101   }
102
103   override var enabled = true
104     set(value) {
105       if (field == value) {
106         return
107       }
108
109       field = value
110       for (c in components) {
111         if (!value) {
112           if (!c.isEnabled) {
113             // current state of component differs from current row state - preserve current state to apply it when row state will be changed
114             c.putClientProperty(COMPONENT_ENABLED_STATE_KEY, false)
115           }
116         }
117         else {
118           if (c.getClientProperty(COMPONENT_ENABLED_STATE_KEY) == false) {
119             // remove because for active row component state can be changed and we don't want to add listener to update value accordingly
120             c.putClientProperty(COMPONENT_ENABLED_STATE_KEY, null)
121             // do not set to true, preserve old component state
122             continue
123           }
124         }
125         c.isEnabled = value
126       }
127     }
128
129   override var visible = true
130     set(value) {
131       if (field == value) {
132         return
133       }
134
135       field = value
136       for (c in components) {
137         c.isVisible = value
138       }
139     }
140
141   override var subRowsEnabled = true
142     set(value) {
143       if (field == value) {
144         return
145       }
146
147       field = value
148       subRows?.forEach {
149         it.enabled = value
150         it.subRowsEnabled = value
151       }
152     }
153
154   override var subRowsVisible = true
155     set(value) {
156       if (field == value) {
157         return
158       }
159
160       field = value
161       subRows?.forEach {
162         it.visible = value
163         it.subRowsVisible = value
164       }
165     }
166
167   override var subRowIndent: Int = -1
168
169   internal val isLabeledIncludingSubRows: Boolean
170     get() = labeled || (subRows?.any { it.isLabeledIncludingSubRows } ?: false)
171
172   internal val columnIndexIncludingSubRows: Int
173     get() = max(columnIndex, subRows?.asSequence()?.map { it.columnIndexIncludingSubRows }?.max() ?: -1)
174
175   override fun createChildRow(label: JLabel?, isSeparated: Boolean, noGrid: Boolean, title: String?): MigLayoutRow {
176     return createChildRow(indent, label, isSeparated, noGrid, title)
177   }
178
179   private fun createChildRow(indent: Int,
180                              label: JLabel? = null,
181                              isSeparated: Boolean = false,
182                              noGrid: Boolean = false,
183                              title: String? = null,
184                              incrementsIndent: Boolean = true): MigLayoutRow {
185     val subRows = getOrCreateSubRowsList()
186     val newIndent = if (!this.incrementsIndent) indent else indent + spacing.indentLevel
187
188     val row = MigLayoutRow(this, builder,
189                            labeled = label != null,
190                            noGrid = noGrid,
191                            indent = if (subRowIndent >= 0) subRowIndent * spacing.indentLevel else newIndent,
192                            incrementsIndent = incrementsIndent)
193
194     if (isSeparated) {
195       val separatorRow = MigLayoutRow(this, builder, indent = newIndent, noGrid = true)
196       configureSeparatorRow(separatorRow, title)
197       separatorRow.enabled = subRowsEnabled
198       separatorRow.subRowsEnabled = subRowsEnabled
199       separatorRow.visible = subRowsVisible
200       separatorRow.subRowsVisible = subRowsVisible
201       row.getOrCreateSubRowsList().add(separatorRow)
202     }
203
204     var insertIndex = subRows.size
205     if (insertIndex > 0 && subRows[insertIndex-1].isTrailingSeparator) {
206       insertIndex--
207     }
208     if (insertIndex > 0 && subRows[insertIndex-1].isComment) {
209       insertIndex--
210     }
211     subRows.add(insertIndex, row)
212
213     row.enabled = subRowsEnabled
214     row.subRowsEnabled = subRowsEnabled
215     row.visible = subRowsVisible
216     row.subRowsVisible = subRowsVisible
217
218     if (label != null) {
219       row.addComponent(label)
220     }
221
222     return row
223   }
224
225   private fun <T : JComponent> addTitleComponent(titleComponent: T, isEmpty: Boolean) {
226     val cc = CC()
227     if (isEmpty) {
228       cc.vertical.gapAfter = gapToBoundSize(spacing.verticalGap * 2, false)
229       isTrailingSeparator = true
230     }
231     else {
232       // TitledSeparator doesn't grow by default opposite to SeparatorComponent
233       cc.growX()
234     }
235     addComponent(titleComponent, cc)
236   }
237
238   override fun titledRow(title: String, init: Row.() -> Unit): Row {
239     return createBlockRow(title, true, init)
240   }
241
242   override fun blockRow(init: Row.() -> Unit): Row {
243     return createBlockRow(null, false, init)
244   }
245
246   private fun createBlockRow(title: String?, isSeparated: Boolean, init: Row.() -> Unit): Row {
247     val parentRow = createChildRow(indent = indent, title = title, isSeparated = isSeparated, incrementsIndent = isSeparated)
248     parentRow.init()
249     val result = parentRow.createChildRow()
250     result.placeholder()
251     result.largeGapAfter()
252     return result
253   }
254
255   override fun hideableRow(title: String, init: Row.() -> Unit): Row {
256     val titledSeparator = HideableTitledSeparator(title)
257     val separatorRow = createChildRow()
258     separatorRow.addTitleComponent(titledSeparator, isEmpty = false)
259     builder.hideableRowNestingLevel++
260     try {
261       val panelRow = createChildRow(indent + spacing.indentLevel)
262       panelRow.init()
263       titledSeparator.row = panelRow
264       titledSeparator.collapse()
265       return panelRow
266     }
267     finally {
268       builder.hideableRowNestingLevel--
269     }
270   }
271
272   private fun getOrCreateSubRowsList(): MutableList<MigLayoutRow> {
273     var subRows = subRows
274     if (subRows == null) {
275       // subRows in most cases > 1
276       subRows = ArrayList()
277       this.subRows = subRows
278     }
279     return subRows
280   }
281
282   // cell mode not tested with "gear" button, wait first user request
283   override fun setCellMode(value: Boolean, isVerticalFlow: Boolean, fullWidth: Boolean) {
284     if (value) {
285       assert(componentIndexWhenCellModeWasEnabled == -1)
286       componentIndexWhenCellModeWasEnabled = components.size
287     }
288     else {
289       val firstComponentIndex = componentIndexWhenCellModeWasEnabled
290       componentIndexWhenCellModeWasEnabled = -1
291
292       val componentCount = components.size - firstComponentIndex
293       if (componentCount == 0) return
294       val component = components.get(firstComponentIndex)
295       val cc = component.constraints
296
297       // do not add split if cell empty or contains the only component
298       if (componentCount > 1) {
299         cc.split(componentCount)
300       }
301       if (fullWidth) {
302         cc.spanX(LayoutUtil.INF)
303       }
304       if (isVerticalFlow) {
305         cc.flowY()
306         // because when vertical buttons placed near scroll pane, it wil be centered by baseline (and baseline not applicable for grow elements, so, will be centered)
307         cc.alignY("top")
308       }
309     }
310   }
311
312   override fun <T : JComponent> component(component: T): CellBuilder<T> {
313     addComponent(component)
314     return CellBuilderImpl(builder, this, component)
315   }
316
317   internal fun addComponent(component: JComponent, cc: CC = CC()) {
318     components.add(component)
319     builder.componentConstraints.put(component, cc)
320
321     if (!visible) {
322       component.isVisible = false
323     }
324     if (!enabled) {
325       component.isEnabled = false
326     }
327
328     if (!shareCellWithPreviousComponentIfNeeded(component, cc)) {
329       // increase column index if cell mode not enabled or it is a first component of cell
330       if (componentIndexWhenCellModeWasEnabled == -1 || componentIndexWhenCellModeWasEnabled == (components.size - 1)) {
331         columnIndex++
332       }
333     }
334
335     if (labeled && components.size == 2 && component.border is LineBorder) {
336       builder.componentConstraints.get(components.first())?.vertical?.gapBefore = builder.defaultComponentConstraintCreator.vertical1pxGap
337     }
338
339     if (component is JRadioButton) {
340       builder.topButtonGroup?.add(component)
341     }
342
343     builder.defaultComponentConstraintCreator.addGrowIfNeeded(cc, component, spacing)
344
345     if (!noGrid && indent > 0 && components.size == 1) {
346       cc.horizontal.gapBefore = gapToBoundSize(indent, true)
347     }
348
349     if (builder.hideableRowNestingLevel > 0) {
350       cc.hideMode = 0
351     }
352
353     // if this row is not labeled and:
354     // a. previous row is labeled and first component is a "Remember" checkbox, skip one column (since this row doesn't have a label)
355     // b. some previous row is labeled and first component is a checkbox, span (since this checkbox should span across label and content cells)
356     if (!labeled && components.size == 1 && component is JCheckBox) {
357       val siblings = parent!!.subRows
358       if (siblings != null && siblings.size > 1) {
359         if (siblings.get(siblings.size - 2).labeled && component.text == UIBundle.message("auth.remember.cb")) {
360           cc.skip(1)
361           cc.horizontal.gapBefore = BoundSize.NULL_SIZE
362         }
363         else if (siblings.any { it.labeled }) {
364           cc.spanX(2)
365         }
366       }
367     }
368
369     // MigLayout doesn't check baseline if component has grow
370     if (labeled && component is JScrollPane && component.viewport.view is JTextArea) {
371       val labelCC = components.get(0).constraints
372       labelCC.alignY("top")
373
374       val labelTop = component.border?.getBorderInsets(component)?.top ?: 0
375       if (labelTop != 0) {
376         labelCC.vertical.gapBefore = gapToBoundSize(labelTop, false)
377       }
378     }
379   }
380
381   private val JComponent.constraints: CC
382     get() = builder.componentConstraints.getOrPut(this) { CC() }
383
384   fun addCommentRow(@Nls comment: String, maxLineLength: Int, forComponent: Boolean) {
385     val commentComponent = ComponentPanelBuilder.createCommentComponent(comment, true, maxLineLength, true)
386     addCommentRow(commentComponent, forComponent)
387   }
388
389   fun addCommentRow(component: JComponent, forComponent: Boolean) {
390     gapAfter = "${spacing.commentVerticalTopGap}px!"
391
392     val isParentRowLabeled = labeled
393     createCommentRow(this, component, indent, isParentRowLabeled, forComponent, columnIndex)
394   }
395
396   private fun shareCellWithPreviousComponentIfNeeded(component: JComponent, componentCC: CC): Boolean {
397     if (components.size > 1 && component is JLabel && component.icon === AllIcons.General.GearPlain) {
398       componentCC.horizontal.gapBefore = builder.defaultComponentConstraintCreator.horizontalUnitSizeGap
399
400       if (lastComponentConstraintsWithSplit == null) {
401         val prevComponent = components.get(components.size - 2)
402         val prevCC = prevComponent.constraints
403         prevCC.split++
404         lastComponentConstraintsWithSplit = prevCC
405       }
406       else {
407         lastComponentConstraintsWithSplit!!.split++
408       }
409       return true
410     }
411     else {
412       lastComponentConstraintsWithSplit = null
413       return false
414     }
415   }
416
417   override fun alignRight() {
418     if (rightIndex != Int.MAX_VALUE) {
419       throw IllegalStateException("right allowed only once")
420     }
421     rightIndex = components.size
422   }
423
424   override fun largeGapAfter() {
425     gapAfter = "${spacing.largeVerticalGap}px!"
426   }
427
428   override fun createRow(label: String?): Row {
429     return createChildRow(label = label?.let { Label(it) })
430   }
431
432   override fun createNoteOrCommentRow(component: JComponent): Row {
433     val cc = CC()
434     cc.vertical.gapBefore = gapToBoundSize(if (subRows == null) spacing.verticalGap else spacing.largeVerticalGap, false)
435     cc.vertical.gapAfter = gapToBoundSize(spacing.verticalGap, false)
436
437     val row = createChildRow(label = null, noGrid = true)
438     row.addComponent(component, cc)
439     return row
440   }
441
442   override fun radioButton(text: String, comment: String?): CellBuilder<JBRadioButton> {
443     val result = super.radioButton(text, comment)
444     attachSubRowsEnabled(result.component)
445     return result
446   }
447
448   override fun radioButton(text: String, prop: KMutableProperty0<Boolean>, comment: String?): CellBuilder<JBRadioButton> {
449     return super.radioButton(text, prop, comment).also { attachSubRowsEnabled(it.component) }
450   }
451
452   override fun onGlobalApply(callback: () -> Unit): Row {
453     builder.applyCallbacks.getOrPut(null, { SmartList() }).add(callback)
454     return this
455   }
456
457   override fun onGlobalReset(callback: () -> Unit): Row {
458     builder.resetCallbacks.getOrPut(null, { SmartList() }).add(callback)
459     return this
460   }
461
462   override fun onGlobalIsModified(callback: () -> Boolean): Row {
463     builder.isModifiedCallbacks.getOrPut(null, { SmartList() }).add(callback)
464     return this
465   }
466 }
467
468 private class CellBuilderImpl<T : JComponent> internal constructor(
469   private val builder: MigLayoutBuilder,
470   private val row: MigLayoutRow,
471   override val component: T
472 ) : CellBuilder<T>, CheckboxCellBuilder, ScrollPaneCellBuilder {
473   private var applyIfEnabled = false
474   private var property: GraphProperty<*>? = null
475
476   override fun withGraphProperty(property: GraphProperty<*>): CellBuilder<T> {
477     this.property = property
478     return this
479   }
480
481   override fun comment(text: String, maxLineLength: Int, forComponent: Boolean): CellBuilder<T> {
482     row.addCommentRow(text, maxLineLength, forComponent)
483     return this
484   }
485
486   override fun commentComponent(component: JComponent, forComponent: Boolean): CellBuilder<T> {
487     row.addCommentRow(component, forComponent)
488     return this
489   }
490
491   override fun focused(): CellBuilder<T> {
492     builder.preferredFocusedComponent = component
493     return this
494   }
495
496   override fun withValidationOnApply(callback: ValidationInfoBuilder.(T) -> ValidationInfo?): CellBuilder<T> {
497     builder.validateCallbacks.add { callback(ValidationInfoBuilder(component.origin), component) }
498     return this
499   }
500
501   override fun withValidationOnInput(callback: ValidationInfoBuilder.(T) -> ValidationInfo?): CellBuilder<T> {
502     builder.componentValidateCallbacks[component.origin] = { callback(ValidationInfoBuilder(component.origin), component) }
503     property?.let { builder.customValidationRequestors.getOrPut(component.origin, { SmartList() }).add(it::afterPropagation) }
504     return this
505   }
506
507   override fun onApply(callback: () -> Unit): CellBuilder<T> {
508     builder.applyCallbacks.getOrPut(component, { SmartList() }).add(callback)
509     return this
510   }
511
512   override fun onReset(callback: () -> Unit): CellBuilder<T> {
513     builder.resetCallbacks.getOrPut(component, { SmartList() }).add(callback)
514     return this
515   }
516
517   override fun onIsModified(callback: () -> Boolean): CellBuilder<T> {
518     builder.isModifiedCallbacks.getOrPut(component, { SmartList() }).add(callback)
519     return this
520   }
521
522   override fun enabled(isEnabled: Boolean) {
523     component.isEnabled = isEnabled
524   }
525
526   override fun enableIf(predicate: ComponentPredicate): CellBuilder<T> {
527     component.isEnabled = predicate()
528     predicate.addListener { component.isEnabled = it }
529     return this
530   }
531
532   override fun applyIfEnabled(): CellBuilder<T> {
533     applyIfEnabled = true
534     return this
535   }
536
537   override fun shouldSaveOnApply(): Boolean {
538     return !(applyIfEnabled && !component.isEnabled)
539   }
540
541   override fun actsAsLabel() {
542     builder.updateComponentConstraints(component) { spanX = 1 }
543   }
544
545   override fun noGrowY() {
546     builder.updateComponentConstraints(component) {
547       growY(0.0f)
548       pushY(0.0f)
549     }
550   }
551
552   override fun sizeGroup(name: String): CellBuilderImpl<T> {
553     builder.updateComponentConstraints(component) {
554       sizeGroup(name)
555     }
556     return this
557   }
558
559   override fun growPolicy(growPolicy: GrowPolicy): CellBuilder<T> {
560     builder.updateComponentConstraints(component) {
561       builder.defaultComponentConstraintCreator.applyGrowPolicy(this, growPolicy)
562     }
563     return this
564   }
565
566   override fun constraints(vararg constraints: CCFlags): CellBuilder<T> {
567     builder.updateComponentConstraints(component) {
568       overrideFlags(this, constraints)
569     }
570     return this
571   }
572
573   override fun withLargeLeftGap(): CellBuilder<T> {
574     builder.updateComponentConstraints(component) {
575       horizontal.gapBefore = gapToBoundSize(builder.spacing.largeHorizontalGap, true)
576     }
577     return this
578   }
579
580   override fun withLeftGap(gapLeft: Int): CellBuilder<T> {
581     builder.updateComponentConstraints(component) {
582       horizontal.gapBefore = gapToBoundSize(gapLeft, true)
583     }
584     return this
585   }
586 }
587
588 private val JComponent.origin: JComponent
589   get() {
590     return when (this) {
591       is TextFieldWithBrowseButton -> textField
592       else -> this
593     }
594   }