diff --git a/platform/credential-store/src/PasswordSafeConfigurable.kt b/platform/credential-store/src/PasswordSafeConfigurable.kt index bfa15dfa5201..f1c2cf2ef97e 100644 --- a/platform/credential-store/src/PasswordSafeConfigurable.kt +++ b/platform/credential-store/src/PasswordSafeConfigurable.kt @@ -145,7 +145,7 @@ internal class PasswordSafeConfigurableUi : ConfigurableUi keePassMasterPassword.setPasswordIsStored(true) - val panel = panel { + return panel { row { label("Save passwords:") } buttonGroup({ updateEnabledState() }) { @@ -165,46 +165,46 @@ internal class PasswordSafeConfigurableUi : ConfigurableUi 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 } } } - - return panel } private fun getProviderType(): ProviderType { diff --git a/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutBuilder.kt b/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutBuilder.kt index ce69cd10303c..08096428b3be 100644 --- a/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutBuilder.kt +++ b/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutBuilder.kt @@ -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 = 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, 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) { - 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, 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, - 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() - var rightIndex = Int.MAX_VALUE - - private var _subRows: MutableList? = null - - override val subRows: List - 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, 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? = 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.create() = if (isEmpty()) null else CC().apply(this) +internal fun Array.create() = if (isEmpty()) null else CC().apply(this) private fun CC.apply(flags: Array): CC { for (flag in flags) { diff --git a/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutRow.kt b/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutRow.kt new file mode 100644 index 000000000000..f3a2450ab50d --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutRow.kt @@ -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, + 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() + var rightIndex = Int.MAX_VALUE + + internal var _subRows: MutableList? = null + + override val subRows: List + 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, 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? = 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 +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/ui/layout/Row.kt b/platform/platform-impl/src/com/intellij/ui/layout/Row.kt index 70b0f87163d3..111d2ac63bde 100644 --- a/platform/platform-impl/src/com/intellij/ui/layout/Row.kt +++ b/platform/platform-impl/src/com/intellij/ui/layout/Row.kt @@ -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() diff --git a/platform/platform-impl/src/com/intellij/ui/layout/layoutImpl.kt b/platform/platform-impl/src/com/intellij/ui/layout/layoutImpl.kt index e1ed95f69e2f..1f60124fee75 100644 --- a/platform/platform-impl/src/com/intellij/ui/layout/layoutImpl.kt +++ b/platform/platform-impl/src/com/intellij/ui/layout/layoutImpl.kt @@ -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) diff --git a/platform/platform-tests/intellij.platform.tests.iml b/platform/platform-tests/intellij.platform.tests.iml index 7ff9ebd0e731..8707b2c850bb 100644 --- a/platform/platform-tests/intellij.platform.tests.iml +++ b/platform/platform-tests/intellij.platform.tests.iml @@ -36,5 +36,6 @@ + \ No newline at end of file diff --git a/platform/platform-tests/testData/ui/layout/align fields in the nested grid.yml b/platform/platform-tests/testData/ui/layout/align fields in the nested grid.yml new file mode 100644 index 000000000000..9a70c60b9423 --- /dev/null +++ b/platform/platform-tests/testData/ui/layout/align fields in the nested grid.yml @@ -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} diff --git a/platform/platform-tests/testData/ui/layout/align fields.yml b/platform/platform-tests/testData/ui/layout/align fields.yml new file mode 100644 index 000000000000..9c3f88c0c4e1 --- /dev/null +++ b/platform/platform-tests/testData/ui/layout/align fields.yml @@ -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} diff --git a/platform/platform-tests/testSrc/com/intellij/ui/layout/MigLayoutTestApp.kt b/platform/platform-tests/testSrc/com/intellij/ui/layout/MigLayoutTestApp.kt index 8e5256569567..d86657293345 100644 --- a/platform/platform-tests/testSrc/com/intellij/ui/layout/MigLayoutTestApp.kt +++ b/platform/platform-tests/testSrc/com/intellij/ui/layout/MigLayoutTestApp.kt @@ -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) { 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)) } } \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/ui/layout/UiDslTest.kt b/platform/platform-tests/testSrc/com/intellij/ui/layout/UiDslTest.kt index 4edd0fb34dc7..d998af3341e0 100644 --- a/platform/platform-tests/testSrc/com/intellij/ui/layout/UiDslTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/ui/layout/UiDslTest.kt @@ -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() - 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>).values) { - removeDefaultCc(cc) - cc.remove("animSpec") - - for (name in arrayOf("horizontal", "vertical")) { - val p = cc.get(name) as MutableMap? ?: 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 - val acList = columnConstraints.remove("constaints")!! as List> - 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) { - 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 "" -) \ No newline at end of file +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 + } +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/ui/layout/migLayoutDebugDumper.kt b/platform/platform-tests/testSrc/com/intellij/ui/layout/migLayoutDebugDumper.kt new file mode 100644 index 000000000000..e5d1a158047c --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/ui/layout/migLayoutDebugDumper.kt @@ -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() + 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>).values) { + removeDefaultCc(cc) + cc.remove("animSpec") + + for (axisName in arrayOf("horizontal", "vertical")) { + val p = cc.get(axisName) as MutableMap? ?: 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)?.let { cleanupColumnConstraints(it) } + return dumpData(map) +} + +@Suppress("UNCHECKED_CAST") +private fun cleanupColumnConstraints(columnConstraints: MutableMap) { + val acList = columnConstraints.remove("constaints")!! as List> + 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) { + 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 "" +) \ No newline at end of file diff --git a/platform/testFramework/extensions/src/com/intellij/testFramework/assertions/snapshot.kt b/platform/testFramework/extensions/src/com/intellij/testFramework/assertions/snapshot.kt index 22890729a81c..5d1ea92098fc 100644 --- a/platform/testFramework/extensions/src/com/intellij/testFramework/assertions/snapshot.kt +++ b/platform/testFramework/extensions/src/com/intellij/testFramework/assertions/snapshot.kt @@ -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(actual: List) : ListAssert(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()) }