diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateCleanArchitectureFeatureDialog.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateCleanArchitectureFeatureDialog.kt index 7530724..10f5a0b 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateCleanArchitectureFeatureDialog.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateCleanArchitectureFeatureDialog.kt @@ -1,15 +1,15 @@ package com.mitteloupe.cag.cleanarchitecturegenerator import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.panel import com.mitteloupe.cag.cleanarchitecturegenerator.form.OnChangeDocumentListener import com.mitteloupe.cag.cleanarchitecturegenerator.form.PredicateDocumentFilter import java.io.File -import javax.swing.JCheckBox import javax.swing.JComponent import javax.swing.text.AbstractDocument @@ -23,9 +23,6 @@ class CreateCleanArchitectureFeatureDialog( private val featureNameTextField = JBTextField() private val featurePackageTextField = JBTextField() private var lastFeatureName: String = PLACEHOLDER - private val appModuleComboBox = ComboBox(appModuleDirectories.map { it.name }.toTypedArray()) - private val ktlintCheckBox = JCheckBox("ktlint") - private val detektCheckBox = JCheckBox("detekt") val featureName: String get() = featureNameTextField.text @@ -33,24 +30,27 @@ class CreateCleanArchitectureFeatureDialog( val featurePackageName: String get() = featurePackageTextField.text.trim() + private var appModuleSelectedIndex: Int = 0 + val selectedAppModuleDirectory: File? get() = if (appModuleDirectories.isEmpty()) { null } else { - val selectedIndex = appModuleComboBox.selectedIndex - if (selectedIndex in appModuleDirectories.indices) { - appModuleDirectories[selectedIndex] + if (appModuleSelectedIndex in appModuleDirectories.indices) { + appModuleDirectories[appModuleSelectedIndex] } else { null } } + private var enableKtlintInternal: Boolean = false val enableKtlint: Boolean - get() = ktlintCheckBox.isSelected + get() = enableKtlintInternal + private var enableDetektInternal: Boolean = false val enableDetekt: Boolean - get() = detektCheckBox.isSelected + get() = enableDetektInternal init { title = CleanArchitectureGeneratorBundle.message("info.feature.generator.title") @@ -101,7 +101,12 @@ class CreateCleanArchitectureFeatureDialog( panel { if (appModuleDirectories.size >= 2) { row(CleanArchitectureGeneratorBundle.message("dialog.feature.app.module.label")) { - cell(appModuleComboBox) + val appModules = appModuleDirectories.map { it.name } + comboBox(appModules, null) + .bindItem( + getter = { appModules[appModuleSelectedIndex] }, + setter = { appModuleSelectedIndex = it?.let(appModules::indexOf) ?: 0 } + ) } } row(CleanArchitectureGeneratorBundle.message("dialog.feature.name.label")) { @@ -111,8 +116,8 @@ class CreateCleanArchitectureFeatureDialog( cell(featurePackageTextField) } row(CleanArchitectureGeneratorBundle.message("dialog.feature.code.quality.label")) { - cell(ktlintCheckBox) - cell(detektCheckBox) + checkBox("ktlint").bindSelected(::enableKtlintInternal) + checkBox("detekt").bindSelected(::enableDetektInternal) } } diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateCleanArchitecturePackageDialog.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateCleanArchitecturePackageDialog.kt index 638e985..2c63009 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateCleanArchitecturePackageDialog.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateCleanArchitecturePackageDialog.kt @@ -3,16 +3,16 @@ package com.mitteloupe.cag.cleanarchitecturegenerator import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.panel -import javax.swing.JCheckBox import javax.swing.JComponent class CreateCleanArchitecturePackageDialog( project: Project ) : DialogWrapper(project) { - private val enableComposeCheckBox = JCheckBox(CleanArchitectureGeneratorBundle.message("dialog.architecture.compose.label"), true) - private val enableKtlintCheckBox = JCheckBox(CleanArchitectureGeneratorBundle.message("dialog.architecture.ktlint.label"), false) - private val enableDetektCheckBox = JCheckBox(CleanArchitectureGeneratorBundle.message("dialog.architecture.detekt.label"), false) + private var enableCompose: Boolean = true + private var enableKtlint: Boolean = false + private var enableDetekt: Boolean = false init { title = CleanArchitectureGeneratorBundle.message("info.architecture.generator.title") @@ -22,21 +22,24 @@ class CreateCleanArchitecturePackageDialog( override fun createCenterPanel(): JComponent = panel { row { - cell(enableComposeCheckBox) + checkBox(CleanArchitectureGeneratorBundle.message("dialog.architecture.compose.label")) + .bindSelected(::enableCompose) } row { - cell(enableKtlintCheckBox) + checkBox(CleanArchitectureGeneratorBundle.message("dialog.architecture.ktlint.label")) + .bindSelected(::enableKtlint) } row { - cell(enableDetektCheckBox) + checkBox(CleanArchitectureGeneratorBundle.message("dialog.architecture.detekt.label")) + .bindSelected(::enableDetekt) } } override fun doValidate(): ValidationInfo? = null - fun isComposeEnabled(): Boolean = enableComposeCheckBox.isSelected + fun isComposeEnabled(): Boolean = enableCompose - fun isKtlintEnabled(): Boolean = enableKtlintCheckBox.isSelected + fun isKtlintEnabled(): Boolean = enableKtlint - fun isDetektEnabled(): Boolean = enableDetektCheckBox.isSelected + fun isDetektEnabled(): Boolean = enableDetekt } diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateDataSourceDialog.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateDataSourceDialog.kt index cbc27ae..e8e35e0 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateDataSourceDialog.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateDataSourceDialog.kt @@ -3,16 +3,13 @@ package com.mitteloupe.cag.cleanarchitecturegenerator import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo -import com.intellij.ui.components.JBCheckBox -import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.panel import com.intellij.util.ui.UIUtil import com.mitteloupe.cag.cleanarchitecturegenerator.form.PredicateDocumentFilter -import javax.swing.Box -import javax.swing.BoxLayout -import javax.swing.JComponent -import javax.swing.JPanel +import java.awt.EventQueue.invokeLater import javax.swing.text.AbstractDocument private const val DATA_SOURCE_SUFFIX = "DataSource" @@ -20,60 +17,52 @@ private const val DATA_SOURCE_SUFFIX = "DataSource" class CreateDataSourceDialog( project: Project? ) : DialogWrapper(project) { - private val dataSourceNameTextField = JBTextField() - private val ktorCheckBox = JBCheckBox("Add Ktor dependencies") - private val retrofitCheckBox = JBCheckBox("Add Retrofit dependencies") + private lateinit var dataSourceNameTextField: JBTextField val dataSourceNameWithSuffix: String get() = "$dataSourceName$DATA_SOURCE_SUFFIX" - private val dataSourceName: String - get() = dataSourceNameTextField.text.trim() + private var dataSourceName: String = "" + private var useKtorInternal: Boolean = false val useKtor: Boolean - get() = ktorCheckBox.isSelected + get() = useKtorInternal + private var useRetrofitInternal: Boolean = false val useRetrofit: Boolean - get() = retrofitCheckBox.isSelected + get() = useRetrofitInternal init { title = CleanArchitectureGeneratorBundle.message("info.datasource.generator.title") init() - - dataSourceNameTextField.columns = 20 - - (dataSourceNameTextField.document as AbstractDocument).documentFilter = - PredicateDocumentFilter { !it.isWhitespace() } } - override fun getPreferredFocusedComponent(): JComponent = dataSourceNameTextField - - override fun createCenterPanel(): JComponent { - val suffixLabel = - JBLabel(DATA_SOURCE_SUFFIX).apply { - foreground = UIUtil.getLabelDisabledForeground() - } - - val nameWithSuffixPanel = - JPanel().apply { - layout = BoxLayout(this, BoxLayout.X_AXIS) - add(dataSourceNameTextField) - add(Box.createHorizontalStrut(4)) - add(suffixLabel) - } - - return panel { + override fun createCenterPanel() = + panel { row(CleanArchitectureGeneratorBundle.message("dialog.datasource.name.label")) { - cell(nameWithSuffixPanel) + textField() + .bindText({ dataSourceName }, { dataSourceName = it }) + .applyToComponent { + (document as AbstractDocument).documentFilter = + PredicateDocumentFilter { !it.isWhitespace() } + dataSourceNameTextField = this + } + label(DATA_SOURCE_SUFFIX) + .applyToComponent { + foreground = UIUtil.getLabelDisabledForeground() + } } row { - cell(ktorCheckBox) + checkBox("Add Ktor dependencies") + .bindSelected(::useKtorInternal) } row { - cell(retrofitCheckBox) + checkBox("Add Retrofit dependencies") + .bindSelected(::useRetrofitInternal) } + }.apply { + invokeLater { dataSourceNameTextField.requestFocusInWindow() } } - } override fun doValidate(): ValidationInfo? = if (dataSourceName.isEmpty()) { diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateUseCaseDialog.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateUseCaseDialog.kt index d5e45a8..d2845f8 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateUseCaseDialog.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateUseCaseDialog.kt @@ -1,28 +1,26 @@ package com.mitteloupe.cag.cleanarchitecturegenerator import com.intellij.icons.AllIcons -import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogPanel import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBTextField -import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.whenItemChangedFromUi import com.intellij.util.ui.UIUtil -import com.mitteloupe.cag.cleanarchitecturegenerator.form.OnChangeDocumentListener import com.mitteloupe.cag.cleanarchitecturegenerator.form.PredicateDocumentFilter import com.mitteloupe.cag.cleanarchitecturegenerator.validation.SymbolValidator -import java.awt.event.FocusAdapter -import java.awt.event.FocusEvent +import java.awt.EventQueue.invokeLater import java.io.File import javax.swing.DefaultComboBoxModel import javax.swing.JComponent -import javax.swing.SwingUtilities +import javax.swing.JLabel +import javax.swing.JTextField +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener import javax.swing.text.AbstractDocument private const val USE_CASE_SUFFIX = "UseCase" @@ -30,95 +28,122 @@ private const val DEFAULT_USE_CASE_NAME = "DoSomething" private const val DEFAULT_DATA_TYPE = "Unit" class CreateUseCaseDialog( - project: Project?, + private val project: Project?, suggestedDirectory: File? ) : DialogWrapper(project) { - private val useCaseNameTextField = JBTextField() - private val directoryField = TextFieldWithBrowseButton() - private val inputDataTypeComboBox = ComboBox() - private val inputWarningLabel = JBLabel() - private val outputDataTypeComboBox = ComboBox() - private val outputWarningLabel = JBLabel() + private val initialDirectory = suggestedDirectory?.absolutePath.orEmpty() + private lateinit var useCaseNameTextField: JComponent + private var useCaseNameText: String = "" + private var directoryPath: String = initialDirectory + private lateinit var inputDataTypeComboBox: ComboBox + private lateinit var inputWarningLabel: JLabel + private lateinit var outputDataTypeComboBox: ComboBox + private lateinit var outputWarningLabel: JLabel private val modelClassFinder = ModelClassFinder() private val symbolValidator = SymbolValidator() - - private var documentListenersSetup = false + private val inputDataTypeModel = DefaultComboBoxModel() + private val outputDataTypeModel = DefaultComboBoxModel() val useCaseNameWithSuffix: String get() = useCaseName.removeSuffix(USE_CASE_SUFFIX) + USE_CASE_SUFFIX private val useCaseName: String - get() = useCaseNameTextField.text.trim() + get() = useCaseNameText.trim() + private var _inputDataType: String? = null val inputDataType: String? - get() = (inputDataTypeComboBox.selectedItem as? String)?.trim()?.takeIf { it.isNotEmpty() } + get() = _inputDataType + private var _outputDataType: String? = null val outputDataType: String? - get() = (outputDataTypeComboBox.selectedItem as? String)?.trim()?.takeIf { it.isNotEmpty() } + get() = _outputDataType val destinationDirectory: File? - get() = directoryField.text.trim().takeIf { it.isNotEmpty() }?.let { File(it) } + get() = directoryPath.trim().takeIf { it.isNotEmpty() }?.let { File(it) } init { title = CleanArchitectureGeneratorBundle.message("info.usecase.generator.title") init() - useCaseNameTextField.columns = 20 - directoryField.text = suggestedDirectory?.absolutePath.orEmpty() - directoryField.addActionListener { - val descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor() - val initialDirectory = LocalFileSystem.getInstance().findFileByIoFile(File(directoryField.text)) - val chosen = FileChooser.chooseFile(descriptor, project, initialDirectory) - if (chosen != null) { - directoryField.text = chosen.path - setupDataTypeComboBoxes() - } - } - - useCaseNameTextField.text = DEFAULT_USE_CASE_NAME - useCaseNameTextField.selectAll() - - (useCaseNameTextField.document as AbstractDocument).documentFilter = - PredicateDocumentFilter { !it.isWhitespace() } - setupDataTypeComboBoxes() setupWarningLabels() } - override fun getPreferredFocusedComponent(): JComponent = useCaseNameTextField - - override fun show() { - super.show() - - SwingUtilities.invokeLater { - validateFieldOnChange() - } - } - - override fun createCenterPanel(): JComponent = - panel { + override fun createCenterPanel(): DialogPanel { + return panel { row(CleanArchitectureGeneratorBundle.message("dialog.usecase.name.label")) { - cell(useCaseNameTextField) - cell( - JBLabel(USE_CASE_SUFFIX).apply { - foreground = UIUtil.getLabelDisabledForeground() + textField() + .bindText(::useCaseNameText) + .onChanged { useCaseNameText = it.text } + .applyToComponent { + useCaseNameTextField = this + invokeLater { + columns = 20 + text = DEFAULT_USE_CASE_NAME + selectAll() + requestFocusInWindow() + } + (document as AbstractDocument).documentFilter = PredicateDocumentFilter { !it.isWhitespace() } } - ) + label(USE_CASE_SUFFIX) + .applyToComponent { foreground = UIUtil.getLabelDisabledForeground() } } row(CleanArchitectureGeneratorBundle.message("dialog.usecase.input.type.label")) { - cell(inputDataTypeComboBox) - cell(inputWarningLabel) + @Suppress("UnstableApiUsage") + comboBox(inputDataTypeModel) + .whenItemChangedFromUi { _inputDataType = it } + .applyToComponent { + inputDataTypeComboBox = this + isEditable = true + val textField = editor.editorComponent as JTextField + textField.handleChange { + println("ERAN: CHANGE! ${textField.text}") + _inputDataType = textField.text + inputDataType.validateDataType(inputWarningLabel) + } + invokeLater { selectedItem = DEFAULT_DATA_TYPE } + } + label("").applyToComponent { inputWarningLabel = this } } row(CleanArchitectureGeneratorBundle.message("dialog.usecase.output.type.label")) { - cell(outputDataTypeComboBox) - cell(outputWarningLabel) + @Suppress("UnstableApiUsage") + comboBox(outputDataTypeModel) + .whenItemChangedFromUi { _outputDataType = it } + .applyToComponent { + outputDataTypeComboBox = this + isEditable = true + val textField = editor.editorComponent as JTextField + textField.handleChange { + println("ERAN: CHANGE! ${textField.text}") + _outputDataType = textField.text + outputDataType.validateDataType(outputWarningLabel) + } + invokeLater { selectedItem = DEFAULT_DATA_TYPE } + } + label("").applyToComponent { outputWarningLabel = this } } row(CleanArchitectureGeneratorBundle.message("dialog.usecase.directory.field.label")) { - cell(directoryField) - .comment(CleanArchitectureGeneratorBundle.message("dialog.usecase.directory.comment")) - .align(Align.FILL) + @Suppress("UnstableApiUsage") + textFieldWithBrowseButton( + project = project, + fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor(), + fileChosen = { + it.path.also { path -> + directoryPath = path + setupDataTypeComboBoxes() + } + } + ) + .bindText(::directoryPath) + .applyToComponent { + invokeLater { + text = initialDirectory + } + toolTipText = CleanArchitectureGeneratorBundle.message("dialog.usecase.directory.comment") + } } } + } override fun doValidate(): ValidationInfo? = if (useCaseName.isEmpty()) { @@ -135,27 +160,6 @@ class CreateUseCaseDialog( outputWarningLabel.clearWarning() } - private fun setupDocumentListenersIfNeeded() { - if (documentListenersSetup) { - return - } - - val inputEditor = inputDataTypeComboBox.editor.editorComponent as? JBTextField - val outputEditor = outputDataTypeComboBox.editor.editorComponent as? JBTextField - - if (inputEditor != null && outputEditor != null) { - inputEditor.validateOnChange() - outputEditor.validateOnChange() - - documentListenersSetup = true - } - } - - private fun validateFieldOnChange() { - inputDataType.validateDataType(inputWarningLabel) - outputDataType.validateDataType(outputWarningLabel) - } - private fun validateInputOutputTypes(): ValidationInfo? { val destinationDirectory = destinationDirectory ?: return null @@ -190,7 +194,7 @@ class CreateUseCaseDialog( return null } - private fun String?.validateDataType(warningLabel: JBLabel) { + private fun String?.validateDataType(warningLabel: JLabel) { if (isNullOrEmpty()) { warningLabel.clearWarning() return @@ -203,56 +207,41 @@ class CreateUseCaseDialog( val destinationDirectory = destinationDirectory if (destinationDirectory != null && !symbolValidator.isValidSymbolInContext(this, destinationDirectory)) { - warningLabel.showFieldWarning( - CleanArchitectureGeneratorBundle.message("error.symbol.not.found") - ) + warningLabel.showFieldWarning(CleanArchitectureGeneratorBundle.message("error.symbol.not.found")) } else { warningLabel.clearWarning() } } - private fun JBLabel.clearWarning() { + private fun JLabel.clearWarning() { icon = null text = " " } - private fun JBLabel.showFieldWarning(message: String) { + private fun JLabel.showFieldWarning(message: String) { icon = AllIcons.General.Warning text = message } private fun setupDataTypeComboBoxes() { - destinationDirectory?.let { destinationDirectory -> + File(initialDirectory).let { destinationDirectory -> val modelClasses = modelClassFinder.findModelClasses(destinationDirectory) val allOptions = ModelClassFinder.PRIMITIVE_TYPES + modelClasses - inputDataTypeComboBox.enableAndPopulateComboBox(allOptions) - outputDataTypeComboBox.enableAndPopulateComboBox(allOptions) + inputDataTypeModel.addAll(allOptions) + outputDataTypeModel.addAll(allOptions) } } - private fun ComboBox.enableAndPopulateComboBox(options: List) { - isEditable = true - addActionListener { - validateFieldOnChange() - } - model = DefaultComboBoxModel().apply { addAll(options) } - selectedItem = DEFAULT_DATA_TYPE - } + private fun JTextField.handleChange(onChange: () -> Unit) { + document.addDocumentListener( + object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) = onChange() - private fun JBTextField.validateOnChange() { - document.addDocumentListener(OnChangeDocumentListener { validateFieldOnChange() }) - addFocusListener( - object : FocusAdapter() { - override fun focusGained(event: FocusEvent) { - setupDocumentListenersIfNeeded() - } + override fun removeUpdate(e: DocumentEvent) = onChange() - override fun focusLost(event: FocusEvent) { - validateFieldOnChange() - } + override fun changedUpdate(e: DocumentEvent) = onChange() } ) - addActionListener { validateFieldOnChange() } } } diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateViewModelDialog.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateViewModelDialog.kt index 812b356..46a1836 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateViewModelDialog.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/CreateViewModelDialog.kt @@ -1,18 +1,15 @@ package com.mitteloupe.cag.cleanarchitecturegenerator -import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.ui.components.JBTextField -import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.panel import com.intellij.util.ui.UIUtil import com.mitteloupe.cag.cleanarchitecturegenerator.form.PredicateDocumentFilter import com.mitteloupe.cag.cleanarchitecturegenerator.validation.SymbolValidator +import java.awt.EventQueue.invokeLater import java.io.File import javax.swing.JComponent import javax.swing.text.AbstractDocument @@ -21,56 +18,62 @@ private const val VIEW_MODEL_SUFFIX = "ViewModel" private const val DEFAULT_VIEW_MODEL_NAME = "My" class CreateViewModelDialog( - project: Project?, - suggestedDirectory: File? + private val project: Project?, + private val suggestedDirectory: File? ) : DialogWrapper(project) { - private val viewModelNameTextField = JBTextField() - private val directoryField = TextFieldWithBrowseButton() + private var viewModelNameText: String = "" + private var directoryPath: String = "" private val symbolValidator = SymbolValidator() val viewModelNameWithSuffix: String get() = viewModelName.removeSuffix(VIEW_MODEL_SUFFIX) + VIEW_MODEL_SUFFIX private val viewModelName: String - get() = viewModelNameTextField.text.trim() + get() = viewModelNameText.trim() val destinationDirectory: File? - get() = if (directoryField.text.isNotEmpty()) File(directoryField.text) else null + get() = + if (directoryPath.isNotEmpty()) { + File(directoryPath) + } else { + null + } init { title = CleanArchitectureGeneratorBundle.message("info.viewmodel.generator.title") init() - - viewModelNameTextField.columns = 20 - directoryField.text = suggestedDirectory?.absolutePath.orEmpty() - directoryField.addActionListener { - val descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor() - val initialDirectory = LocalFileSystem.getInstance().findFileByIoFile(File(directoryField.text)) - val chosen = FileChooser.chooseFile(descriptor, project, initialDirectory) - if (chosen != null) { - directoryField.text = chosen.path - } - } - - viewModelNameTextField.text = DEFAULT_VIEW_MODEL_NAME - viewModelNameTextField.selectAll() - - (viewModelNameTextField.document as AbstractDocument).documentFilter = - PredicateDocumentFilter { !it.isWhitespace() } } override fun createCenterPanel(): JComponent = panel { row(CleanArchitectureGeneratorBundle.message("dialog.viewmodel.name.label")) { - cell(viewModelNameTextField) + textField() + .bindText(::viewModelNameText) + .onChanged { viewModelNameText = it.text } + .applyToComponent { + invokeLater { + columns = 20 + text = DEFAULT_VIEW_MODEL_NAME + selectAll() + } + toolTipText = CleanArchitectureGeneratorBundle.message("dialog.viewmodel.directory.tooltip") + (document as AbstractDocument).documentFilter = PredicateDocumentFilter { !it.isWhitespace() } + } label(VIEW_MODEL_SUFFIX) .applyToComponent { foreground = UIUtil.getLabelDisabledForeground() } } row(CleanArchitectureGeneratorBundle.message("dialog.viewmodel.directory.field.label")) { - cell(directoryField) - .comment(CleanArchitectureGeneratorBundle.message("dialog.viewmodel.directory.comment")) - .align(Align.FILL) + @Suppress("UnstableApiUsage") + textFieldWithBrowseButton( + project = project, + fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor() + ) + .bindText(::directoryPath) + .applyToComponent { + invokeLater { text = suggestedDirectory?.absolutePath.orEmpty() } + toolTipText = CleanArchitectureGeneratorBundle.message("dialog.viewmodel.directory.tooltip") + } } } diff --git a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/validation/SymbolValidator.kt b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/validation/SymbolValidator.kt index 7bc89c2..ecfe764 100644 --- a/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/validation/SymbolValidator.kt +++ b/plugin/src/main/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/validation/SymbolValidator.kt @@ -10,7 +10,9 @@ class SymbolValidator( private val fileSystemWrapper: FileSystemWrapper = IntelliJFileSystemWrapper() ) { fun isValidSymbolSyntax(type: String): Boolean { - if (type.isEmpty()) return false + if (type.isBlank()) { + return false + } val normalizedType = type.trim() val baseType = normalizedType.substringBefore('<') return isValidKotlinIdentifier(baseType) && @@ -22,7 +24,7 @@ class SymbolValidator( contextDirectory: File ): Boolean { if (!isValidSymbolSyntax(type)) { - return true + return false } val baseType = type.trim().substringBefore('<') @@ -35,13 +37,30 @@ class SymbolValidator( return true } - return findTypeInModule(baseType, contextDirectory) + val entityName = + if (baseType.contains('.')) { + baseType.substringAfterLast('.') + } else { + baseType + } + + return findTypeInModule(entityName, contextDirectory) } private fun isValidKotlinIdentifier(identifier: String): Boolean { - if (identifier.isEmpty()) return false - if (!identifier.first().isLetter() && identifier.first() != '_') return false - return identifier.all { it.isLetterOrDigit() || it == '_' } + val parts = identifier.split('.') + parts.forEach { part -> + if (part.isEmpty()) { + return false + } + if (!part.first().isLetter() && part.first() != '_') { + return false + } + if (!part.all { it.isLetterOrDigit() || it == '_' }) { + return false + } + } + return true } private fun hasValidGenericSyntax(type: String): Boolean { @@ -114,15 +133,13 @@ class SymbolValidator( return null } - private fun findSourceDirectory(moduleRoot: File): File? { - val srcMain = File(moduleRoot, "src/main/kotlin") - if (srcMain.exists()) return srcMain - - val src = File(moduleRoot, "src") - if (src.exists()) return src - - return null - } + private fun findSourceDirectory(moduleRoot: File): File? = + listOf("src/main/kotlin", "src/main/java", "src") + .asSequence() + .map { path -> File(moduleRoot, path) } + .firstOrNull { file -> + file.exists() && file.isDirectory + } private fun searchForTypeInDirectory( typeName: String, @@ -155,7 +172,7 @@ class SymbolValidator( ): Boolean { val patterns = listOf( - """(?:class|interface|object|enum class)\s+$typeName(?:\s|<|$)""".toRegex(), + """(?:class|interface|object|enum class)\s+$typeName(?:\s|<|\(|$)""".toRegex(), """typealias\s+$typeName(?:\s|<|=)""".toRegex() ) diff --git a/plugin/src/main/resources/messages/CleanArchitectureGeneratorBundle.properties b/plugin/src/main/resources/messages/CleanArchitectureGeneratorBundle.properties index 76e9316..d456fe6 100644 --- a/plugin/src/main/resources/messages/CleanArchitectureGeneratorBundle.properties +++ b/plugin/src/main/resources/messages/CleanArchitectureGeneratorBundle.properties @@ -34,7 +34,7 @@ info.viewmodel.generator.title=New Clean Architecture ViewModel info.viewmodel.generator.confirmation=Success! dialog.viewmodel.name.label=ViewModel name: dialog.viewmodel.directory.field.label=Destination directory: -dialog.viewmodel.directory.comment=Target directory for the ViewModel +dialog.viewmodel.directory.tooltip=Target directory for the ViewModel validation.viewmodel.name.required=ViewModel name is required validation.viewmodel.name.invalid=Invalid ViewModel name: {0} validation.viewmodel.directory.not.exists=Directory does not exist: {0} diff --git a/plugin/src/test/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/validation/SymbolValidatorTest.kt b/plugin/src/test/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/validation/SymbolValidatorTest.kt index 4a6b2b2..24f8b75 100644 --- a/plugin/src/test/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/validation/SymbolValidatorTest.kt +++ b/plugin/src/test/kotlin/com/mitteloupe/cag/cleanarchitecturegenerator/validation/SymbolValidatorTest.kt @@ -1,5 +1,7 @@ package com.mitteloupe.cag.cleanarchitecturegenerator.validation +import com.mitteloupe.cag.cleanarchitecturegenerator.test.filesystem.FakeFileSystemWrapper +import org.jetbrains.kotlin.incremental.createDirectory import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -11,12 +13,15 @@ import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameters import org.junit.runners.Suite.SuiteClasses import java.io.File +import java.nio.file.Files +import kotlin.io.path.createTempDirectory @RunWith(Enclosed::class) @SuiteClasses( SymbolValidatorTest.BasicTests::class, - SymbolValidatorTest.IsValidSymbolSyntaxParameterizedTests::class, - SymbolValidatorTest.IsValidSymbolInContextParameterizedTests::class + SymbolValidatorTest.IsValidSymbolSyntaxTests::class, + SymbolValidatorTest.IsValidSymbolInContextTests::class, + SymbolValidatorTest.IsValidSymbolInContextClassTests::class ) class SymbolValidatorTest { class BasicTests { @@ -53,30 +58,11 @@ class SymbolValidatorTest { } @RunWith(Parameterized::class) - class IsValidSymbolSyntaxParameterizedTests( + class IsValidSymbolSyntaxTests( private val input: String, private val expectedResult: Boolean, @Suppress("unused") private val description: String ) { - private lateinit var classUnderTest: SymbolValidator - - @Before - fun setUp() { - classUnderTest = SymbolValidator() - } - - @Test - fun `Given symbol when isValidSymbolSyntax then returns expected result`() { - // Given - val symbol = input - - // When - val actualResult = classUnderTest.isValidSymbolSyntax(symbol) - - // Then - assertEquals("Unexpected result for '$symbol'", expectedResult, actualResult) - } - companion object { private const val DESCRIPTION_INVALID_GENERIC_TYPE = "invalid generic type" private const val DESCRIPTION_VALID_GENERIC_TYPE = "valid generic type" @@ -85,8 +71,8 @@ class SymbolValidatorTest { @JvmStatic @Parameters(name = "{2}: ''{0}'' -> {1}") - fun parameters(): Collection> { - return listOf( + fun parameters(): Collection> = + listOf( arrayOf("String", true, DESCRIPTION_VALID_IDENTIFIER), arrayOf("MyClass", true, DESCRIPTION_VALID_IDENTIFIER), arrayOf("_private", true, DESCRIPTION_VALID_IDENTIFIER), @@ -100,15 +86,45 @@ class SymbolValidatorTest { arrayOf("List<>", false, DESCRIPTION_INVALID_GENERIC_TYPE), arrayOf("List<>", false, DESCRIPTION_INVALID_GENERIC_TYPE) ) - } + } + + private lateinit var classUnderTest: SymbolValidator + + @Before + fun setUp() { + classUnderTest = SymbolValidator() + } + + @Test + fun `Given symbol when isValidSymbolSyntax then returns expected result`() { + // Given + val symbol = input + + // When + val actualResult = classUnderTest.isValidSymbolSyntax(symbol) + + // Then + assertEquals("Unexpected result for '$symbol'", expectedResult, actualResult) } } @RunWith(Parameterized::class) - class IsValidSymbolInContextParameterizedTests( - private val input: String, + class IsValidSymbolInContextTests( + private val symbol: String, private val expectedResult: Boolean ) { + companion object { + @JvmStatic + @Parameters(name = "Given primitive type {0} then returns {1}") + fun parameters(): Collection> = + listOf( + arrayOf("String", true), + arrayOf("Int", true), + arrayOf("Boolean", true), + arrayOf("Unit", true) + ) + } + private lateinit var classUnderTest: SymbolValidator @Before @@ -117,33 +133,70 @@ class SymbolValidatorTest { } @Test - fun `Given symbol when isValidSymbolInContext then returns expected result`() { + fun `When isValidSymbolInContext`() { // Given - val symbol = input val contextDirectory = File(".") // When val actualResult = classUnderTest.isValidSymbolInContext(symbol, contextDirectory) // Then - if (expectedResult) { - assertTrue("Expected '$symbol' to be valid in context", actualResult) - } else { - assertFalse("Expected '$symbol' to be invalid in context", actualResult) - } + assertEquals(expectedResult, actualResult) } + } + @RunWith(Parameterized::class) + class IsValidSymbolInContextClassTests( + private val qualifiedSymbol: String, + private val expectedResult: Boolean + ) { companion object { @JvmStatic - @Parameters(name = "primitive type in context: ''{0}'' -> {1}") - fun parameters(): Collection> { - return listOf( - arrayOf("String", true), - arrayOf("Int", true), - arrayOf("Boolean", true), - arrayOf("Unit", true) + @Parameters(name = "Given custom type {0} then returns {1}") + fun parameters(): Collection> = + listOf( + arrayOf("com.test.model.DemoModel", true), + arrayOf("com.Int", false), + arrayOf("com.Boolean", false), + arrayOf("com.Unit", false) ) - } + } + + private lateinit var classUnderTest: SymbolValidator + + @Before + fun setUp() { + val temporaryDirectory = Files.createTempDirectory("IsValidSymbolInContextClassTests_").toFile() + + classUnderTest = SymbolValidator(fileSystemWrapper = FakeFileSystemWrapper(temporaryDirectory)) + } + + @Test + fun `When isValidSymbolInContext`() { + // Given + val contextDirectory = createTempDirectory(prefix = "isValidSymbolInContext_").toFile() + val gradleFile = File(contextDirectory, "build.gradle.kts") + gradleFile.createNewFile() + val classesPath = "/src/main/java/main/com/test/model" + val classesDirectory = + File(contextDirectory, classesPath).apply { + createDirectory() + } + val classesFile = File(classesDirectory, "Classes.kt") + classesFile.writeText( + """ + package com.test.model + + class DemoModel { + } + """.trimIndent() + ) + + // When + val actualResult = classUnderTest.isValidSymbolInContext(qualifiedSymbol, contextDirectory) + + // Then + assertEquals(expectedResult, actualResult) } } }