Kotlin UI DSL — align fields in the nested grid

This commit is contained in:
Vladimir Krivosheev
2018-03-08 23:05:13 +01:00
parent 9a9a0f6227
commit 28c6164137
12 changed files with 573 additions and 441 deletions
@@ -145,7 +145,7 @@ internal class PasswordSafeConfigurableUi : ConfigurableUi<PasswordSafeSettings>
keePassMasterPassword.setPasswordIsStored(true)
val panel = panel {
return panel {
row { label("Save passwords:") }
buttonGroup({ updateEnabledState() }) {
@@ -165,46 +165,46 @@ internal class PasswordSafeConfigurableUi : ConfigurableUi<PasswordSafeSettings>
fileChooserDescriptor = fileChooserDescriptor,
fileChosen = ::normalizeSelectedFile)
gearButton(
object : AnAction("Clear") {
override fun actionPerformed(event: AnActionEvent) {
if (MessageDialogBuilder.yesNo("Clear Passwords", "Are you sure want to remove all passwords?").yesText("Remove Passwords").isYes) {
passwordSafe.clearPasswords()
}
object : AnAction("Clear") {
override fun actionPerformed(event: AnActionEvent) {
if (MessageDialogBuilder.yesNo("Clear Passwords", "Are you sure want to remove all passwords?").yesText("Remove Passwords").isYes) {
passwordSafe.clearPasswords()
}
},
object : AnAction("Import") {
override fun actionPerformed(event: AnActionEvent) {
chooseFile(fileChooserDescriptor, event) {
val wantedDbFile = Paths.get(normalizeSelectedFile(it))
val dbFile = getCurrentDbFile()
if (dbFile != wantedDbFile) {
val contextComponent = event.getData(PlatformDataKeys.CONTEXT_COMPONENT) as Component
Messages.showInputDialog(
contextComponent, "Master Password:", "Specify Master Password", null)?.trim().nullize()?.let { masterPassword ->
try {
Files.copy(wantedDbFile, dbFile, StandardCopyOption.REPLACE_EXISTING)
passwordSafe.currentProvider = KeePassCredentialStore(existingMasterPassword = masterPassword.toByteArray(),
dbFile = getCurrentDbFile())
}
catch (e: Exception) {
LOG.error(e)
if (e.message == "Inconsistent stream bytes") {
Messages.showMessageDialog(contextComponent, if (e.message == "Inconsistent stream bytes") "Password is not correct" else "Internal error", "Cannot Import", Messages.getErrorIcon())
}
}
keePassMasterPassword.text = ""
}
},
object : AnAction("Import") {
override fun actionPerformed(event: AnActionEvent) {
chooseFile(fileChooserDescriptor, event) {
val wantedDbFile = Paths.get(normalizeSelectedFile(it))
val dbFile = getCurrentDbFile()
if (dbFile != wantedDbFile) {
val contextComponent = event.getData(PlatformDataKeys.CONTEXT_COMPONENT) as Component
Messages.showInputDialog(
contextComponent, "Master Password:", "Specify Master Password", null)?.trim().nullize()?.let { masterPassword ->
try {
Files.copy(wantedDbFile, dbFile, StandardCopyOption.REPLACE_EXISTING)
passwordSafe.currentProvider = KeePassCredentialStore(existingMasterPassword = masterPassword.toByteArray(),
dbFile = getCurrentDbFile())
}
catch (e: Exception) {
LOG.error(e)
if (e.message == "Inconsistent stream bytes") {
Messages.showMessageDialog(contextComponent, if (e.message == "Inconsistent stream bytes") "Password is not correct" else "Internal error", "Cannot Import", Messages.getErrorIcon())
}
}
keePassMasterPassword.text = ""
}
}
}
}
}
)
}
row("Master Password:") {
keePassMasterPassword(growPolicy = GrowPolicy.SHORT_TEXT)
}
if (!SystemInfo.isWindows) {
row { hint("Stored using weak encryption. It is recommended to store password database on encrypted volume for additional security.") }
row { hint("Stored using weak encryption. It is recommended to store on encrypted volume for additional security.") }
}
}
@@ -218,8 +218,6 @@ internal class PasswordSafeConfigurableUi : ConfigurableUi<PasswordSafeSettings>
}
}
}
return panel
}
private fun getProviderType(): ProviderType {
@@ -12,14 +12,16 @@ import com.intellij.openapi.ui.OnePixelDivider
import com.intellij.ui.SeparatorComponent
import com.intellij.ui.TextFieldWithHistory
import com.intellij.ui.TextFieldWithHistoryWithBrowseButton
import com.intellij.ui.components.Label
import com.intellij.ui.components.noteComponent
import com.intellij.util.SmartList
import net.miginfocom.layout.*
import net.miginfocom.swing.MigLayout
import java.awt.Component
import java.awt.Container
import javax.swing.*
import javax.swing.ButtonGroup
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.text.JTextComponent
/**
@@ -31,18 +33,21 @@ internal class MigLayoutBuilder : LayoutBuilderImpl {
private val componentConstraints: MutableMap<Component, CC> = SmartHashMap()
override fun newRow(label: JLabel?, buttonGroup: ButtonGroup?, separated: Boolean, indented: Boolean): Row {
override fun newRow(label: JLabel?, buttonGroup: ButtonGroup?, separated: Boolean): Row {
return newRow(rows, label, buttonGroup, separated)
}
internal fun newRow(rowList: MutableList<MigLayoutRow>, label: JLabel?, buttonGroup: ButtonGroup? = null, separated: Boolean = false): Row {
if (separated) {
val row = MigLayoutRow(componentConstraints, this, noGrid = true, separated = true)
rows.add(row)
rowList.add(row)
row.apply { SeparatorComponent(0, OnePixelDivider.BACKGROUND, null)() }
}
val row = MigLayoutRow(componentConstraints, this, label != null, buttonGroup = buttonGroup, indented = indented)
rows.add(row)
val row = MigLayoutRow(componentConstraints, this, label != null, buttonGroup = buttonGroup)
rowList.add(row)
if (label != null) {
// componentConstraints.put(label, CC().growPrioX(-1))
row.apply { label() }
}
@@ -59,10 +64,9 @@ internal class MigLayoutBuilder : LayoutBuilderImpl {
}
override fun build(container: Container, layoutConstraints: Array<out LCFlags>) {
val isLabeled = rows.firstOrNull({ it.labeled && !it.indented }) != null
var gapTop = -1
val lc = c()
val lc = createLayoutConstraints()
if (layoutConstraints.isEmpty()) {
lc.fillX()
// not fillY because it leads to enormously large cells - we use cc `push` in addition to cc `grow` as a more robust and easy solution
@@ -74,22 +78,18 @@ internal class MigLayoutBuilder : LayoutBuilderImpl {
lc.noVisualPadding()
lc.hideMode = 3
val columnConstraints = if (isLabeled) AC() else null
val columnConstraints = AC()
var columnIndex = 0
if (isLabeled) {
columnConstraints!!.grow(0f, columnIndex++)
}
container.layout = MigLayout(lc, columnConstraints)
val isNoGrid = layoutConstraints.contains(LCFlags.noGrid)
for (row in rows) {
fun configureComponents(row: MigLayoutRow, prevRow: MigLayoutRow?, isSubRow: Boolean, isLabeled: Boolean) {
val lastComponent = row.components.lastOrNull()
if (lastComponent == null) {
if (row === rows.first()) {
if (prevRow == null) {
// do not add gap for the first row
continue
return
}
// https://goo.gl/LDylKm
@@ -109,63 +109,94 @@ internal class MigLayoutBuilder : LayoutBuilderImpl {
addGrowIfNeed(cc, component)
if (!isNoGrid) {
if (component === lastComponent) {
isSplitRequired = false
cc.wrap()
if (isNoGrid) {
container.add(component, cc)
continue
}
columnConstraints?.grow(100f, columnIndex++)
if (component === lastComponent) {
isSplitRequired = false
cc.wrap()
if (isLabeled) {
columnConstraints.grow(100f, columnIndex++)
}
}
if (row.noGrid) {
if (component === row.components.first()) {
// rowConstraints.noGrid() doesn't work correctly
if (row.noGrid) {
if (component === row.components.first()) {
// rowConstraints.noGrid() doesn't work correctly
cc.spanX()
if (row.separated) {
cc.vertical.gapBefore = gapToBoundSize(VERTICAL_GAP * 3, false)
cc.vertical.gapAfter = gapToBoundSize(VERTICAL_GAP * 2, false)
}
}
}
else {
var isSkippableComponent = true
if (component === row.components.first()) {
val isHintComponent = component.getClientProperty(COMPONENT_TAG_HINT) == true
if ((isSubRow && !isHintComponent) || (isHintComponent && prevRow != null && !prevRow.labeled)) {
cc.horizontal.gapBefore = gapToBoundSize(HORIZONTAL_GAP * 3, true)
}
if (isLabeled) {
if (row.labeled) {
isSkippableComponent = false
}
else {
cc.skip()
}
}
if (row.components.size == 1) {
cc.spanX()
if (row.separated) {
cc.vertical.gapBefore = gapToBoundSize(VERTICAL_GAP * 3, false)
cc.vertical.gapAfter = gapToBoundSize(VERTICAL_GAP * 2, false)
}
}
}
else {
var isSkippableComponent = true
if (component === row.components.first()) {
if (row.indented) {
cc.horizontal.gapBefore = gapToBoundSize(HORIZONTAL_GAP * 3, true)
}
if (isLabeled) {
if (row.labeled) {
isSkippableComponent = false
}
else {
cc.skip()
}
}
}
if (isSkippableComponent) {
if (isSplitRequired) {
isSplitRequired = false
cc.split()
}
// do not add gap if next component is gear action button
if (component !== lastComponent && !row.components.get(index + 1).let { it is JLabel && it.icon === AllIcons.General.Gear }) {
cc.horizontal.gapAfter = gapToBoundSize(HORIZONTAL_GAP * 2, true)
}
}
}
if (index >= row.rightIndex) {
cc.horizontal.gapBefore = BoundSize(null, null, null, true, null)
if (isSkippableComponent) {
if (isSplitRequired) {
isSplitRequired = false
cc.split()
}
// do not add gap if next component is gear action button
if (component !== lastComponent && !row.components.get(index + 1).let { it is JLabel && it.icon === AllIcons.General.Gear }) {
cc.horizontal.gapAfter = gapToBoundSize(HORIZONTAL_GAP * 2, true)
}
}
}
if (index >= row.rightIndex) {
cc.horizontal.gapBefore = BoundSize(null, null, null, true, null)
}
container.add(component, cc)
}
}
fun processRows(rows: List<MigLayoutRow>, isSubRow: Boolean) {
val isLabeled = rows.firstOrNull(MigLayoutRow::labeled) != null
var prevRow: MigLayoutRow? = null
for (row in rows) {
columnIndex = 0
if (isLabeled) {
columnConstraints.grow(0f, columnIndex++)
}
configureComponents(row, prevRow, isSubRow, isLabeled)
row._subRows?.let {
processRows(it, true)
}
prevRow = row
}
}
processRows(rows, false)
// do not hold components
componentConstraints.clear()
}
@@ -185,161 +216,23 @@ private fun addGrowIfNeed(cc: CC, component: Component) {
}
}
private class MigLayoutRow(private val componentConstraints: MutableMap<Component, CC>,
override val builder: LayoutBuilderImpl,
val labeled: Boolean = false,
val noGrid: Boolean = false,
private val buttonGroup: ButtonGroup? = null,
val separated: Boolean = false,
val indented: Boolean = false) : Row() {
val components = SmartList<Component>()
var rightIndex = Int.MAX_VALUE
private var _subRows: MutableList<Row>? = null
override val subRows: List<Row>
get() = _subRows ?: emptyList()
override var enabled: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
for (c in components) {
c.isEnabled = value
}
}
override var visible: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
for (c in components) {
c.isVisible = value
}
}
override var subRowsEnabled: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
_subRows?.forEach { it.enabled = value }
}
override var subRowsVisible: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
_subRows?.forEach { it.visible = value }
}
override operator fun JComponent.invoke(vararg constraints: CCFlags, gapLeft: Int, growPolicy: GrowPolicy?) {
addComponent(this, constraints, gapLeft = gapLeft, growPolicy = growPolicy)
}
private fun addComponent(component: Component, constraints: Array<out CCFlags>, gapLeft: Int, growPolicy: GrowPolicy?) {
if (buttonGroup != null && component is JToggleButton) {
buttonGroup.add(component)
}
createComponentConstraints(constraints, gapLeft = gapLeft, growPolicy = growPolicy)?.let {
componentConstraints.put(component, it)
}
components.add(component)
}
override fun alignRight() {
if (rightIndex != Int.MAX_VALUE) {
throw IllegalStateException("right allowed only once")
}
rightIndex = components.size
}
override fun createRow(label: String?): Row {
val row = builder.newRow(label?.let { Label(it) }, indented = true)
if (_subRows == null) {
_subRows = SmartList()
}
_subRows!!.add(row)
return row
}
}
private fun createComponentConstraints(constraints: Array<out CCFlags>? = null,
gapLeft: Int = 0,
gapAfter: Int = 0,
gapTop: Int = 0,
gapBottom: Int = 0,
split: Int = -1,
growPolicy: GrowPolicy?): CC? {
@Suppress("LocalVariableName")
var _cc = constraints?.create()
fun cc(): CC {
if (_cc == null) {
_cc = CC()
}
return _cc!!
}
if (gapLeft != 0) {
cc().horizontal.gapBefore = gapToBoundSize(gapLeft, true)
}
if (gapAfter != 0) {
cc().horizontal.gapAfter = gapToBoundSize(gapAfter, true)
}
if (gapTop != 0) {
cc().vertical.gapBefore = gapToBoundSize(gapTop, false)
}
if (gapBottom != 0) {
cc().vertical.gapAfter = gapToBoundSize(gapBottom, false)
}
if (split != -1) {
cc().split = split
}
if (growPolicy == GrowPolicy.SHORT_TEXT) {
cc().maxWidth("210")
}
else if (growPolicy == GrowPolicy.MEDIUM_TEXT) {
cc().minWidth("210")
cc().maxWidth("350")
}
return _cc
}
private fun gapToBoundSize(value: Int, isHorizontal: Boolean): BoundSize {
internal fun gapToBoundSize(value: Int, isHorizontal: Boolean): BoundSize {
val unitValue = UnitValue(value.toFloat(), "", isHorizontal, UnitValue.STATIC, null)
return BoundSize(unitValue, unitValue, null, false, null)
}
// default values differs to MigLayout - IntelliJ Platform defaults are used
// see com.intellij.uiDesigner.core.AbstractLayout.DEFAULT_HGAP and DEFAULT_VGAP (multiplied by 2 to achieve the same look (it seems in terms of MigLayout gap is both left and right space))
private fun c(insets: String? = "0", gridGapX: Int = HORIZONTAL_GAP * 2, gridGapY: Int = VERTICAL_GAP): LC {
private fun createLayoutConstraints(gridGapX: Int = HORIZONTAL_GAP * 2, gridGapY: Int = VERTICAL_GAP): LC {
// no setter for gap, so, create string to parse
val lc = LC()
lc.gridGapX = gapToBoundSize(gridGapX, true)
lc.gridGapY = gapToBoundSize(gridGapY, false)
insets?.let {
lc.insets(it)
}
lc.insets = ConstraintParser.parseInsets("0", true)
return lc
}
private fun Array<out CCFlags>.create() = if (isEmpty()) null else CC().apply(this)
internal fun Array<out CCFlags>.create() = if (isEmpty()) null else CC().apply(this)
private fun CC.apply(flags: Array<out CCFlags>): CC {
for (flag in flags) {
@@ -0,0 +1,143 @@
// Copyright 2000-2018 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.
package com.intellij.ui.layout
import com.intellij.ui.components.Label
import com.intellij.util.SmartList
import net.miginfocom.layout.CC
import java.awt.Component
import javax.swing.ButtonGroup
import javax.swing.JComponent
import javax.swing.JToggleButton
internal class MigLayoutRow(private val componentConstraints: MutableMap<Component, CC>,
override val builder: MigLayoutBuilder,
val labeled: Boolean = false,
val noGrid: Boolean = false,
private val buttonGroup: ButtonGroup? = null,
val separated: Boolean = false) : Row() {
val components = SmartList<JComponent>()
var rightIndex = Int.MAX_VALUE
internal var _subRows: MutableList<MigLayoutRow>? = null
override val subRows: List<Row>
get() = _subRows ?: emptyList()
override var enabled: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
for (c in components) {
c.isEnabled = value
}
}
override var visible: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
for (c in components) {
c.isVisible = value
}
}
override var subRowsEnabled: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
_subRows?.forEach { it.enabled = value }
}
override var subRowsVisible: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
_subRows?.forEach { it.visible = value }
}
override operator fun JComponent.invoke(vararg constraints: CCFlags, gapLeft: Int, growPolicy: GrowPolicy?) {
addComponent(this, constraints, gapLeft = gapLeft, growPolicy = growPolicy)
}
private fun addComponent(component: JComponent, constraints: Array<out CCFlags>, gapLeft: Int, growPolicy: GrowPolicy?) {
if (buttonGroup != null && component is JToggleButton) {
buttonGroup.add(component)
}
createComponentConstraints(constraints, gapLeft = gapLeft, growPolicy = growPolicy)?.let {
componentConstraints.put(component, it)
}
components.add(component)
}
override fun alignRight() {
if (rightIndex != Int.MAX_VALUE) {
throw IllegalStateException("right allowed only once")
}
rightIndex = components.size
}
override fun createRow(label: String?): Row {
if (_subRows == null) {
_subRows = SmartList()
}
return builder.newRow(rowList = _subRows!!, label = label?.let { Label(it) })
}
}
private fun createComponentConstraints(constraints: Array<out CCFlags>? = null,
gapLeft: Int = 0,
gapAfter: Int = 0,
gapTop: Int = 0,
gapBottom: Int = 0,
split: Int = -1,
growPolicy: GrowPolicy?): CC? {
@Suppress("LocalVariableName")
var _cc = constraints?.create()
fun cc(): CC {
if (_cc == null) {
_cc = CC()
}
return _cc!!
}
if (gapLeft != 0) {
cc().horizontal.gapBefore = gapToBoundSize(gapLeft, true)
}
if (gapAfter != 0) {
cc().horizontal.gapAfter = gapToBoundSize(gapAfter, true)
}
if (gapTop != 0) {
cc().vertical.gapBefore = gapToBoundSize(gapTop, false)
}
if (gapBottom != 0) {
cc().vertical.gapAfter = gapToBoundSize(gapBottom, false)
}
if (split != -1) {
cc().split = split
}
if (growPolicy == GrowPolicy.SHORT_TEXT) {
cc().maxWidth("210")
}
else if (growPolicy == GrowPolicy.MEDIUM_TEXT) {
cc().minWidth("210")
cc().maxWidth("350")
}
return _cc
}
@@ -28,6 +28,8 @@ import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JLabel
internal const val COMPONENT_TAG_HINT = "kotlin.dsl.hint.component"
abstract class Row {
abstract var enabled: Boolean
@@ -41,8 +43,10 @@ abstract class Row {
protected abstract val builder: LayoutBuilderImpl
fun label(text: String, gapLeft: Int = 0, style: ComponentStyle? = null, fontColor: FontColor? = null, bold: Boolean = false) {
Label(text, style, fontColor, bold)(gapLeft = gapLeft)
fun label(text: String, gapLeft: Int = 0, style: ComponentStyle? = null, fontColor: FontColor? = null, bold: Boolean = false): JLabel {
val label = Label(text, style, fontColor, bold)
label(gapLeft = gapLeft)
return label
}
fun link(text: String, style: ComponentStyle? = null, action: () -> Unit) {
@@ -91,7 +95,8 @@ abstract class Row {
}
fun hint(text: String) {
label(text, style = ComponentStyle.SMALL, fontColor = FontColor.BRIGHTER, gapLeft = 3 * HORIZONTAL_GAP)
val component = label(text, style = ComponentStyle.SMALL, fontColor = FontColor.BRIGHTER)
component.putClientProperty(COMPONENT_TAG_HINT, true)
}
fun panel(title: String, wrappedComponent: Component, vararg constraints: CCFlags) {
@@ -116,7 +121,6 @@ abstract class Row {
return row
}
inline fun row(init: Row.() -> Unit): Row {
val row = createRow(null)
row.init()
@@ -14,7 +14,7 @@ internal const val VERTICAL_GAP = 5
fun createLayoutBuilder() = LayoutBuilder(MigLayoutBuilder())
interface LayoutBuilderImpl {
fun newRow(label: JLabel? = null, buttonGroup: ButtonGroup? = null, separated: Boolean = false, indented: Boolean = false): Row
fun newRow(label: JLabel? = null, buttonGroup: ButtonGroup? = null, separated: Boolean = false): Row
fun build(container: Container, layoutConstraints: Array<out LCFlags>)
@@ -36,5 +36,6 @@
<orderEntry type="library" scope="TEST" name="fest-swing" level="project" />
<orderEntry type="library" scope="TEST" name="assertj-swing" level="project" />
<orderEntry type="library" scope="TEST" name="jackson" level="project" />
<orderEntry type="library" scope="TEST" name="snakeyaml" level="project" />
</component>
</module>
@@ -0,0 +1,26 @@
rowConstraints: ''
columnConstraints:
count: 2.0
constraints:
- {}
- {grow: 100.0}
componentConstrains:
'JRadioButton #0': {spanX: 2097051.0, wrap: true}
'JLabel #1':
horizontal:
gapBefore:
min: {value: 30.0}
preferred: {value: 30.0}
'JTextField #2': {split: 2097051.0}
'JLabel #3': {wrap: true}
'JLabel #4':
horizontal:
gapBefore:
min: {value: 30.0}
preferred: {value: 30.0}
'JBPasswordField #5':
wrap: true
horizontal:
size:
max: {value: 210.0}
'JBLabel #6': {skip: 1.0, spanX: 2097051.0, wrap: true}
@@ -0,0 +1,11 @@
rowConstraints: ''
columnConstraints:
count: 2.0
constraints:
- {}
- {grow: 100.0}
componentConstrains:
'JLabel #0': {}
'JCheckBox #1': {wrap: true}
'JLabel #2': {}
'JTextField #3': {wrap: true}
@@ -1,8 +1,13 @@
// Copyright 2000-2018 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.
package com.intellij.ui.layout
import com.intellij.ui.components.CheckBox
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.ui.components.JBPasswordField
import com.intellij.ui.components.RadioButton
import net.miginfocom.layout.LayoutUtil
import net.miginfocom.swing.MigLayout
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import javax.swing.JFrame
@@ -13,21 +18,52 @@ object MigLayoutTestApp {
fun main(args: Array<String>) {
LayoutUtil.setGlobalDebugMillis(1000)
val androidModuleNameComponent = JTextField("input")
val androidCheckBox = CheckBox("Android module name:")
val panel = panel {
row("Create Android module") { androidCheckBox() }
row("Android module name:") { androidModuleNameComponent() }
row { label("Save passwords:") }
buttonGroup {
row {
RadioButton("In KeePass")()
row("Database:") {
JTextField()()
gearButton(
object : AnAction("Clear") {
override fun actionPerformed(event: AnActionEvent) {
}
},
object : AnAction("Import") {
override fun actionPerformed(event: AnActionEvent) {
}
}
)
}
row("Master Password:") {
JBPasswordField()(growPolicy = GrowPolicy.SHORT_TEXT)
}
row {
hint("Stored using weak encryption.")
}
}
row {
RadioButton("Do not save, forget passwords after restart")()
}
row {
hint("Existing KeePass file will be removed.")
}
}
}
val frame = JFrame()
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
frame.contentPane = panel
frame.contentPane.add(panel, BorderLayout.CENTER)
frame.contentPane.background = Color.WHITE
frame.background = Color.WHITE
frame.pack()
frame.setLocationRelativeTo(null)
frame.minimumSize = Dimension(512, 256)
frame.isVisible = true
System.out.println(configurationToJson(panel, panel.layout as MigLayout, false))
}
}
@@ -1,29 +1,32 @@
// Copyright 2000-2018 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.
package com.intellij.ui.layout
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.gson.GsonBuilder
import com.intellij.openapi.application.invokeAndWaitIfNeed
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.ui.components.CheckBox
import com.intellij.ui.components.JBPasswordField
import com.intellij.ui.components.RadioButton
import com.intellij.util.io.exists
import com.intellij.util.io.outputStream
import com.intellij.util.io.sanitizeFileName
import com.intellij.util.io.write
import net.miginfocom.layout.Grid
import net.miginfocom.layout.LayoutUtil
import net.miginfocom.swing.MigLayout
import org.assertj.core.data.Offset
import org.assertj.swing.assertions.Assertions.assertThat
import org.assertj.swing.edt.FailOnThreadViolationRepaintManager
import org.assertj.swing.edt.GuiActionRunner
import org.assertj.swing.fixture.FrameFixture
import org.intellij.lang.annotations.Language
import org.junit.*
import org.junit.After
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import java.awt.*
import java.awt.image.BufferedImage
import java.io.StringReader
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.Callable
import javax.imageio.ImageIO
import javax.swing.JFrame
@@ -48,33 +51,6 @@ class UiDslTest {
@JvmField
val testName = TestName()
@Before
fun setUp() {
val frame = GuiActionRunner.execute(Callable {
LayoutUtil.setGlobalDebugMillis(1000)
val androidModuleNameComponent = JTextField("input")
val androidCheckBox = CheckBox("Android module name:")
val panel = panel {
row("Create Android module") { androidCheckBox() }
row("Android module name:") { androidModuleNameComponent() }
}
panel.background = Color.WHITE
panel.name = "test"
val frame = JFrame("wrapper")
frame.isUndecorated = true
frame.contentPane.background = Color.WHITE
frame.background = Color.WHITE
frame.contentPane.add(panel, BorderLayout.CENTER)
frame.minimumSize = Dimension(512, 256)
frame
})
window = FrameFixture(frame)
window.show()
}
@After
fun tearDown() {
window.cleanUp()
@@ -90,171 +66,102 @@ class UiDslTest {
return window.target()
}
private fun assertLayout(@Language("JSON") jsonData: String) {
val component = window.panel("test").target()
assertThat(configurationToJson(component, (component as JPanel).layout as MigLayout, false)).isEqualTo(jsonData.trimIndent())
@Test
fun `align fields in the nested grid`() {
doTest(panel {
buttonGroup {
row {
RadioButton("In KeePass")()
row("Database:") {
JTextField()()
gearButton()
}
row("Master Password:") {
JBPasswordField()(growPolicy = GrowPolicy.SHORT_TEXT)
}
row {
hint("Stored using weak encryption.")
}
}
}
}, "[0, 0, 512, 23], [0, 28, 139, 26], [159, 28, 353, 26], [159, 28, 353, 26], [0, 59, 139, 26], [159, 59, 353, 26], [159, 90, 353, 14]")
}
@Test
fun test() {
val component = window.panel("test").target()
val layout = (component as JPanel).layout as MigLayout
assertLayout("""
{
"rowConstraints": "",
"columnConstraints": {
"count": 3.0,
"constraints": [
{},
{
"grow": 100.0
},
{
"grow": 100.0
}
]
},
"componentConstrains": {
"JLabel #0": {},
"JCheckBox #1": {
"wrap": true
},
"JLabel #2": {},
"JTextField #3": {
"wrap": true
}
fun `align fields`() {
doTest(panel {
row("Create Android module") { CheckBox("Android module name:")() }
row("Android module name:") { JTextField("input")() }
}, "[0, 0, 145, 23], [165, 0, 347, 23], [0, 28, 145, 26], [165, 28, 347, 26]")
}
}""")
val gridField = MigLayout::class.java.getDeclaredField("grid")
gridField.isAccessible = true
val grid = gridField.get(layout) as Grid
val rectangles = MigLayoutTestUtil.getRectangles(grid)
private fun doTest(panel: JPanel, expectedLocations: String) {
val frame = GuiActionRunner.execute(Callable {
LayoutUtil.setGlobalDebugMillis(1000)
panel.background = Color.WHITE
panel.name = "test"
val frame = JFrame("wrapper")
frame.isUndecorated = true
frame.contentPane.background = Color.WHITE
frame.background = Color.WHITE
frame.contentPane.add(panel, BorderLayout.CENTER)
frame.minimumSize = Dimension(512, 256)
frame
})
window = FrameFixture(frame)
window.show()
val component = window.panel("test").target() as JPanel
val layout = component.layout as MigLayout
val imageName = sanitizeFileName(testName.methodName)
val actualLayoutJson = configurationToJson(component, component.layout as MigLayout, false)
try {
assertThat(rectangles.joinToString(",") { "[${it.joinToString(",")}]" }).isEqualTo(
"[0,0,145,23],[165,0,251,23],[0,28,145,26],[165,28,251,26]")
val expectedLayoutDataFile = Paths.get(PlatformTestUtil.getPlatformTestDataPath(), "ui", "layout", "$imageName.yml")
if (expectedLayoutDataFile.exists()) {
com.intellij.testFramework.assertions.Assertions.assertThat(actualLayoutJson).isEqualTo(expectedLayoutDataFile)
}
else {
expectedLayoutDataFile.write(actualLayoutJson)
}
val gridField = MigLayout::class.java.getDeclaredField("grid")
gridField.isAccessible = true
val grid = gridField.get(layout) as Grid
val rectangles = MigLayoutTestUtil.getRectangles(grid)
assertThat(rectangles.joinToString(", ") { "[${it.joinToString(", ")}]" }).isEqualTo(expectedLocations)
if (imageDir.isNullOrEmpty()) {
return
}
val imagePath = Paths.get(imageDir, "${sanitizeFileName(testName.methodName)}.png")
val imagePath = Paths.get(imageDir, "$imageName.png")
if (!imagePath.exists()) {
System.out.println("Write a new snapshot image ${imagePath.fileName}")
saveImage(imagePath)
return
}
assertThat(componentToImage(getContentPane())).isEqualTo(ImageIO.read(imagePath.toFile()))
assertThat(componentToImage(getContentPane())).isEqualTo(ImageIO.read(imagePath.toFile()), Offset.offset(32))
}
catch (e: AssertionError) {
if (!imageDir.isNullOrEmpty()) {
saveImage(Paths.get(imageDir, "${sanitizeFileName(testName.methodName)}-NEW.png"))
Paths.get(imageDir, "$imageName-NEW.yml").write(actualLayoutJson)
saveImage(Paths.get(imageDir, "$imageName-NEW.png"))
}
throw e
}
}
@Suppress("UNCHECKED_CAST")
private fun configurationToJson(component: JPanel, layout: MigLayout, isIncludeLayoutConstraints: Boolean): String {
val objectMapper = ObjectMapper()
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT)
val componentConstrains = LinkedHashMap<String, Any>()
for ((index, c) in component.components.withIndex()) {
componentConstrains.put("${c.javaClass.simpleName} #${index}", layout.getComponentConstraints(c))
}
val json = objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(linkedMapOf(
"layoutConstraints" to if (isIncludeLayoutConstraints) layout.layoutConstraints else null,
"rowConstraints" to layout.rowConstraints,
"columnConstraints" to layout.columnConstraints,
"componentConstrains" to componentConstrains
))
// *** *** jackson has ugly API and not clear how to write custom filter, so, GSON is used
val gson = GsonBuilder().setPrettyPrinting().create()
val map = gson.fromJson(StringReader(json), MutableMap::class.java)
@Suppress("UNCHECKED_CAST")
for (cc in (map.get("componentConstrains") as MutableMap<String, MutableMap<String, Any?>>).values) {
removeDefaultCc(cc)
cc.remove("animSpec")
for (name in arrayOf("horizontal", "vertical")) {
val p = cc.get(name) as MutableMap<String, Any?>? ?: continue
val size = p.get("size") as MutableMap<*, *>?
if (size != null && size.get("unset") == true) {
size.remove("unset")
if (size.isEmpty()) {
p.remove("size")
}
}
removeDefaultCc(p)
if (p.isEmpty()) {
cc.remove(name)
}
}
}
val columnConstraints = map.get("columnConstraints") as MutableMap<String, Any>
val acList = columnConstraints.remove("constaints")!! as List<MutableMap<String, Any>>
columnConstraints.put("constraints", acList)
for (ac in acList) {
val size = ac.get("size") as MutableMap<*, *>?
if (size != null && size.get("unset") == true) {
size.remove("unset")
if (size.isEmpty()) {
ac.remove("size")
}
if (ac.get("shrinkPriority") == 100.0) {
ac.remove("shrinkPriority")
}
if (ac.get("shrink") == 100.0) {
ac.remove("shrink")
}
if (ac.get("growPriority") == 100.0) {
ac.remove("growPriority")
}
}
}
return gson.toJson(map)
}
private fun removeDefaultCc(cc: MutableMap<String, Any?>) {
for ((name, value) in DEFAULT_CC) {
if (cc.get(name) == value) {
cc.remove(name)
}
}
}
}
private fun componentToImage(component: Component): BufferedImage {
// we don't need retina image
val image = BufferedImage(component.width, component.height, BufferedImage.TYPE_BYTE_GRAY)
val g = image.graphics
component.paint(g)
g.dispose()
return image
}
private val DEFAULT_CC = mapOf(
"dockSide" to -1.0,
"split" to 1.0,
"spanX" to 1.0,
"spanY" to 1.0,
"cellX" to -1.0,
"cellY" to -1.0,
"hideMode" to -1.0,
"growPriority" to 100.0,
"shrinkPriority" to 100.0,
"shrink" to 100.0,
"grow" to 100.0,
"boundsInGrid" to true,
"" to ""
)
private fun componentToImage(component: Component, type: Int = BufferedImage.TYPE_BYTE_GRAY): BufferedImage {
return invokeAndWaitIfNeed {
// we don't need retina image
val image = BufferedImage(component.width, component.height, type)
val g = image.graphics
component.paint(g)
g.dispose()
image
}
}
@@ -0,0 +1,130 @@
// Copyright 2000-2018 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.
package com.intellij.ui.layout
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.gson.GsonBuilder
import com.intellij.testFramework.assertions.dumpData
import net.miginfocom.swing.MigLayout
import java.io.StringReader
import java.util.*
import javax.swing.JPanel
@Suppress("UNCHECKED_CAST")
fun configurationToJson(component: JPanel, layout: MigLayout, isIncludeLayoutConstraints: Boolean): String {
val objectMapper = ObjectMapper()
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT)
val componentConstrains = LinkedHashMap<String, Any>()
for ((index, c) in component.components.withIndex()) {
componentConstrains.put("${c.javaClass.simpleName} #${index}", layout.getComponentConstraints(c))
}
val json = objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(linkedMapOf(
"layoutConstraints" to if (isIncludeLayoutConstraints) layout.layoutConstraints else null,
"rowConstraints" to layout.rowConstraints,
"columnConstraints" to layout.columnConstraints,
"componentConstrains" to componentConstrains
))
// *** *** jackson has ugly API and not clear how to write custom filter, so, GSON is used
val gson = GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.create()
val map = gson.fromJson(StringReader(json), MutableMap::class.java)
@Suppress("UNCHECKED_CAST")
for (cc in (map.get("componentConstrains") as MutableMap<String, MutableMap<String, Any?>>).values) {
removeDefaultCc(cc)
cc.remove("animSpec")
for (axisName in arrayOf("horizontal", "vertical")) {
val p = cc.get(axisName) as MutableMap<String, Any?>? ?: continue
val size = p.get("size") as MutableMap<*, *>?
if (size != null && size.get("unset") == true) {
size.remove("unset")
if (size.isEmpty()) {
p.remove("size")
}
}
for (name2 in arrayOf("size", "gapBefore", "gapAfter")) {
val v = p.get(name2) as? MutableMap<*, *>? ?: continue
for (name in arrayOf("min", "max", "preferred")) {
val p2 = v.get(name) as? MutableMap<*, *> ?: continue
if (p2.get("unit") == 1.0) {
p2.remove("unit")
}
if (p2.get("operation") == 100.0) {
p2.remove("operation")
}
if (p2.get(axisName) == true) {
p2.remove(axisName)
}
if (p2.isEmpty()) {
v.remove(name)
}
}
}
removeDefaultCc(p)
if (p.isEmpty()) {
cc.remove(axisName)
}
}
}
(map.get("columnConstraints") as? MutableMap<String, Any>)?.let { cleanupColumnConstraints(it) }
return dumpData(map)
}
@Suppress("UNCHECKED_CAST")
private fun cleanupColumnConstraints(columnConstraints: MutableMap<String, Any>) {
val acList = columnConstraints.remove("constaints")!! as List<MutableMap<String, Any>>
columnConstraints.put("constraints", acList)
for (ac in acList) {
val size = ac.get("size") as MutableMap<*, *>?
if (size != null && size.get("unset") == true) {
size.remove("unset")
if (size.isEmpty()) {
ac.remove("size")
}
if (ac.get("shrinkPriority") == 100.0) {
ac.remove("shrinkPriority")
}
if (ac.get("shrink") == 100.0) {
ac.remove("shrink")
}
if (ac.get("growPriority") == 100.0) {
ac.remove("growPriority")
}
}
}
}
private fun removeDefaultCc(cc: MutableMap<String, Any?>) {
for ((name, value) in DEFAULT_CC) {
if (cc.get(name) == value) {
cc.remove(name)
}
}
}
private val DEFAULT_CC = mapOf(
"dockSide" to -1.0,
"split" to 1.0,
"spanX" to 1.0,
"spanY" to 1.0,
"cellX" to -1.0,
"cellY" to -1.0,
"hideMode" to -1.0,
"growPriority" to 100.0,
"shrinkPriority" to 100.0,
"shrink" to 100.0,
"grow" to 100.0,
"boundsInGrid" to true,
"" to ""
)
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2000-2018 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.
package com.intellij.testFramework.assertions
import com.intellij.openapi.util.text.StringUtilRt
@@ -25,7 +11,6 @@ import org.yaml.snakeyaml.nodes.Node
import org.yaml.snakeyaml.nodes.Tag
import org.yaml.snakeyaml.representer.Represent
import org.yaml.snakeyaml.representer.Representer
import java.io.StringWriter
import java.nio.file.Path
import java.util.regex.Pattern
@@ -37,14 +22,12 @@ class ListAssertEx<ELEMENT>(actual: List<ELEMENT>) : ListAssert<ELEMENT>(actual)
}
}
private fun dumpData(data: Any): String {
fun dumpData(data: Any): String {
val dumperOptions = DumperOptions()
dumperOptions.isAllowReadOnlyProperties = true
dumperOptions.lineBreak = DumperOptions.LineBreak.UNIX
val yaml = Yaml(DumpRepresenter(), dumperOptions)
val writer = StringWriter()
yaml.dump(data, writer)
return writer.toString()
return yaml.dump(data)
}
private class DumpRepresenter : Representer() {
@@ -59,7 +42,7 @@ private class DumpRepresenter : Representer() {
internal fun compareFileContent(actual: Any, snapshotFile: Path) {
val expectedContent = StringUtilRt.convertLineSeparators(snapshotFile.readText())
val actualContent = dumpData(actual)
val actualContent = if (actual is String) actual else dumpData(actual)
if (actualContent != expectedContent) {
throw FileComparisonFailure(null, expectedContent, actualContent, snapshotFile.toString())
}