mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-15 02:59:33 +07:00
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
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3dd5b1ed3e
commit
906a3a598c
@@ -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<out TargetEnvironmentConfiguration>,
|
||||
project: Project,
|
||||
@NlsContexts.DialogTitle title: String,
|
||||
property: PropertyBinding<String>): CellBuilder<TextFieldWithBrowseButton> {
|
||||
property: PropertyBinding<String>,
|
||||
noLocalFs: Boolean = false): CellBuilder<TextFieldWithBrowseButton> {
|
||||
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<out TargetEnvironmentConfiguration>,
|
||||
project: Project,
|
||||
@NlsContexts.DialogTitle title: String,
|
||||
property: MutableProperty<String>): Cell<TextFieldWithBrowseButton> {
|
||||
targetSupplier: Supplier<out TargetEnvironmentConfiguration>,
|
||||
project: Project,
|
||||
@NlsContexts.DialogTitle title: String,
|
||||
property: MutableProperty<String>): Cell<TextFieldWithBrowseButton> {
|
||||
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)
|
||||
|
||||
@@ -66,14 +66,15 @@ class WslTargetType : TargetEnvironmentType<WslTargetEnvironmentConfiguration>(T
|
||||
title: String?,
|
||||
textComponentAccessor: TextComponentAccessor<T>,
|
||||
component: T,
|
||||
configurationSupplier: Supplier<out TargetEnvironmentConfiguration>): ActionListener = ActionListener {
|
||||
configurationSupplier: Supplier<out TargetEnvironmentConfiguration>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <T extends Component> ActionListener createBrowser(@NotNull Project project,
|
||||
@NlsContexts.DialogTitle String title,
|
||||
@NotNull TextComponentAccessor<T> textComponentAccessor,
|
||||
@NotNull T component,
|
||||
@NotNull Supplier<? extends TargetEnvironmentConfiguration> configurationSupplier);
|
||||
@NotNull Supplier<? extends TargetEnvironmentConfiguration> 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].
|
||||
*
|
||||
* <p>
|
||||
* This interface displays ability and provides API to get connection settings, which are shown in UI. See IDEA-255466.
|
||||
*/
|
||||
interface ConfigurableCurrentConfigurationProvider {
|
||||
|
||||
@@ -67,7 +67,7 @@ class GradleRuntimeTargetUI<C : TargetEnvironmentConfiguration>(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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()};
|
||||
}
|
||||
|
||||
@@ -643,4 +643,3 @@ public final class PythonSdkType extends SdkType {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Sdk> = empt
|
||||
title,
|
||||
PY_SDK_COMBOBOX_TEXT_ACCESSOR,
|
||||
childComponent,
|
||||
Supplier { targetEnvironmentConfiguration })
|
||||
Supplier { targetEnvironmentConfiguration },
|
||||
true)
|
||||
}
|
||||
else {
|
||||
// The fallback where the path is entered manually
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<TargetEnvironmentConfiguration, BrowsableTargetEnvironmentType>? =
|
||||
(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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user