From 906a3a598cdf29451aea8be4e5f6af4641237ab6 Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Mon, 18 Jul 2022 21:12:16 +0200 Subject: [PATCH] PY-52925: Fix file browsing for WSL and other remote targets User should have ability to browse target, not to type path manually GitOrigin-RevId: b309e0683d0bc2ab3bfe1444ce945cb235d9fb14 --- .../intellij/execution/target/TargetUIUtil.kt | 23 +++++-- .../execution/wsl/target/WslTargetType.kt | 5 +- .../BrowsableTargetEnvironmentType.java | 17 ++++- .../execution/target/GradleRuntimeTargetUI.kt | 2 +- .../python/configuration/EditSdkDialog.java | 41 +++++++++++- .../configuration/PythonSdkDetailsDialog.java | 6 +- .../jetbrains/python/sdk/PythonSdkType.java | 1 - .../sdk/add/PySdkPathChoosingComboBox.kt | 5 +- .../python/sdk/add/target/BrowsePaths.kt | 3 +- .../python/ui/ManualPathEntryDialog.kt | 50 -------------- .../remotePathEditor/ManualPathEntryDialog.kt | 66 +++++++++++++++++++ .../python/ui/ManualPathEntryDialogTest.kt | 1 + 12 files changed, 148 insertions(+), 72 deletions(-) delete mode 100644 python/src/com/jetbrains/python/ui/ManualPathEntryDialog.kt create mode 100644 python/src/com/jetbrains/python/ui/remotePathEditor/ManualPathEntryDialog.kt diff --git a/platform/execution-impl/src/com/intellij/execution/target/TargetUIUtil.kt b/platform/execution-impl/src/com/intellij/execution/target/TargetUIUtil.kt index 1b5abfa627ed..61fb1d6b8f00 100644 --- a/platform/execution-impl/src/com/intellij/execution/target/TargetUIUtil.kt +++ b/platform/execution-impl/src/com/intellij/execution/target/TargetUIUtil.kt @@ -16,19 +16,24 @@ import java.util.function.Supplier import javax.swing.JComponent import javax.swing.JPanel +/** + * See [BrowsableTargetEnvironmentType.createBrowser] + */ @Deprecated("Use overloaded method with Kotlin UI DSL 2 API") fun textFieldWithBrowseTargetButton(row: Row, targetType: BrowsableTargetEnvironmentType, targetSupplier: Supplier, project: Project, @NlsContexts.DialogTitle title: String, - property: PropertyBinding): CellBuilder { + property: PropertyBinding, + noLocalFs: Boolean = false): CellBuilder { val textFieldWithBrowseButton = TextFieldWithBrowseButton() val browser = targetType.createBrowser(project, title, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, textFieldWithBrowseButton.textField, - targetSupplier) + targetSupplier, + noLocalFs) textFieldWithBrowseButton.addActionListener(browser) textFieldWithBrowseButton.text = property.get() return row.component(textFieldWithBrowseButton).withBinding(TextFieldWithBrowseButton::getText, @@ -36,17 +41,21 @@ fun textFieldWithBrowseTargetButton(row: Row, property) } +/** + * See [BrowsableTargetEnvironmentType.createBrowser] + */ fun com.intellij.ui.dsl.builder.Row.textFieldWithBrowseTargetButton(targetType: BrowsableTargetEnvironmentType, - targetSupplier: Supplier, - project: Project, - @NlsContexts.DialogTitle title: String, - property: MutableProperty): Cell { + targetSupplier: Supplier, + project: Project, + @NlsContexts.DialogTitle title: String, + property: MutableProperty): Cell { val textFieldWithBrowseButton = TextFieldWithBrowseButton() val browser = targetType.createBrowser(project, title, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, textFieldWithBrowseButton.textField, - targetSupplier) + targetSupplier, + false) textFieldWithBrowseButton.addActionListener(browser) return cell(textFieldWithBrowseButton) .bind(TextFieldWithBrowseButton::getText, TextFieldWithBrowseButton::setText, property) diff --git a/platform/execution-impl/src/com/intellij/execution/wsl/target/WslTargetType.kt b/platform/execution-impl/src/com/intellij/execution/wsl/target/WslTargetType.kt index 45958e27d6a9..9f1bdee778f3 100644 --- a/platform/execution-impl/src/com/intellij/execution/wsl/target/WslTargetType.kt +++ b/platform/execution-impl/src/com/intellij/execution/wsl/target/WslTargetType.kt @@ -66,14 +66,15 @@ class WslTargetType : TargetEnvironmentType(T title: String?, textComponentAccessor: TextComponentAccessor, component: T, - configurationSupplier: Supplier): ActionListener = ActionListener { + configurationSupplier: Supplier, + noLocalFs: Boolean): ActionListener = ActionListener { val configuration = configurationSupplier.get() if (configuration is WslTargetEnvironmentConfiguration) { configuration.distribution?.let { WslPathBrowser(object : TextAccessor { override fun setText(text: String) = textComponentAccessor.setText(component, text) override fun getText() = textComponentAccessor.getText(component) - }).browsePath(it, component) + }).browsePath(it, component, accessWindowsFs = !noLocalFs) return@ActionListener } } diff --git a/platform/execution/src/com/intellij/execution/target/BrowsableTargetEnvironmentType.java b/platform/execution/src/com/intellij/execution/target/BrowsableTargetEnvironmentType.java index c1ea00af507d..84dcd072f323 100644 --- a/platform/execution/src/com/intellij/execution/target/BrowsableTargetEnvironmentType.java +++ b/platform/execution/src/com/intellij/execution/target/BrowsableTargetEnvironmentType.java @@ -10,18 +10,31 @@ import java.awt.*; import java.awt.event.ActionListener; import java.util.function.Supplier; +/** + * Environment type provides access to its filesystem for services like {@link com.intellij.openapi.ui.TextFieldWithBrowseButton} + * So you can browse (possibily remote) target. + */ public interface BrowsableTargetEnvironmentType { + /** + * @param textComponentAccessor where path should be set. See {@link TextComponentAccessor#TEXT_FIELD_WHOLE_TEXT} + * @param component text field component + * @param configurationSupplier returns environment configuration + * @param noLocalFs some targets (WSL is the only known for now) may provide access to the local filesystem. + * True means you do not need it + * @return Action listener should be installed on "browse" button you want to show target FS browser. + */ @NotNull ActionListener createBrowser(@NotNull Project project, @NlsContexts.DialogTitle String title, @NotNull TextComponentAccessor textComponentAccessor, @NotNull T component, - @NotNull Supplier configurationSupplier); + @NotNull Supplier configurationSupplier, + boolean noLocalFs); /** * When configurable contains both connection parameters and components using them (text fields with browsing in this case), * those components need to have current connection settings available, not the last applied to with [Configurable.apply]. - * + *

* This interface displays ability and provides API to get connection settings, which are shown in UI. See IDEA-255466. */ interface ConfigurableCurrentConfigurationProvider { diff --git a/plugins/gradle/src/org/jetbrains/plugins/gradle/execution/target/GradleRuntimeTargetUI.kt b/plugins/gradle/src/org/jetbrains/plugins/gradle/execution/target/GradleRuntimeTargetUI.kt index a6d45e782f34..7f89c7ed7fcb 100644 --- a/plugins/gradle/src/org/jetbrains/plugins/gradle/execution/target/GradleRuntimeTargetUI.kt +++ b/plugins/gradle/src/org/jetbrains/plugins/gradle/execution/target/GradleRuntimeTargetUI.kt @@ -67,7 +67,7 @@ class GradleRuntimeTargetUI(private val conf val configuration = configurationProvider.environmentConfiguration val targetType = configuration.getTargetType() as? BrowsableTargetEnvironmentType ?: break addTargetActionListener(configurationProvider.pathMapper, - targetType.createBrowser(project, title, TEXT_FIELD_WHOLE_TEXT, textField) { configuration }) + targetType.createBrowser(project, title, TEXT_FIELD_WHOLE_TEXT, textField, { configuration }, false)) return this } } diff --git a/python/src/com/jetbrains/python/configuration/EditSdkDialog.java b/python/src/com/jetbrains/python/configuration/EditSdkDialog.java index 48a5ec45dfac..ee9fdba9c321 100644 --- a/python/src/com/jetbrains/python/configuration/EditSdkDialog.java +++ b/python/src/com/jetbrains/python/configuration/EditSdkDialog.java @@ -1,10 +1,16 @@ // Copyright 2000-2021 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.jetbrains.python.configuration; +import com.intellij.execution.target.BrowsableTargetEnvironmentType; +import com.intellij.execution.target.TargetBasedSdkAdditionalData; +import com.intellij.execution.target.TargetEnvironmentConfigurationKt; import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.SdkAdditionalData; import com.intellij.openapi.projectRoots.SdkModificator; import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.TextComponentAccessor; import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.io.FileUtil; import com.intellij.ui.ClickListener; @@ -19,9 +25,11 @@ import com.jetbrains.python.sdk.flavors.CondaEnvSdkFlavor; import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.DocumentEvent; +import java.awt.event.ActionListener; import java.awt.event.MouseEvent; @@ -48,12 +56,20 @@ public class EditSdkDialog extends DialogWrapper { }); final String homePath = sdk.getHomePath(); myInterpreterPathTextField.setText(homePath); - myInterpreterPathTextField.addBrowseFolderListener(PyBundle.message("sdk.edit.dialog.specify.interpreter.path"), null, project, - PythonSdkType.getInstance().getHomeChooserDescriptor()); + var sdkAdditionalData = sdk.getSdkAdditionalData(); + var label = PyBundle.message("sdk.edit.dialog.specify.interpreter.path"); + var targetListener = createBrowseTargetListener(project, sdkAdditionalData, label); + if (targetListener != null) { + myInterpreterPathTextField.addActionListener(targetListener); + } + else { + myInterpreterPathTextField.addBrowseFolderListener(label, null, project, + PythonSdkType.getInstance().getHomeChooserDescriptor()); + } myRemoveAssociationLabel.setVisible(false); final PythonSdkFlavor sdkFlavor = PythonSdkFlavor.getPlatformIndependentFlavor(homePath); if ((sdkFlavor instanceof VirtualEnvSdkFlavor) || (sdkFlavor instanceof CondaEnvSdkFlavor)) { - PythonSdkAdditionalData data = (PythonSdkAdditionalData) sdk.getSdkAdditionalData(); + PythonSdkAdditionalData data = (PythonSdkAdditionalData)sdk.getSdkAdditionalData(); if (data != null) { final String path = data.getAssociatedModulePath(); if (path != null) { @@ -86,6 +102,25 @@ public class EditSdkDialog extends DialogWrapper { }.installOn(myRemoveAssociationLabel); } + /** + * @return action to call when user clicks on "browse" button if sdk is target SDK that supports browsing + */ + private @Nullable ActionListener createBrowseTargetListener(@NotNull Project project, @NotNull SdkAdditionalData sdkAdditionalData, + @NotNull @NlsContexts.DialogTitle String label) { + if (!(sdkAdditionalData instanceof TargetBasedSdkAdditionalData)) { + return null; + } + var configuration = ((TargetBasedSdkAdditionalData)sdkAdditionalData).getTargetEnvironmentConfiguration(); + if (configuration != null) { + var type = TargetEnvironmentConfigurationKt.getTargetType(configuration); + if (type instanceof BrowsableTargetEnvironmentType) { + return ((BrowsableTargetEnvironmentType)type).createBrowser(project, label, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, + myInterpreterPathTextField.getTextField(), () -> configuration, true); + } + } + return null; + } + @Override protected JComponent createCenterPanel() { return myMainPanel; diff --git a/python/src/com/jetbrains/python/configuration/PythonSdkDetailsDialog.java b/python/src/com/jetbrains/python/configuration/PythonSdkDetailsDialog.java index dd2681c165c2..a3e9d554bace 100644 --- a/python/src/com/jetbrains/python/configuration/PythonSdkDetailsDialog.java +++ b/python/src/com/jetbrains/python/configuration/PythonSdkDetailsDialog.java @@ -41,7 +41,7 @@ import com.jetbrains.python.sdk.*; import com.jetbrains.python.sdk.add.PyAddSdkDialog; import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; import com.jetbrains.python.target.PyTargetAwareAdditionalData; -import com.jetbrains.python.ui.ManualPathEntryDialog; +import com.jetbrains.python.ui.remotePathEditor.ManualPathEntryDialog; import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -554,8 +554,8 @@ public class PythonSdkDetailsDialog extends DialogWrapper { return remoteInterpreterManager.chooseRemoteFiles(myProject, (PyRemoteSdkAdditionalDataBase)sdkAdditionalData, false); } else if (sdkAdditionalData instanceof PyTargetAwareAdditionalData) { - // TODO [targets] Use proper file chooser dialog for corresponding target - ManualPathEntryDialog dialog = new ManualPathEntryDialog(myProject, Platform.UNIX); + var dialog = new ManualPathEntryDialog(myProject, Platform.UNIX, + ((PyTargetAwareAdditionalData)sdkAdditionalData).getTargetEnvironmentConfiguration()); if (dialog.showAndGet()) { return new String[]{dialog.getPath()}; } diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkType.java b/python/src/com/jetbrains/python/sdk/PythonSdkType.java index a7559fa980a8..c89cb456178d 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkType.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkType.java @@ -643,4 +643,3 @@ public final class PythonSdkType extends SdkType { return true; } } - diff --git a/python/src/com/jetbrains/python/sdk/add/PySdkPathChoosingComboBox.kt b/python/src/com/jetbrains/python/sdk/add/PySdkPathChoosingComboBox.kt index 4bf8ab9c616b..47e80f51b18f 100644 --- a/python/src/com/jetbrains/python/sdk/add/PySdkPathChoosingComboBox.kt +++ b/python/src/com/jetbrains/python/sdk/add/PySdkPathChoosingComboBox.kt @@ -35,7 +35,7 @@ import com.intellij.util.PathUtil import com.jetbrains.python.PyBundle import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.add.target.createDetectedSdk -import com.jetbrains.python.ui.ManualPathEntryDialog +import com.jetbrains.python.ui.remotePathEditor.ManualPathEntryDialog import java.awt.event.ActionListener import java.util.function.Supplier import javax.swing.JComboBox @@ -94,7 +94,8 @@ class PySdkPathChoosingComboBox @JvmOverloads constructor(sdks: List = empt title, PY_SDK_COMBOBOX_TEXT_ACCESSOR, childComponent, - Supplier { targetEnvironmentConfiguration }) + Supplier { targetEnvironmentConfiguration }, + true) } else { // The fallback where the path is entered manually diff --git a/python/src/com/jetbrains/python/sdk/add/target/BrowsePaths.kt b/python/src/com/jetbrains/python/sdk/add/target/BrowsePaths.kt index 1d78fb0fe74c..4dca18f5d628 100644 --- a/python/src/com/jetbrains/python/sdk/add/target/BrowsePaths.kt +++ b/python/src/com/jetbrains/python/sdk/add/target/BrowsePaths.kt @@ -34,6 +34,7 @@ fun TextFieldWithBrowseButton.withTargetBrowser(targetType: BrowsableTargetEnvir title, com.intellij.openapi.ui.TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, textField, - targetSupplier) + targetSupplier, + true) addActionListener(browser) } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/ui/ManualPathEntryDialog.kt b/python/src/com/jetbrains/python/ui/ManualPathEntryDialog.kt deleted file mode 100644 index b312374d8829..000000000000 --- a/python/src/com/jetbrains/python/ui/ManualPathEntryDialog.kt +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2000-2021 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.jetbrains.python.ui - -import com.intellij.execution.Platform -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.util.io.OSAgnosticPathUtil -import com.intellij.ui.layout.* -import com.jetbrains.python.PyBundle -import javax.swing.JComponent - -/** - * The dialog that allows to specify the path to a file or directory manually. - * - * Performs validation that the path that is being added is an absolute path on - * the specified [platform]. - */ -class ManualPathEntryDialog(project: Project?, private val platform: Platform) : DialogWrapper(project) { - var path: String = "" - private set - - init { - title = PyBundle.message("enter.path.dialog.title") - init() - } - - override fun createCenterPanel(): JComponent = - panel { - row(label = PyBundle.message("path.label")) { - textField(prop = ::path).withValidationOnApply { textField -> - val text = textField.text - when { - text.isBlank() -> error(PyBundle.message("path.must.not.be.empty.error.message")) - !isAbsolutePath(text, platform) -> error(PyBundle.message("path.must.be.absolute.error.message")) - text.endsWith(" ") -> warning(PyBundle.message("path.ends.with.whitespace.warning.message")) - else -> null - } - }.focused() - } - } - - companion object { - fun isAbsolutePath(path: String, platform: Platform): Boolean = when (platform) { - Platform.UNIX -> path.startsWith("/") - Platform.WINDOWS -> isAbsoluteWindowsPath(path) - } - - private fun isAbsoluteWindowsPath(path: String): Boolean = OSAgnosticPathUtil.isAbsoluteDosPath(path) - } -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/ui/remotePathEditor/ManualPathEntryDialog.kt b/python/src/com/jetbrains/python/ui/remotePathEditor/ManualPathEntryDialog.kt new file mode 100644 index 000000000000..6c2b19337bbe --- /dev/null +++ b/python/src/com/jetbrains/python/ui/remotePathEditor/ManualPathEntryDialog.kt @@ -0,0 +1,66 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.ui.remotePathEditor + +import com.intellij.execution.Platform +import com.intellij.execution.target.BrowsableTargetEnvironmentType +import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.execution.target.getTargetType +import com.intellij.execution.target.textFieldWithBrowseTargetButton +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.io.OSAgnosticPathUtil +import com.intellij.ui.layout.* +import com.jetbrains.python.PyBundle +import java.util.function.Supplier +import javax.swing.JComponent + +/** + * The dialog that allows to specify the path to a file or directory manually. + * + * Performs validation that the path that is being added is an absolute path on + * the specified [platform]. + */ +class ManualPathEntryDialog(private val project: Project?, + private val platform: Platform = Platform.UNIX, + targetConfig: TargetEnvironmentConfiguration? = null) : DialogWrapper(project) { + + private val targetConfigAndType: Pair? = + (targetConfig?.getTargetType() as? BrowsableTargetEnvironmentType)?.let { Pair(targetConfig, it) } + var path: String = "" + private set + + init { + title = PyBundle.message("enter.path.dialog.title") + init() + } + + override fun createCenterPanel(): JComponent { + val label = PyBundle.message("path.label") + return panel { + row(label = label) { + val textFieldComponent = if (targetConfigAndType == null) + textField(prop = ::path) + else + textFieldWithBrowseTargetButton(this, targetConfigAndType.second, Supplier { targetConfigAndType.first }, project!!, label, this@ManualPathEntryDialog::path.toBinding(), false) + textFieldComponent.withValidationOnApply { textField -> + val text = textField.text + when { + text.isBlank() -> error(PyBundle.message("path.must.not.be.empty.error.message")) + !isAbsolutePath(text, platform) -> error(PyBundle.message("path.must.be.absolute.error.message")) + text.endsWith(" ") -> warning(PyBundle.message("path.ends.with.whitespace.warning.message")) + else -> null + } + }.focused() + } + } + } + + companion object { + fun isAbsolutePath(path: String, platform: Platform): Boolean = when (platform) { + Platform.UNIX -> path.startsWith("/") + Platform.WINDOWS -> isAbsoluteWindowsPath(path) + } + + private fun isAbsoluteWindowsPath(path: String): Boolean = OSAgnosticPathUtil.isAbsoluteDosPath(path) + } +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/ui/ManualPathEntryDialogTest.kt b/python/testSrc/com/jetbrains/python/ui/ManualPathEntryDialogTest.kt index 232ec0f20fd4..9e16f17d0966 100644 --- a/python/testSrc/com/jetbrains/python/ui/ManualPathEntryDialogTest.kt +++ b/python/testSrc/com/jetbrains/python/ui/ManualPathEntryDialogTest.kt @@ -2,6 +2,7 @@ package com.jetbrains.python.ui import com.intellij.execution.Platform +import com.jetbrains.python.ui.remotePathEditor.ManualPathEntryDialog import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith