From 8fd021580761fd19dfe3e9deb30b8d95507f27f3 Mon Sep 17 00:00:00 2001 From: Andrew Steinmetz Date: Wed, 24 Sep 2025 01:07:01 -0700 Subject: [PATCH] refactor project into viewmodel for cleaner separation of concerns --- .../devkit/{ui => actions}/ModuleCreator.kt | 2 +- .../devkit/actions/NewFeatureModuleAction.kt | 7 +- .../actions/NewFeatureModuleDefaults.kt | 94 +++++ .../actions/NewFeatureModuleViewModel.kt | 199 ++++++++++ .../devkit/actions/NewModuleDialog.kt | 217 +++++++++++ .../devkit/ui/NewModuleDialog.kt | 348 ------------------ 6 files changed, 514 insertions(+), 353 deletions(-) rename src/main/kotlin/com/plusmobileapps/devkit/{ui => actions}/ModuleCreator.kt (99%) create mode 100644 src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleDefaults.kt create mode 100644 src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleViewModel.kt create mode 100644 src/main/kotlin/com/plusmobileapps/devkit/actions/NewModuleDialog.kt delete mode 100644 src/main/kotlin/com/plusmobileapps/devkit/ui/NewModuleDialog.kt diff --git a/src/main/kotlin/com/plusmobileapps/devkit/ui/ModuleCreator.kt b/src/main/kotlin/com/plusmobileapps/devkit/actions/ModuleCreator.kt similarity index 99% rename from src/main/kotlin/com/plusmobileapps/devkit/ui/ModuleCreator.kt rename to src/main/kotlin/com/plusmobileapps/devkit/actions/ModuleCreator.kt index dbd5966..bf490be 100644 --- a/src/main/kotlin/com/plusmobileapps/devkit/ui/ModuleCreator.kt +++ b/src/main/kotlin/com/plusmobileapps/devkit/actions/ModuleCreator.kt @@ -1,4 +1,4 @@ -package com.plusmobileapps.devkit.ui +package com.plusmobileapps.devkit.actions import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType diff --git a/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleAction.kt b/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleAction.kt index 37fa902..fc148b5 100644 --- a/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleAction.kt +++ b/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleAction.kt @@ -5,7 +5,6 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.icons.AllIcons -import com.plusmobileapps.devkit.ui.NewModuleDialog class NewFeatureModuleAction : AnAction("New Feature Module", "Create a new module", AllIcons.Actions.ModuleDirectory) { @@ -13,10 +12,10 @@ class NewFeatureModuleAction : AnAction("New Feature Module", "Create a new modu val project = e.project ?: return val selectedFile = e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return - val dialog = NewModuleDialog(project, selectedFile) + val viewModel = NewFeatureModuleViewModel(project, selectedFile) + val dialog = NewModuleDialog(viewModel) if (dialog.showAndGet()) { - // Dialog was confirmed, process the input - dialog.createModule() + viewModel.createModule() } } diff --git a/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleDefaults.kt b/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleDefaults.kt new file mode 100644 index 0000000..396a257 --- /dev/null +++ b/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleDefaults.kt @@ -0,0 +1,94 @@ +package com.plusmobileapps.devkit.actions + +object NewFeatureModuleDefaults { + fun getDefaultPublicBuildGradle(): String { + return """ + plugins { + kotlin("multiplatform") + } + + kotlin { + jvm() + js(IR) { + browser() + nodejs() + } + + sourceSets { + val commonMain by getting { + dependencies { + // Public module dependencies + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } + } + """.trimIndent() + } + + fun getDefaultImplBuildGradle(): String { + return """ + plugins { + kotlin("multiplatform") + } + + kotlin { + jvm() + js(IR) { + browser() + nodejs() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project("${'$'}projectDirectory:${'$'}directoryName:public")) + // Implementation module dependencies + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } + } + """.trimIndent() + } + + fun getDefaultTestingBuildGradle(): String { + return """ + plugins { + kotlin("multiplatform") + } + + kotlin { + jvm() + js(IR) { + browser() + nodejs() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project("${'$'}projectDirectory:${'$'}directoryName:public")) + implementation(project("${'$'}projectDirectory:${'$'}directoryName:impl")) + // Testing module dependencies + implementation(kotlin("test")) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleViewModel.kt b/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleViewModel.kt new file mode 100644 index 0000000..9a8d015 --- /dev/null +++ b/src/main/kotlin/com/plusmobileapps/devkit/actions/NewFeatureModuleViewModel.kt @@ -0,0 +1,199 @@ +package com.plusmobileapps.devkit.actions + +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.vfs.VirtualFile +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class NewFeatureModuleViewModel( + private val project: Project, + private val parentDirectory: VirtualFile, +) { + + val _state = MutableStateFlow( + loadPersistedValues() + ) + + val state: StateFlow = _state.asStateFlow() + + private fun loadPersistedValues(): State { + val properties: PropertiesComponent = PropertiesComponent.getInstance() + return State( + packageName = properties.getValue(PACKAGE_NAME_KEY, "com.example"), + directoryName = "", + namespace = properties.getValue(PACKAGE_NAME_KEY, "com.example"), + createPublic = true, + createImpl = true, + createTesting = false, + publicBuildGradle = properties.getValue(PUBLIC_BUILD_GRADLE_KEY, NewFeatureModuleDefaults.getDefaultPublicBuildGradle()), + implBuildGradle = properties.getValue(IMPL_BUILD_GRADLE_KEY, NewFeatureModuleDefaults.getDefaultImplBuildGradle()), + testingBuildGradle = properties.getValue(TESTING_BUILD_GRADLE_KEY, NewFeatureModuleDefaults.getDefaultTestingBuildGradle()) + ) + } + + fun onNameSpaceUpdated(nameSpace: String) { + _state.update { + it.copy(namespace = nameSpace) + } + } + + fun onPackageNameUpdated(packageName: String) { + _state.update { + it.copy(packageName = packageName) + } + updateNamespace() + } + + fun onDirectoryNameUpdated(directoryName: String) { + _state.update { + it.copy(directoryName = directoryName) + } + updateNamespace() + } + + fun onCreatePublicUpdated(createPublic: Boolean) { + _state.update { + it.copy(createPublic = createPublic) + } + } + + fun onCreateImplUpdated(createImpl: Boolean) { + _state.update { + it.copy(createImpl = createImpl) + } + } + + fun onCreateTestingUpdated(createTesting: Boolean) { + _state.update { + it.copy(createTesting = createTesting) + } + } + + fun onPublicBuildGradleUpdated(publicBuildGradle: String) { + _state.update { + it.copy(publicBuildGradle = publicBuildGradle) + } + } + + fun onImplBuildGradleUpdated(implBuildGradle: String) { + _state.update { + it.copy(implBuildGradle = implBuildGradle) + } + } + + fun onTestingBuildGradleUpdated(testingBuildGradle: String) { + _state.update { + it.copy(testingBuildGradle = testingBuildGradle) + } + } + + fun savePersistedValues() { + val properties = PropertiesComponent.getInstance() + val currentState = state.value + + // Save package name and build.gradle.kts templates + properties.setValue(PACKAGE_NAME_KEY, currentState.packageName) + properties.setValue(PUBLIC_BUILD_GRADLE_KEY, currentState.publicBuildGradle) + properties.setValue(IMPL_BUILD_GRADLE_KEY, currentState.implBuildGradle) + properties.setValue(TESTING_BUILD_GRADLE_KEY, currentState.testingBuildGradle) + } + + fun createModule() { +// val packageName = packageNameField.text.trim() +// val directoryName = directoryNameField.text.trim() +// +// if (packageName.isEmpty() || directoryName.isEmpty()) { +// return +// } +// +// // Use packageName.directoryName as namespace to create correct directory structure +// val namespace = "$packageName.$directoryName" +// +// // Calculate project directory path relative to project root +// val projectBasePath = project.basePath +// val parentPath = parentDirectory.path +// val projectDirectory = if (projectBasePath != null && parentPath.startsWith(projectBasePath)) { +// // Get relative path from project root, ensuring it starts with ":" +// val relativePath = parentPath.substring(projectBasePath.length) +// .replace("/", ":") +// .let { if (it.startsWith(":")) it else ":$it" } +// relativePath +// } else { +// // Fallback if we can't determine relative path +// ":${parentDirectory.name}" +// } +// +// // Replace placeholders in templates with actual values +// val processedPublicTemplate = if (publicModuleCheckBox.isSelected) { +// publicBuildGradleArea.text +// .replace("\$directoryName", directoryName) +// .replace("\$projectDirectory", projectDirectory) +// } else null +// +// val processedImplTemplate = if (implModuleCheckBox.isSelected) { +// implBuildGradleArea.text +// .replace("\$directoryName", directoryName) +// .replace("\$projectDirectory", projectDirectory) +// } else null +// +// val processedTestingTemplate = if (testingModuleCheckBox.isSelected) { +// testingBuildGradleArea.text +// .replace("\$directoryName", directoryName) +// .replace("\$projectDirectory", projectDirectory) +// } else null +// +// val moduleCreator = ModuleCreator( +// project = project, +// parentDirectory = parentDirectory, +// namespace = namespace, +// directoryName = directoryName, +// createPublic = publicModuleCheckBox.isSelected, +// createImpl = implModuleCheckBox.isSelected, +// createTesting = testingModuleCheckBox.isSelected, +// publicBuildGradleTemplate = processedPublicTemplate, +// implBuildGradleTemplate = processedImplTemplate, +// testingBuildGradleTemplate = processedTestingTemplate +// ) +// +// moduleCreator.createModules() + } + + private fun updateNamespace() { + val currentState = state.value + val packageName = currentState.packageName.trim() + val directoryName = currentState.directoryName.trim() + + _state.update { + it.copy( + namespace = if (packageName.isNotEmpty() && directoryName.isNotEmpty()) { + "$packageName.$directoryName" + } else packageName.ifEmpty { + "" + } + ) + } + } + + data class State( + val packageName: String, + val directoryName: String, + val namespace: String, + val createPublic: Boolean, + val createImpl: Boolean, + val createTesting: Boolean, + val publicBuildGradle: String, + val implBuildGradle: String, + val testingBuildGradle: String + ) + + companion object { + private const val PACKAGE_NAME_KEY = "plusdevkit.packageName" + private const val PUBLIC_BUILD_GRADLE_KEY = "plusdevkit.publicBuildGradle" + private const val IMPL_BUILD_GRADLE_KEY = "plusdevkit.implBuildGradle" + private const val TESTING_BUILD_GRADLE_KEY = "plusdevkit.testingBuildGradle" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/plusmobileapps/devkit/actions/NewModuleDialog.kt b/src/main/kotlin/com/plusmobileapps/devkit/actions/NewModuleDialog.kt new file mode 100644 index 0000000..76eab02 --- /dev/null +++ b/src/main/kotlin/com/plusmobileapps/devkit/actions/NewModuleDialog.kt @@ -0,0 +1,217 @@ +package com.plusmobileapps.devkit.actions + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBTabbedPane +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import javax.swing.BorderFactory +import javax.swing.Box +import javax.swing.BoxLayout +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.JScrollPane + +class NewModuleDialog( + private val viewModel: NewFeatureModuleViewModel +) : DialogWrapper(true) { + + private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + + + private val directoryNameField = JBTextField() + private val packageNameField = JBTextField() + private val namespaceField = JBTextField() + + private val publicModuleCheckBox = JBCheckBox("Public", true) + private val implModuleCheckBox = JBCheckBox("Impl", false) + private val testingModuleCheckBox = JBCheckBox("Testing", false) + + private val publicBuildGradleArea = JBTextArea(15, 50) + private val implBuildGradleArea = JBTextArea(15, 50) + private val testingBuildGradleArea = JBTextArea(15, 50) + + init { + title = "Create New Module" + scope.launch { + viewModel.state.collect { + updateUi(it) + } + } + observeUiEvents() + init() + } + + override fun createCenterPanel(): JComponent { + val mainPanel = JPanel(BorderLayout()) + + // Left side - Checkboxes with outlined box + val checkboxPanel = createCheckBoxPanel() + + // Right side - Text fields + val fieldsPanel = FormBuilder.createFormBuilder() + .addLabeledComponent("Directory Name:", directoryNameField) + .addLabeledComponent("Package Name:", packageNameField) + .addLabeledComponent("Namespace:", namespaceField) + .panel + + // Top panel with left and right sections + val topPanel = JPanel(BorderLayout()) + topPanel.add(checkboxPanel, BorderLayout.WEST) + topPanel.add(fieldsPanel, BorderLayout.CENTER) + + // Add some spacing + topPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) + + // Tabs section for build.gradle.kts templates + val tabbedPane = JBTabbedPane() + + // Public module tab + val publicScrollPane = JScrollPane(publicBuildGradleArea) + tabbedPane.addTab("Public Module", publicScrollPane) + + // Impl module tab + val implScrollPane = JScrollPane(implBuildGradleArea) + tabbedPane.addTab("Impl Module", implScrollPane) + + // Testing module tab + val testingScrollPane = JScrollPane(testingBuildGradleArea) + tabbedPane.addTab("Testing Module", testingScrollPane) + + // Assemble main panel + mainPanel.add(topPanel, BorderLayout.NORTH) + mainPanel.add(tabbedPane, BorderLayout.CENTER) + + mainPanel.preferredSize = JBUI.size(700, 500) + + return mainPanel + } + + private fun createCheckBoxPanel(): JPanel = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(publicModuleCheckBox) + add(Box.createVerticalStrut(10)) + add(implModuleCheckBox) + add(Box.createVerticalStrut(10)) + add(testingModuleCheckBox) + add(Box.createVerticalGlue()) + // Add outlined border around checkboxes + border = BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), + "Create Module Type" + ) + + // Set minimum width to prevent title ellipsization + val minWidth = 150 + minimumSize = Dimension(minWidth, 0) + preferredSize = Dimension(minWidth, preferredSize.height) + } + + private fun observeUiEvents() { + namespaceField.addKeyListener(object : KeyAdapter() { + override fun keyReleased(e: KeyEvent?) { + viewModel.onNameSpaceUpdated(namespaceField.text) + } + }) + + directoryNameField.addKeyListener(object : KeyAdapter() { + override fun keyReleased(e: KeyEvent?) { + viewModel.onDirectoryNameUpdated(directoryNameField.text) + } + }) + + packageNameField.addKeyListener(object : KeyAdapter() { + override fun keyReleased(e: KeyEvent?) { + viewModel.onPackageNameUpdated(packageNameField.text) + } + }) + + publicModuleCheckBox.addActionListener { + viewModel.onCreatePublicUpdated(publicModuleCheckBox.isSelected) + } + + implModuleCheckBox.addActionListener { + viewModel.onCreateImplUpdated(implModuleCheckBox.isSelected) + } + + testingModuleCheckBox.addActionListener { + viewModel.onCreateTestingUpdated(testingModuleCheckBox.isSelected) + } + + publicBuildGradleArea.addKeyListener(object : KeyAdapter() { + override fun keyReleased(e: KeyEvent?) { + viewModel.onPublicBuildGradleUpdated(publicBuildGradleArea.text) + } + }) + + implBuildGradleArea.addKeyListener(object : KeyAdapter() { + override fun keyReleased(e: KeyEvent?) { + viewModel.onImplBuildGradleUpdated(implBuildGradleArea.text) + } + }) + + testingBuildGradleArea.addKeyListener(object : KeyAdapter() { + override fun keyReleased(e: KeyEvent?) { + viewModel.onTestingBuildGradleUpdated(testingBuildGradleArea.text) + } + }) + } + + override fun doOKAction() { + viewModel.savePersistedValues() + super.doOKAction() + } + + override fun doValidate(): ValidationInfo? { + if (directoryNameField.text.isNullOrBlank()) { + return ValidationInfo("Directory name is required", directoryNameField) + } + + if (packageNameField.text.isNullOrBlank()) { + return ValidationInfo("Package name is required", packageNameField) + } + + if (!publicModuleCheckBox.isSelected && !implModuleCheckBox.isSelected && !testingModuleCheckBox.isSelected) { + return ValidationInfo("At least one module type must be selected", publicModuleCheckBox) + } + + return null + } + + private fun updateUi(state: NewFeatureModuleViewModel.State) { + // Update text areas for build.gradle.kts templates + publicBuildGradleArea.text = state.publicBuildGradle + implBuildGradleArea.text = state.implBuildGradle + testingBuildGradleArea.text = state.testingBuildGradle + + // Update text fields + directoryNameField.text = state.directoryName + namespaceField.text = state.namespace + packageNameField.text = state.packageName + + + // Checkboxes for which modules to create + publicModuleCheckBox.isSelected = state.createPublic + implModuleCheckBox.isSelected = state.createImpl + testingModuleCheckBox.isSelected = state.createTesting + } + + override fun dispose() { + scope.cancel() + super.dispose() + } +} diff --git a/src/main/kotlin/com/plusmobileapps/devkit/ui/NewModuleDialog.kt b/src/main/kotlin/com/plusmobileapps/devkit/ui/NewModuleDialog.kt deleted file mode 100644 index eac6129..0000000 --- a/src/main/kotlin/com/plusmobileapps/devkit/ui/NewModuleDialog.kt +++ /dev/null @@ -1,348 +0,0 @@ -package com.plusmobileapps.devkit.ui - -import com.intellij.ide.util.PropertiesComponent -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.ui.components.JBCheckBox -import com.intellij.ui.components.JBTabbedPane -import com.intellij.ui.components.JBTextArea -import com.intellij.ui.components.JBTextField -import com.intellij.util.ui.FormBuilder -import com.intellij.util.ui.JBUI -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent -import javax.swing.BorderFactory -import javax.swing.Box -import javax.swing.BoxLayout -import javax.swing.JComponent -import javax.swing.JPanel -import javax.swing.JScrollPane - -class NewModuleDialog( - private val project: Project, - private val parentDirectory: VirtualFile -) : DialogWrapper(true) { - - companion object { - private const val PACKAGE_NAME_KEY = "plusdevkit.packageName" - private const val PUBLIC_BUILD_GRADLE_KEY = "plusdevkit.publicBuildGradle" - private const val IMPL_BUILD_GRADLE_KEY = "plusdevkit.implBuildGradle" - private const val TESTING_BUILD_GRADLE_KEY = "plusdevkit.testingBuildGradle" - } - - private val directoryNameField = JBTextField() - private val packageNameField = JBTextField() - private val namespaceField = JBTextField() - - private val publicModuleCheckBox = JBCheckBox("Public", true) - private val implModuleCheckBox = JBCheckBox("Impl", false) - private val testingModuleCheckBox = JBCheckBox("Testing", false) - - private val publicBuildGradleArea = JBTextArea(15, 50) - private val implBuildGradleArea = JBTextArea(15, 50) - private val testingBuildGradleArea = JBTextArea(15, 50) - - init { - title = "Create New Module" - loadPersistedValues() - setupAutoFillNamespace() - init() - } - - override fun createCenterPanel(): JComponent { - val mainPanel = JPanel(BorderLayout()) - - // Left side - Checkboxes with outlined box - val checkboxPanel = JPanel() - checkboxPanel.layout = BoxLayout(checkboxPanel, BoxLayout.Y_AXIS) - checkboxPanel.add(publicModuleCheckBox) - checkboxPanel.add(Box.createVerticalStrut(10)) - checkboxPanel.add(implModuleCheckBox) - checkboxPanel.add(Box.createVerticalStrut(10)) - checkboxPanel.add(testingModuleCheckBox) - checkboxPanel.add(Box.createVerticalGlue()) - - // Add outlined border around checkboxes - checkboxPanel.border = BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), - "Create Module Type" - ) - - // Set minimum width to prevent title ellipsization - val minWidth = 150 - checkboxPanel.minimumSize = Dimension(minWidth, 0) - checkboxPanel.preferredSize = Dimension(minWidth, checkboxPanel.preferredSize.height) - - // Right side - Text fields - val fieldsPanel = FormBuilder.createFormBuilder() - .addLabeledComponent("Directory Name:", directoryNameField) - .addLabeledComponent("Package Name:", packageNameField) - .addLabeledComponent("Namespace:", namespaceField) - .panel - - // Top panel with left and right sections - val topPanel = JPanel(BorderLayout()) - topPanel.add(checkboxPanel, BorderLayout.WEST) - topPanel.add(fieldsPanel, BorderLayout.CENTER) - - // Add some spacing - topPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) - - // Tabs section for build.gradle.kts templates - val tabbedPane = JBTabbedPane() - - // Public module tab - val publicScrollPane = JScrollPane(publicBuildGradleArea) - tabbedPane.addTab("Public Module", publicScrollPane) - - // Impl module tab - val implScrollPane = JScrollPane(implBuildGradleArea) - tabbedPane.addTab("Impl Module", implScrollPane) - - // Testing module tab - val testingScrollPane = JScrollPane(testingBuildGradleArea) - tabbedPane.addTab("Testing Module", testingScrollPane) - - // Assemble main panel - mainPanel.add(topPanel, BorderLayout.NORTH) - mainPanel.add(tabbedPane, BorderLayout.CENTER) - - mainPanel.preferredSize = JBUI.size(700, 500) - - return mainPanel - } - - private fun setupAutoFillNamespace() { - val updateNamespace = { - val packageName = packageNameField.text.trim() - val directoryName = directoryNameField.text.trim() - - namespaceField.text = if (packageName.isNotEmpty() && directoryName.isNotEmpty()) { - "$packageName.$directoryName" - } else if (packageName.isNotEmpty()) { - packageName - } else { - "" - } - } - - packageNameField.addKeyListener(object : KeyAdapter() { - override fun keyReleased(e: KeyEvent?) { - updateNamespace() - } - }) - - directoryNameField.addKeyListener(object : KeyAdapter() { - override fun keyReleased(e: KeyEvent?) { - updateNamespace() - } - }) - } - - private fun loadPersistedValues() { - val properties = PropertiesComponent.getInstance() - - // Load package name (persisted) - packageNameField.text = properties.getValue(PACKAGE_NAME_KEY, "com.example") - - // Load build.gradle.kts templates (persisted) - publicBuildGradleArea.text = properties.getValue(PUBLIC_BUILD_GRADLE_KEY, getDefaultPublicBuildGradle()) - implBuildGradleArea.text = properties.getValue(IMPL_BUILD_GRADLE_KEY, getDefaultImplBuildGradle()) - testingBuildGradleArea.text = properties.getValue(TESTING_BUILD_GRADLE_KEY, getDefaultTestingBuildGradle()) - - // Directory name is not persisted - starts empty each time - directoryNameField.text = "" - - // Update namespace based on loaded values - val packageName = packageNameField.text.trim() - if (packageName.isNotEmpty()) { - namespaceField.text = packageName - } - } - - private fun savePersistedValues() { - val properties = PropertiesComponent.getInstance() - - // Save package name and build.gradle.kts templates - properties.setValue(PACKAGE_NAME_KEY, packageNameField.text) - properties.setValue(PUBLIC_BUILD_GRADLE_KEY, publicBuildGradleArea.text) - properties.setValue(IMPL_BUILD_GRADLE_KEY, implBuildGradleArea.text) - properties.setValue(TESTING_BUILD_GRADLE_KEY, testingBuildGradleArea.text) - } - - override fun doOKAction() { - savePersistedValues() - super.doOKAction() - } - - override fun doValidate(): ValidationInfo? { - if (directoryNameField.text.isNullOrBlank()) { - return ValidationInfo("Directory name is required", directoryNameField) - } - - if (packageNameField.text.isNullOrBlank()) { - return ValidationInfo("Package name is required", packageNameField) - } - - if (!publicModuleCheckBox.isSelected && !implModuleCheckBox.isSelected && !testingModuleCheckBox.isSelected) { - return ValidationInfo("At least one module type must be selected", publicModuleCheckBox) - } - - return null - } - - fun createModule() { - val packageName = packageNameField.text.trim() - val directoryName = directoryNameField.text.trim() - - if (packageName.isEmpty() || directoryName.isEmpty()) { - return - } - - // Use packageName.directoryName as namespace to create correct directory structure - val namespace = "$packageName.$directoryName" - - // Calculate project directory path relative to project root - val projectBasePath = project.basePath - val parentPath = parentDirectory.path - val projectDirectory = if (projectBasePath != null && parentPath.startsWith(projectBasePath)) { - // Get relative path from project root, ensuring it starts with ":" - val relativePath = parentPath.substring(projectBasePath.length) - .replace("/", ":") - .let { if (it.startsWith(":")) it else ":$it" } - relativePath - } else { - // Fallback if we can't determine relative path - ":${parentDirectory.name}" - } - - // Replace placeholders in templates with actual values - val processedPublicTemplate = if (publicModuleCheckBox.isSelected) { - publicBuildGradleArea.text - .replace("\$directoryName", directoryName) - .replace("\$projectDirectory", projectDirectory) - } else null - - val processedImplTemplate = if (implModuleCheckBox.isSelected) { - implBuildGradleArea.text - .replace("\$directoryName", directoryName) - .replace("\$projectDirectory", projectDirectory) - } else null - - val processedTestingTemplate = if (testingModuleCheckBox.isSelected) { - testingBuildGradleArea.text - .replace("\$directoryName", directoryName) - .replace("\$projectDirectory", projectDirectory) - } else null - - val moduleCreator = ModuleCreator( - project = project, - parentDirectory = parentDirectory, - namespace = namespace, - directoryName = directoryName, - createPublic = publicModuleCheckBox.isSelected, - createImpl = implModuleCheckBox.isSelected, - createTesting = testingModuleCheckBox.isSelected, - publicBuildGradleTemplate = processedPublicTemplate, - implBuildGradleTemplate = processedImplTemplate, - testingBuildGradleTemplate = processedTestingTemplate - ) - - moduleCreator.createModules() - } - - private fun getDefaultPublicBuildGradle(): String { - return """ - plugins { - kotlin("multiplatform") - } - - kotlin { - jvm() - js(IR) { - browser() - nodejs() - } - - sourceSets { - val commonMain by getting { - dependencies { - // Public module dependencies - } - } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - } - } - } - } - """.trimIndent() - } - - private fun getDefaultImplBuildGradle(): String { - return """ - plugins { - kotlin("multiplatform") - } - - kotlin { - jvm() - js(IR) { - browser() - nodejs() - } - - sourceSets { - val commonMain by getting { - dependencies { - implementation(project("${'$'}projectDirectory:${'$'}directoryName:public")) - // Implementation module dependencies - } - } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - } - } - } - } - """.trimIndent() - } - - private fun getDefaultTestingBuildGradle(): String { - return """ - plugins { - kotlin("multiplatform") - } - - kotlin { - jvm() - js(IR) { - browser() - nodejs() - } - - sourceSets { - val commonMain by getting { - dependencies { - implementation(project("${'$'}projectDirectory:${'$'}directoryName:public")) - implementation(project("${'$'}projectDirectory:${'$'}directoryName:impl")) - // Testing module dependencies - implementation(kotlin("test")) - } - } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - } - } - } - } - """.trimIndent() - } -}