From c9d485e340a8cfb56633d7eb27fff25d6447385e Mon Sep 17 00:00:00 2001 From: Pavel Kirpichenkov Date: Mon, 20 Oct 2025 14:50:15 +0300 Subject: [PATCH] [kotlin] KTIJ-35016 Introduce observable UI settings for move refactoring Settings allow sharing UI state between dependent move model parts. This is necessary for conditional rendering of widgets. For example, simultaneous move of expects with actuals needs a slightly customized UI. GitOrigin-RevId: 722cc642ec192990113b866fa38a70c97f8ac323 --- .../k2/refactoring/move/ui/K2MoveModel.kt | 46 +++++++++++++++---- .../refactoring/move/ui/K2MoveTargetModel.kt | 33 +++++++++---- .../move/ui/ObservableUiSettings.kt | 33 +++++++++++++ 3 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ObservableUiSettings.kt diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveModel.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveModel.kt index d1736c38d189..83e7d42e3bb4 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveModel.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveModel.kt @@ -2,6 +2,9 @@ package org.jetbrains.kotlin.idea.k2.refactoring.move.ui import com.intellij.openapi.editor.Editor +import com.intellij.openapi.observable.properties.AtomicBooleanProperty +import com.intellij.openapi.observable.properties.MutableBooleanProperty +import com.intellij.openapi.observable.properties.ObservableBooleanProperty import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.util.NlsContexts @@ -38,7 +41,11 @@ import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull /** * @see K2MoveDescriptor */ -sealed class K2MoveModel { +sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveModelObservableSettings { + init { + observableUiSettings.registerK2MoveModelSettings(this) + } + abstract val project: Project abstract val source: K2MoveSourceModel<*> @@ -81,6 +88,18 @@ sealed class K2MoveModel { } } + override val mppDeclarationsObservable: ObservableBooleanProperty + get() = mppDeclarations.observableProperty + + override val searchForTextObservable: ObservableBooleanProperty + get() = searchForText.observableProperty + + override val searchInCommentsObservable: ObservableBooleanProperty + get() = searchInComments.observableProperty + + override val searchReferencesObservable: ObservableBooleanProperty + get() = searchReferences.observableProperty + enum class Setting(private val text: @NlsContexts.Checkbox String) { SEARCH_FOR_TEXT(KotlinBundle.message("search.for.text.occurrences")) { override var state: Boolean @@ -124,12 +143,16 @@ sealed class K2MoveModel { }; abstract var state: Boolean + // lazy prevents service access from constructor + internal val observableProperty: MutableBooleanProperty by lazy { AtomicBooleanProperty(state) } fun createComboBox(panel: Panel, enabled: Boolean = true) { panel.row { val checkBox = checkBox(text).enabled(enabled) if (enabled) { checkBox.bindSelected(::state) + // bind doesn't register state changes before dialog confirmation, but the UI should be updated on all state changes + checkBox.onChanged { observableProperty.set(it.isSelected) } } else { checkBox.selected(false) } @@ -145,8 +168,9 @@ sealed class K2MoveModel { override val source: K2MoveSourceModel.FileSource, override val target: K2MoveTargetModel.SourceDirectory, override val inSourceRoot: Boolean, - override val moveCallBack: MoveCallback? = null - ) : K2MoveModel() { + observableUiSettings: ObservableUiSettings, + override val moveCallBack: MoveCallback? = null, + ) : K2MoveModel(observableUiSettings) { private fun PsiFile.isAlreadyInTarget(): Boolean { return parent == target.directory && when (this) { is PsiJavaFile -> packageName == target.pkgName.asString() @@ -192,8 +216,9 @@ sealed class K2MoveModel { override val source: K2MoveSourceModel.ElementSource, override val target: K2MoveTargetModel, override val inSourceRoot: Boolean, + observableUiSettings: ObservableUiSettings, override val moveCallBack: MoveCallback? = null - ) : K2MoveModel() { + ) : K2MoveModel(observableUiSettings) { private fun isValidFileRefactoring(fileName: String): Boolean { fun KtFile.isTargetFile(): Boolean { return containingDirectory == target.directory @@ -307,6 +332,7 @@ sealed class K2MoveModel { } val inSourceRoot = isInSourceRoot(project, elementsToMove, targetContainer) val explicitPkgMoveFqName = findExplicitPkgMoveFqName(elementsToMove) + val observableUiSettings = ObservableUiSettingsImpl() return when { (elementsToMove.all { it is KtFile } && targetContainer is PsiDirectory) @@ -321,22 +347,23 @@ sealed class K2MoveModel { pkgName = pkg, directory = targetContainer, explicitPkgMoveFqName = explicitPkgMoveFqName, + observableUiSettings = observableUiSettings, ) } else { // no default target is provided, happens when invoking refactoring via keyboard instead of drag-and-drop val file = elementsToMove.firstOrNull { it.containingFile != null }?.containingFile ?: error("No default target found") val directory = file.containingDirectory ?: error("No default target found") val pkgName = elementsToMove.firstIsInstanceOrNull()?.containingKtFile?.packageFqName ?: FqName.ROOT - K2MoveTargetModel.SourceDirectory(pkgName, directory, explicitPkgMoveFqName = null) + K2MoveTargetModel.SourceDirectory(pkgName, directory, explicitPkgMoveFqName = null, observableUiSettings) } - Files(project, source, target, inSourceRoot, moveCallBack) + Files(project, source, target, inSourceRoot, observableUiSettings, moveCallBack) } targetContainer is KtFile || targetContainer.isSingleClassContainer() || isSingleFileMove(elementsToMove) -> { val source = K2MoveSourceModel.ElementSource(declarationsFromFiles.toSet()) val targetFile = targetContainer?.containingFile val target = if (targetFile is KtFile) { - K2MoveTargetModel.File(targetFile) + K2MoveTargetModel.File(targetFile, observableUiSettings) } else if (targetContainer is PsiDirectory) { val pkg = targetContainer.getFqNameWithImplicitPrefixOrRoot() K2MoveTargetModel.File( @@ -344,6 +371,7 @@ sealed class K2MoveModel { pkg = pkg, directory = targetContainer, explicitPkgMoveFqName = explicitPkgMoveFqName, + observableUiSettings = observableUiSettings, ) } else { // no default target is provided, happens when invoking refactoring via keyboard instead of drag-and-drop val firstElem = elementsToMove.firstOrNull() as KtElement @@ -352,7 +380,8 @@ sealed class K2MoveModel { K2MoveTargetModel.Declarations( defaultDirectory = psiDirectory, defaultPkgName = containingFile.packageFqName, - defaultFileName = findSourceFileNameByMovedElements(elementsToMove) + defaultFileName = findSourceFileNameByMovedElements(elementsToMove), + observableUiSettings = observableUiSettings, ) } @@ -361,6 +390,7 @@ sealed class K2MoveModel { source = source, target = target, inSourceRoot = inSourceRoot, + observableUiSettings = observableUiSettings, moveCallBack = moveCallBack ) } diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveTargetModel.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveTargetModel.kt index 1fd40b8dd39c..32931474806e 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveTargetModel.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveTargetModel.kt @@ -46,6 +46,8 @@ sealed interface K2MoveTargetModel { val explicitPkgMoveFqName: FqName? + val observableUiSettings: ObservableUiSettings + /** * Creates a [K2MoveTargetDescriptor] from this model. * @param kmpSourceRoot source KMP directory that corresponds to the base [directory] in a different source set. @@ -60,6 +62,7 @@ sealed interface K2MoveTargetModel { override var pkgName: FqName, override var directory: PsiDirectory, override val explicitPkgMoveFqName: FqName?, + override val observableUiSettings: ObservableUiSettings, ) : K2MoveTargetModel { private val initialDirectory = directory @@ -147,7 +150,8 @@ sealed interface K2MoveTargetModel { pkgName: FqName, directory: PsiDirectory, explicitPkgMoveFqName: FqName?, - ) : SourceDirectoryChooser(pkgName, directory, explicitPkgMoveFqName) { + observableUiSettings: ObservableUiSettings, + ) : SourceDirectoryChooser(pkgName, directory, explicitPkgMoveFqName, observableUiSettings) { override fun toDescriptor(kmpSourceRoot: PsiDirectory?): K2MoveTargetDescriptor.Directory { return K2MoveTargetDescriptor.Directory( pkgName = pkgName, @@ -167,7 +171,8 @@ sealed interface K2MoveTargetModel { pkg: FqName, directory: PsiDirectory, explicitPkgMoveFqName: FqName?, - ) : SourceDirectoryChooser(pkg, directory, explicitPkgMoveFqName) { + observableUiSettings: ObservableUiSettings, + ) : SourceDirectoryChooser(pkg, directory, explicitPkgMoveFqName, observableUiSettings) { var fileName: String = fileName protected set @@ -221,8 +226,13 @@ sealed interface K2MoveTargetModel { } } - class File(fileName: String, pkg: FqName, directory: PsiDirectory, explicitPkgMoveFqName: FqName?) : - FileChooser(fileName, pkg, directory, explicitPkgMoveFqName) { + class File( + fileName: String, + pkg: FqName, + directory: PsiDirectory, + explicitPkgMoveFqName: FqName?, + observableUiSettings: ObservableUiSettings, + ) : FileChooser(fileName, pkg, directory, explicitPkgMoveFqName, observableUiSettings) { override fun toDescriptor(kmpSourceRoot: PsiDirectory?): K2MoveTargetDescriptor.File = K2MoveTargetDescriptor.File( fileName = fileName, @@ -242,8 +252,9 @@ sealed interface K2MoveTargetModel { class Declarations( defaultDirectory: PsiDirectory, defaultPkgName: FqName, - defaultFileName: String - ) : FileChooser(defaultFileName, defaultPkgName, defaultDirectory, explicitPkgMoveFqName = null) { + defaultFileName: String, + observableUiSettings: ObservableUiSettings, + ) : FileChooser(defaultFileName, defaultPkgName, defaultDirectory, explicitPkgMoveFqName = null, observableUiSettings) { private val propertyGraph = PropertyGraph() private val destinationClassProperty = propertyGraph.property(null) @@ -354,9 +365,15 @@ sealed interface K2MoveTargetModel { } companion object { - fun File(file: KtFile): File { + fun File(file: KtFile, observableUiSettings: ObservableUiSettings): File { val directory = file.containingDirectory ?: error("No containing directory was found") - return File(file.name, file.packageFqName, directory, null) + return File( + fileName = file.name, + pkg = file.packageFqName, + directory = directory, + explicitPkgMoveFqName = null, + observableUiSettings = observableUiSettings, + ) } } } \ No newline at end of file diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ObservableUiSettings.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ObservableUiSettings.kt new file mode 100644 index 000000000000..005c5004377f --- /dev/null +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ObservableUiSettings.kt @@ -0,0 +1,33 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +package org.jetbrains.kotlin.idea.k2.refactoring.move.ui + +import com.intellij.openapi.observable.properties.ObservableBooleanProperty + +interface ObservableUiSettings : K2MoveModelObservableSettings { + fun registerK2MoveModelSettings(settings: K2MoveModelObservableSettings) +} + +interface K2MoveModelObservableSettings { + val searchReferencesObservable: ObservableBooleanProperty + val searchForTextObservable: ObservableBooleanProperty + val searchInCommentsObservable: ObservableBooleanProperty + val mppDeclarationsObservable: ObservableBooleanProperty +} + +internal class ObservableUiSettingsImpl : ObservableUiSettings { + private lateinit var k2MoveModelSettings: K2MoveModelObservableSettings + + override val searchReferencesObservable: ObservableBooleanProperty + get() = k2MoveModelSettings.searchReferencesObservable + override val searchForTextObservable: ObservableBooleanProperty + get() = k2MoveModelSettings.searchForTextObservable + override val searchInCommentsObservable: ObservableBooleanProperty + get() = k2MoveModelSettings.searchInCommentsObservable + override val mppDeclarationsObservable: ObservableBooleanProperty + get() = k2MoveModelSettings.mppDeclarationsObservable + + override fun registerK2MoveModelSettings(settings: K2MoveModelObservableSettings) { + k2MoveModelSettings = settings + } +}